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.GeoDataFrames.

Folium resources

Find more information about the capabilities of the Folium package on its official web pages:

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.

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)
import folium

interactive_map = folium.Map(
    location=(60.2, 24.8),
    zoom_start=10,
    control_scale=True
)

interactive_map
Make this Notebook Trusted to load map: File -> Trust Notebook

Save the resulting map#

To save this map to an HTML file that can be opened in any web browser, use folium.Map.save():

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 or point to a custom tileset URL.

While we’re at it, let’s also vary the centre location and the zoom level of the map:

interactive_map = folium.Map(
    location=(40.7, -73.9),
    zoom_start=12,
    tiles="Stamen Toner"
)
interactive_map
Make this Notebook Trusted to load map: File -> Trust Notebook

Add a point marker#

To add a single marker to a Folium map, create a folium.Marker. Supply a folium.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.

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
Make this Notebook Trusted to load map: File -> Trust Notebook

Add a layer of points#

Folium also supports to add entire layers, for instance, as geopandas.GeoDataFrames. Folium implements Leaflet’s geoJSON layers 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.

import geopandas

addresses = geopandas.read_file(DATA_DIRECTORY / "addresses.gpkg")
addresses
address geometry
0 Ruoholahti, 14, Itämerenkatu, Ruoholahti, Läns... POINT (24.91556 60.16320)
1 Kamppi, 1, Kampinkuja, Kamppi, Eteläinen suurp... POINT (24.93166 60.16905)
2 Bangkok9, 8, Kaivokatu, Keskusta, Kluuvi, Etel... POINT (24.94168 60.16996)
3 Hermannin rantatie, Verkkosaari, Kalasatama, S... POINT (24.97865 60.19005)
4 9, Tyynenmerenkatu, Jätkäsaari, Länsisatama, E... POINT (24.92151 60.15662)
5 18, Kontulantie, Kontula, Mellunkylä, Itäinen ... POINT (25.08174 60.23522)
6 Itäväylä, Vartioharju, Vartiokylä, Itäinen suu... POINT (25.10651 60.21957)
7 Tapulikaupungintie, Tapulikaupunki, Suutarila,... POINT (25.02948 60.27858)
8 Sompionpolku, Fallkullan kiila, Tapanila, Tapa... POINT (25.02867 60.26370)
9 Sodexo, 5, Atomitie, Strömberg, Pitäjänmäen te... POINT (24.87103 60.22250)
10 Metro Rautatientori D, 1, Simonkatu, Kamppi, E... POINT (24.93774 60.16975)
11 Kuparitie, Lassila, Haaga, Läntinen suurpiiri,... POINT (24.88281 60.23080)
12 Rumpupolku, Kannelmäki, Kaarela, Läntinen suur... POINT (24.87613 60.23916)
13 K-Supermarket Mustapekka, 1, Mäkitorpantie, Pa... POINT (24.94801 60.22179)
14 Yliopiston Apteekki, 15, Malminkaari, Ala-Malm... POINT (25.01295 60.25107)
15 23, Kylätie, Etelä-Haaga, Haaga, Läntinen suur... POINT (24.89418 60.21722)
16 Malminkartanontie, Malminkartano, Kaarela, Län... POINT (24.86592 60.25035)
17 Oulunkylän tori, Patola, Oulunkylä, Pohjoinen ... POINT (24.96566 60.22982)
18 6, Ratapihantie, Itä-Pasila, Pasila, Keskinen ... POINT (24.93435 60.19857)
19 Pizza Benitto, 15, Pitäjänmäentie, Reimarla, P... POINT (24.86084 60.22402)
20 K-Market Pukinmäki, 2, Eskolantie, Savela, Puk... POINT (24.99362 60.24365)
21 Tattariharjuntie, Ala-Malmi, Malmi, Koillinen ... POINT (25.02055 60.24340)
22 Danske Bank, 1, Tallinnanaukio, Itäkeskus, Var... POINT (25.07835 60.20982)
23 Tyynylaavantie, Keski-Vuosaari, Vuosaari, Itäi... POINT (25.13517 60.20727)
24 Myllypurontie, Myllypuro, Vartiokylä, Itäinen ... POINT (25.07478 60.22531)
25 Mellunmäenraitio, Mellunmäki, Mellunkylä, Itäi... POINT (25.10979 60.23785)
26 Vaasanpolku, Kurvi, Harju, Alppiharju, Keskine... POINT (24.96108 60.18801)
27 Alko, 2, Hiihtäjäntie, Länsi-Herttoniemi, Hert... POINT (25.02832 60.19442)
28 Metro Kulosaari, 2, Ukko-Pekan porras, Kulosaa... POINT (25.00681 60.18872)
29 K-Market, 16, Siltasaarenkatu, Siltasaari, Kal... POINT (24.94954 60.17944)
30 Kampin keskus, 1, Urho Kekkosen katu, Kamppi, ... POINT (24.93312 60.16909)
31 Ruoholahdenkatu, Hietalahti, Kamppi, Eteläinen... POINT (24.93028 60.16650)
32 3, Tyynenmerenkatu, Jätkäsaari, Länsisatama, E... POINT (24.92121 60.15878)
33 Oluthuone Kaisla, 4, Vilhonkatu, Kaisaniemi, K... POINT (24.94709 60.17191)
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
Make this Notebook Trusted to load map: File -> Trust Notebook

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:

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()
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.

population_grid = population_grid[["index", "asukkaita", "geometry"]]
population_grid = population_grid.rename(columns={
    "asukkaita": "population"
})

Index column for choropleth maps

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 joint (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.

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)

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
Make this Notebook Trusted to load map: File -> Trust Notebook

To make the map slightly nicer, let’s still request more categories (bins), change the colour range (using fill_color), set the line thickness to zero, and add a layer name to the legend:

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",

    bins=9,
    fill_color="YlOrRd",
    line_weight=0,
    legend_name="Population, 2020",

    highlight=True
)
population_grid_layer.add_to(interactive_map)

interactive_map
Make this Notebook Trusted to load map: File -> Trust Notebook

Add a tooltip to a choropleth map#

In such an interactive map, it would be nice to display the value of each grid cell polygon when hovering the mouse over it. Folium does not support this out-of-the-box, but with a simple trick, we can extend its functionality: We add a transparent polygon layer using a ‘basic‘ folium.features.GeoJson, and configure it to display tooltips.

We can keep the map we created above, and simply add another layer to it.

# folium GeoJson layers expect a styling function,
# that receives each of the map’s feature and returns
# an individual style. It can, however, also return a
# static style:
def style_function(feature):
    return {
        "color": "transparent",
        "fillColor": "transparent"
    }


# More complex tooltips can be created using the
# `folium.features.GeoJsonTooltip` class. Below, we use
# its most basic features: `fields` specifies which columns
# should be displayed, `aliases` how they should be labelled.
tooltip = folium.features.GeoJsonTooltip(
    fields=("population",),
    aliases=("Population:",)
)


tooltip_layer = folium.features.GeoJson(
    population_grid,
    style_function=style_function,
    tooltip=tooltip
)
tooltip_layer.add_to(interactive_map)

interactive_map
Make this Notebook Trusted to load map: File -> Trust Notebook

Python packages for interactive (web) maps

Folium is just one of many packages that provide an easy way to create interactive maps using data stored in (geo-)pandas data frames. Other interesting libraries include: