Interactive maps#
Online maps have been interactive for a long time: virtually all online maps allow to zoom in and out, to pan the map extent, and to select map features, or otherwise query information about them.
Interactive content in web pages, such as online maps, are typically implemented using JavaScript/ECMAScript, a scripting language originally targeted at web pages, primarily, but used for many other applications.
In the open source realm, there exist a number of different JavaScript libraries for interactive web cartography, including Leaflet, which we will use in this lesson, and OpenLayers.
No worries, we will not have to write a single line of JavaScript; this is a Python course, after all. Rather, we will take advantage of the Folium Python package: it helps create interactive Leaflet maps from data stored in geopandas.GeoDataFrame
s.
:::{admonition} Folium resources :class: note
Find more information about the capabilities of the Folium package on its official web pages: - Documentation - Example gallery - Quickstart tutorial ::
Create a simple interactive web map#
We will start by creating a simple interactive web map that contains nothing but a base map. This is so we get acustomed to how Folium’s syntax works, and which steps we have to take.
We create a folium.Map
object, and specify centred around which location
and at which initial zoom level (~0-20) a map shall be displayed. By setting control_scale
to True
, we make Folium display a scale bar.
[1]:
import pathlib
NOTEBOOK_PATH = pathlib.Path().resolve()
DATA_DIRECTORY = NOTEBOOK_PATH / "data"
# We will export HTML pages during this lesson,
# let’s also prepare an output directory for them:
HTML_DIRECTORY = NOTEBOOK_PATH / "html"
HTML_DIRECTORY.mkdir(exist_ok=True)
[2]:
import folium
interactive_map = folium.Map(
location=(60.2, 24.8),
zoom_start=10,
control_scale=True
)
interactive_map
[2]:
Save the resulting map#
To save this map to an HTML file that can be opened in any web browser, use `folium.Map.save()
<https://python-visualization.github.io/branca/element.html#branca.element.Element.save>`__:
[3]:
interactive_map.save(HTML_DIRECTORY / "base-map.html")
Change the base map#
If you want to use a different base layer than the default OpenStreetMap, folium.Map
accepts a parameter tiles
, that can either reference one of the built-in map providers.
While we’re at it, let’s also vary the centre location and the zoom level of the map:
[4]:
interactive_map = folium.Map(
location=(60.2, 25.00),
zoom_start=12,
tiles="cartodbpositron"
)
interactive_map
[4]:
Or we can point to a custom tileset URL:
[5]:
interactive_map = folium.Map(
location=(60.2, 25.00),
zoom_start=12,
tiles="https://mt1.google.com/vt/lyrs=r&x={x}&y={y}&z={z}",
attr="Google maps",
)
interactive_map
[5]:
Add a point marker#
To add a single marker to a Folium map, create a `folium.Marker
<https://python-visualization.github.io/folium/modules.html#folium.map.Marker>`__. Supply a `folium.Icon
<https://python-visualization.github.io/folium/modules.html#folium.map.Icon>`__ as a parameter icon
to influence how the marker is styled, and set tooltip
to display a text when the mouse pointer hovers over it.
[6]:
interactive_map = folium.Map(
location=(60.2, 25.0),
zoom_start=12
)
kumpula = folium.Marker(
location=(60.204, 24.962),
tooltip="Kumpula Campus",
icon=folium.Icon(color="green", icon="ok-sign")
)
kumpula.add_to(interactive_map)
interactive_map
[6]:
Add a layer of points#
Folium also supports to add entire layers, for instance, as geopandas.GeoDataFrames
. Folium implements Leaflet’s ``geoJSON` layers <https://leafletjs.com/reference.html#geojson>`__ in its folium.features.GeoJson
class. We can initialise such a class (and layer) with a geo-data frame, and add it to a map. In the example below, we use the addresses.gpkg
data set we create in lesson 3.
[7]:
import geopandas
addresses = geopandas.read_file(DATA_DIRECTORY / "addresses.gpkg")
addresses.head()
[7]:
address | geometry | |
---|---|---|
0 | Ruoholahti, 14, Itämerenkatu, Ruoholahti, Läns... | POINT (24.91556 60.16320) |
1 | 1, Kampinkuja, Kamppi, Eteläinen suurpiiri, He... | POINT (24.93009 60.16846) |
2 | Espresso House, 8, Kaivokatu, Keskusta, Kluuvi... | POINT (24.94153 60.17016) |
3 | Hermannin rantatie, Verkkosaari, Kalasatama, S... | POINT (24.97853 60.19050) |
4 | 9, Tyynenmerenkatu, Jätkäsaari, Länsisatama, E... | POINT (24.92169 60.15667) |
[8]:
interactive_map = folium.Map(
location=(60.2, 25.0),
zoom_start=12
)
addresses_layer = folium.features.GeoJson(
addresses,
name="Public transport stops"
)
addresses_layer.add_to(interactive_map)
interactive_map
[8]:
We can also add a pop-up window to our map which would show the addresses at the point of interest upon clicking:
[9]:
interactive_map = folium.Map(
location=(60.2, 25.0),
zoom_start=12
)
popup = folium.GeoJsonPopup(
fields=["address"],
aliases=["Address"],
localize=True,
labels=True,
style="background-color: yellow;",
)
addresses_layer = folium.features.GeoJson(
addresses,
name="Public transport stops",
popup=popup
)
addresses_layer.add_to(interactive_map)
interactive_map
[9]:
Add a polygon layer#
In the following section we are going to revisit another data set with which we have worked before: the Helsinki Region population grid we got to know in lesson 2, and which you used during exercise 3. We can load the layer directly from HSY’s open data WFS endpoint:
[10]:
# To ignore the SSL certificate issue
import ssl
ssl._create_default_https_context = ssl._create_unverified_context
population_grid = (
geopandas.read_file(
"https://kartta.hsy.fi/geoserver/wfs"
"?service=wfs"
"&version=2.0.0"
"&request=GetFeature"
"&typeName=asuminen_ja_maankaytto:Vaestotietoruudukko_2020"
"&srsName=EPSG:4326"
"&bbox=24.6,60.1,25.2,60.4,EPSG:4326"
)
.set_crs("EPSG:4326")
)
population_grid.head()
[10]:
gml_id | index | asukkaita | asvaljyys | ika0_9 | ika10_19 | ika20_29 | ika30_39 | ika40_49 | ika50_59 | ika60_69 | ika70_79 | ika_yli80 | geometry | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | Vaestotietoruudukko_2020.174 | 3952 | 7 | 86 | 99 | 99 | 99 | 99 | 99 | 99 | 99 | 99 | 99 | POLYGON ((24.59351 60.26574, 24.59348 60.26798... |
1 | Vaestotietoruudukko_2020.175 | 3958 | 17 | 105 | 99 | 99 | 99 | 99 | 99 | 99 | 99 | 99 | 99 | POLYGON ((24.59367 60.25227, 24.59365 60.25452... |
2 | Vaestotietoruudukko_2020.176 | 3959 | 13 | 65 | 99 | 99 | 99 | 99 | 99 | 99 | 99 | 99 | 99 | POLYGON ((24.59370 60.25003, 24.59367 60.25227... |
3 | Vaestotietoruudukko_2020.177 | 3960 | 29 | 65 | 99 | 99 | 99 | 99 | 99 | 99 | 99 | 99 | 99 | POLYGON ((24.59373 60.24779, 24.59370 60.25003... |
4 | Vaestotietoruudukko_2020.178 | 3961 | 14 | 70 | 99 | 99 | 99 | 99 | 99 | 99 | 99 | 99 | 99 | POLYGON ((24.59376 60.24554, 24.59373 60.24779... |
Let’s first clean the data frame: drop all columns we don’t need, and rename the remaining ones to English.
[11]:
population_grid = population_grid[["index", "asukkaita", "geometry"]]
population_grid = population_grid.rename(columns={
"asukkaita": "population"
})
:::{admonition} Index column for choropleth maps :class: hint
We will use the folium.Choropleth
to display the population grid. Choropleth maps are more than simply polygon geometries, which could be displayed as a folium.features.GeoJson
layer, just like we used for the address points, above. Rather, the class takes care of categorising data, adding a legend, and a few more small tasks to quickly create beautiful thematic maps.
The class expects an input data set that has an explicit, str
-type, index column, as it treats the geospatial input and the thematic input as separate data sets that need to be joined (see also, below, how we specify both geo_data
and data
).
A good approach to create such a column is to copy the data frame’s index into a new column, for instance id
. ::
..
[12]:
population_grid["id"] = population_grid.index.astype(str)
Now we can create the polygon choropleth layer, and add it to a map object. Due to the slightly complex architecture of Folium, we have to supply a number of parameters: - geo_data
and data
, the geospatial and thematic input data sets, respectively. Can be the same geopandas.GeoDataFrame
. - columns
: a tuple of the names of relevant columns in data
: a unique index column, and the column containing thematic data - key_on
: which column in geo_data
to use for joining
data
(this is basically identical to columns
, except it’s only the first value)
[13]:
interactive_map = folium.Map(
location=(60.17, 24.94),
zoom_start=12
)
population_grid_layer = folium.Choropleth(
geo_data=population_grid,
data=population_grid,
columns=("id", "population"),
key_on="feature.id"
)
population_grid_layer.add_to(interactive_map)
interactive_map
[13]: