Interactive maps

In this tutorial we will learn how to publish data from Python on interactive leaflet.js maps.

JavaScript (JS) is a programming language for adding interactive content (such a zoomamble maps!) on webpages. Leaflet is a popular JavaScript library for creating interactive maps for webpages (OpenLayers is another JavaScript library for the same purpose).

Here, will mainly focus on Folium - a Python library that makes it easy to convert data from (Geo)DataFrames into interactive Leaflet maps.

Explore also…

Other interesting libraries for creating interactive visualizations from spatial data:

Folium

Folium is a Python library that makes it possible visualize data on an interactive Leaflet map.

Resources:

Creating a simple interactive web-map

Import folium and other useful packages:

import folium
from pyproj import crs
import geopandas as gpd
import matplotlib.pyplot as plt

We will start by creating a simple interactive web-map without any data on it. We just visualize OpenStreetMap on a specific location of the world.

First thing that we need to do is to create a Map instance and define a location for zooming in the data:

# Create a Map instance
m = folium.Map(location=[60.25, 24.8], zoom_start=10, control_scale=True)

The first parameter location takes a pair of lat, lon values as list as an input which will determine where the map will be positioned when user opens up the map. zoom_start -parameter adjusts the default zoom-level for the map (the higher the number the closer the zoom is). control_scale defines if map should have a scalebar or not.

Let’s see what our map looks like:

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

We can also save the map as a html file:

outfp = "base_map.html"
m.save(outfp)

You should now see a html file in your working directory. You can open the file in a web-browser in order to see the map, or in a text editor in order to see the source definition.

Let’s create another map with different settings (location, bacground map, zoom levels etc). See documentation of the Map() object for all avaiable options.

tiles -parameter is used for changing the background map provider and map style (see the documentation for all in-built options).

# Let's change the basemap style to 'Stamen Toner'
m = folium.Map(location=[40.730610, -73.935242], tiles='Stamen Toner',
                zoom_start=12, control_scale=True, prefer_canvas=True)

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

Adding layers to the map

Let’s first have a look how we can add a simple marker on the webmap:

# Create a Map instance
m = folium.Map(location=[60.20, 24.96],
    zoom_start=12, control_scale=True)

# Add marker
# Run: help(folium.Icon) for more info about icons
folium.Marker(
    location=[60.20426, 24.96179],
    popup='Kumpula Campus',
    icon=folium.Icon(color='green', icon='ok-sign'),
).add_to(m)

#Show map
m
Make this Notebook Trusted to load map: File -> Trust Notebook

As mentioned, Folium combines the strenghts of data manipulation in Python with the mapping capabilities of Leaflet.js. Eventually, we would like to include the plotting of interactive maps as the last part of our data analysis workflow.

Let’s see how we can plot data from a geodataframe using folium.

# File path
points_fp = r"data/addresses.shp"

# Read the data
points = gpd.read_file(points_fp)

#Check input data
points.head()
address id geometry
0 Kampinkuja 1, 00100 Helsinki, Finland 1001 POINT (24.93017 60.16837)
1 Kaivokatu 8, 00101 Helsinki, Finland 1002 POINT (24.94189 60.16987)
2 Hermanstads strandsväg 1, 00580 Helsingfors, F... 1003 POINT (24.97740 60.18736)
3 Itäväylä, 00900 Helsinki, Finland 1004 POINT (25.09196 60.21448)
4 Tyynenmerenkatu 9, 00220 Helsinki, Finland 1005 POINT (24.92148 60.15658)
points.head()
address id geometry
0 Kampinkuja 1, 00100 Helsinki, Finland 1001 POINT (24.93017 60.16837)
1 Kaivokatu 8, 00101 Helsinki, Finland 1002 POINT (24.94189 60.16987)
2 Hermanstads strandsväg 1, 00580 Helsingfors, F... 1003 POINT (24.97740 60.18736)
3 Itäväylä, 00900 Helsinki, Finland 1004 POINT (25.09196 60.21448)
4 Tyynenmerenkatu 9, 00220 Helsinki, Finland 1005 POINT (24.92148 60.15658)
  • conver the points to GeoJSON features using folium:

# Convert points to GeoJSON
points_gjson = folium.features.GeoJson(points, name="Public transport stations")
# Check the GeoJSON features
#points_gjson.data.get('features')

Now we have our population data stored as GeoJSON format which basically contains the data as text in a similar way that it would be written in the .geojson -file.

Add the points onto the Helsinki basemap:

# Create a Map instance
m = folium.Map(location=[60.25, 24.8], tiles = 'cartodbpositron', zoom_start=11, control_scale=True)

# Add points to the map instance
points_gjson.add_to(m)

# Alternative syntax for adding points to the map instance
#m.add_child(points_gjson)

#Show map
m
Make this Notebook Trusted to load map: File -> Trust Notebook

Layer control

We can also add a LayerControl object on our map, which allows the user to control which map layers are visible. See the documentation for available parameters (you can e.g. change the position of the layer control icon).

# Create a layer control object and add it to our map instance
folium.LayerControl().add_to(m)

#Show map
m
Make this Notebook Trusted to load map: File -> Trust Notebook

Heatmap

Folium plugins allow us to use popular tools available in leaflet. One of these plugins is HeatMap, which creates a heatmap layer from input points.

Let’s visualize a heatmap of the public transport stations in Helsinki using the addresses input data. folium.plugins.HeatMap requires a list of points, or a numpy array as input, so we need to first manipulate the data a bit:

# Get x and y coordinates for each point
points["x"] = points["geometry"].x
points["y"] = points["geometry"].y

# Create a list of coordinate pairs
locations = list(zip(points["y"], points["x"]))

Check the data:

locations
[(60.1683731, 24.9301701),
 (60.1698665, 24.9418933),
 (60.18735880000001, 24.9774004),
 (60.21448089999999, 25.0919641),
 (60.1565781, 24.9214846),
 (60.23489060000001, 25.0816923),
 (60.2033879, 25.042239),
 (60.2753891, 25.035855),
 (60.2633799, 25.0291078),
 (60.22243630000001, 24.8718598),
 (60.1711874, 24.94251),
 (60.2306474, 24.8840504),
 (60.240163, 24.877383),
 (60.22163339999999, 24.9483202),
 (60.25149829999999, 25.0125655),
 (60.2177823, 24.893153),
 (60.2485471, 24.86186),
 (60.2291135, 24.9670533),
 (60.1986856, 24.9334051),
 (60.22401389999999, 24.8609335),
 (60.2436961, 24.9934979),
 (60.24444239999999, 25.040583),
 (60.20966609999999, 25.0778094),
 (60.20751019999999, 25.1424936),
 (60.225599, 25.0756547),
 (60.2382054, 25.1080054),
 (60.18789030000001, 24.9609122),
 (60.19413939999999, 25.0291263),
 (60.18837519999999, 25.0068399),
 (60.1793862, 24.9494874),
 (60.1694809, 24.9337569),
 (60.16500139999999, 24.9250072),
 (60.159069, 24.9214046),
 (60.1719108, 24.9468514),
 (60.20548979999999, 25.1204966)]
from folium.plugins import HeatMap

# Create a Map instance
m = folium.Map(location=[60.25, 24.8], tiles = 'stamentoner', zoom_start=10, control_scale=True)

# Add heatmap to map instance
# Available parameters: HeatMap(data, name=None, min_opacity=0.5, max_zoom=18, max_val=1.0, radius=25, blur=15, gradient=None, overlay=True, control=True, show=True)
HeatMap(locations).add_to(m)

# Show map
m
Make this Notebook Trusted to load map: File -> Trust Notebook

Clustered point map

Let’s visualize the address points (locations of transport stations in Helsinki) on top of the choropleth map using clustered markers using folium’s MarkerCluster class.

from folium.plugins import MarkerCluster
# Create a Map instance
m = folium.Map(location=[60.25, 24.8], tiles = 'cartodbpositron', zoom_start=11, control_scale=True)
# Following this example: https://github.com/python-visualization/folium/blob/master/examples/MarkerCluster.ipynb

# Get x and y coordinates for each point
points["x"] = points["geometry"].x
points["y"] = points["geometry"].y

# Create a list of coordinate pairs
locations = list(zip(points["y"], points["x"]))
# Create a folium marker cluster
marker_cluster = MarkerCluster(locations)

# Add marker cluster to map
marker_cluster.add_to(m)

# Show map
m
Make this Notebook Trusted to load map: File -> Trust Notebook

Choropleth map

Next, let’s check how we can overlay a population map on top of a basemap using folium’s choropleth method. This method is able to read the geometries and attributes directly from a geodataframe. This example is modified from the Folium quicksart.

  • First read in the population grid from HSY wfs like we did in lesson 3:

import geopandas as gpd
from pyproj import CRS
import requests
import geojson

# Specify the url for web feature service
url = 'https://kartta.hsy.fi/geoserver/wfs'

# Specify parameters (read data in json format).
# Available feature types in this particular data source: http://geo.stat.fi/geoserver/vaestoruutu/wfs?service=wfs&version=2.0.0&request=describeFeatureType
params = dict(service='WFS',
              version='2.0.0',
              request='GetFeature',
              typeName='asuminen_ja_maankaytto:Vaestotietoruudukko_2020',
              outputFormat='json')

# Fetch data from WFS using requests
r = requests.get(url, params=params)

# Create GeoDataFrame from geojson
data = gpd.GeoDataFrame.from_features(geojson.loads(r.content))
# Clean overall cell
data = data[data['index'] != 27699]
# Check the data
data.head()
geometry index asukkaita asvaljyys ika0_9 ika10_19 ika20_29 ika30_39 ika40_49 ika50_59 ika60_69 ika70_79 ika_yli80
0 POLYGON ((25472499.995 6685998.998, 25472499.9... 703 5 51 99 99 99 99 99 99 99 99 99
1 POLYGON ((25472499.995 6684249.004, 25472499.9... 710 8 44 99 99 99 99 99 99 99 99 99
2 POLYGON ((25472499.995 6683999.005, 25472499.9... 711 5 90 99 99 99 99 99 99 99 99 99
3 POLYGON ((25472499.995 6682998.998, 25472499.9... 715 13 34 99 99 99 99 99 99 99 99 99
4 POLYGON ((25472749.993 6690249.003, 25472749.9... 848 5 53 99 99 99 99 99 99 99 99 99
from pyproj import CRS
# Define crs
data.crs = CRS.from_epsg(3879)

Re-project layer into WGS 84 (epsg: 4326)

# Re-project to WGS84
data = data.to_crs(epsg=4326)

# Check layer crs definition
print(data.crs)
epsg:4326

Rename columns

# Change the name of a column
data = data.rename(columns={'asukkaita': 'pop20'})
# Create a Geo-id which is needed by the Folium (it needs to have a unique identifier for each row)
data['geoid'] = data.index.astype(str)
# Select only needed columns
data = data[['geoid', 'pop20', 'geometry']]

# Convert to geojson (not needed for the simple coropleth map!)
#pop_json = data.to_json()

#check data
data.head()
geoid pop20 geometry
0 0 5 POLYGON ((24.50287 60.28562, 24.50284 60.28787...
1 1 8 POLYGON ((24.50311 60.26992, 24.50308 60.27216...
2 2 5 POLYGON ((24.50315 60.26767, 24.50311 60.26992...
3 3 13 POLYGON ((24.50328 60.25870, 24.50325 60.26094...
4 4 5 POLYGON ((24.50682 60.32378, 24.50678 60.32603...

Create an interactive choropleth map from the population grid:

# Create a Map instance
m = folium.Map(location=[60.25, 24.8], tiles = 'cartodbpositron', zoom_start=10, control_scale=True)

# Plot a choropleth map
# Notice: 'geoid' column that we created earlier needs to be assigned always as the first column
folium.Choropleth(
    geo_data=data,
    name='Population in 2020',
    data=data,
    columns=['geoid', 'pop20'],
    key_on='feature.id',
    fill_color='YlOrRd',
    fill_opacity=0.7,
    line_opacity=0.2,
    line_color='white', 
    line_weight=0,
    highlight=False, 
    smooth_factor=1.0,
    #threshold_scale=[100, 250, 500, 1000, 2000],
    legend_name= 'Population in Helsinki').add_to(m)

#Show map
m
Make this Notebook Trusted to load map: File -> Trust Notebook