{ "cells": [ { "cell_type": "markdown", "metadata": { "deletable": true, "editable": true }, "source": [ "# Interactive maps\n", "\n", "In this tutorial we will learn how to publish data from Python on interactive [leaflet.js](http://leafletjs.com/) maps. \n", "\n", "JavaScript (JS) is a programming language for adding interactive content (such a zoomamble maps!) on webpages. [Leaflet](http://leafletjs.com/) is a popular JavaScript library for creating interactive maps for webpages ([OpenLayers](https://openlayers.org/) is another JavaScript library for the same purpose). \n", "\n", "Here, will mainly focus on [Folium](https://python-visualization.github.io/folium/) - a Python library that makes it easy to convert data from (Geo)DataFrames into interactive Leaflet maps.\n", "\n", "
\n", "\n", "**Explore also...**\n", " \n", "Other interesting libraries for creating interactive visualizations from spatial data:\n", " \n", "- [mapboxgl](https://github.com/mapbox/mapboxgl-jupyter)\n", "- [Bokeh](https://docs.bokeh.org/en/latest/)\n", "- [Geoviews](http://geoviews.org/)\n", "- [plotly express](https://plotly.com/python/maps/)\n", "\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Folium\n", "\n", "\n", "[Folium](https://github.com/python-visualization/folium) is a Python library that makes\n", "it possible visualize data on an interactive Leaflet map.\n", "\n", "**Resources:**\n", "\n", "- [Folium Documentation](https://python-visualization.github.io/folium/)\n", "- [Example Gallery](https://nbviewer.jupyter.org/github/python-visualization/folium/tree/master/examples/)\n", "- [Folium Quickstart](https://python-visualization.github.io/folium/quickstart.html)\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Creating a simple interactive web-map\n", "\n", "Import folium and other useful packages:" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "import folium" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "from pyproj import crs\n", "import geopandas as gpd\n", "import matplotlib.pyplot as plt" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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.\n", "\n", "First thing that we need to do is to create [a Map instance](https://python-visualization.github.io/folium/modules.html#folium.folium.Map) and define a location for zooming in the data: " ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "deletable": true, "editable": true }, "outputs": [], "source": [ "# Create a Map instance\n", "m = folium.Map(location=[60.25, 24.8], zoom_start=10, control_scale=True)" ] }, { "cell_type": "markdown", "metadata": { "deletable": true, "editable": true }, "source": [ "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." ] }, { "cell_type": "markdown", "metadata": { "deletable": true, "editable": true }, "source": [ "Let's see what our map looks like: " ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "collapsed": false, "deletable": true, "editable": true, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "data": { "text/html": [ "
Make this Notebook Trusted to load map: File -> Trust Notebook
" ], "text/plain": [ "" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "m" ] }, { "cell_type": "markdown", "metadata": { "deletable": true, "editable": true }, "source": [ "We can also save the map as a html file:" ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "collapsed": false, "deletable": true, "editable": true, "jupyter": { "outputs_hidden": false } }, "outputs": [], "source": [ "outfp = \"base_map.html\"\n", "m.save(outfp)" ] }, { "cell_type": "markdown", "metadata": { "deletable": true, "editable": true }, "source": [ "\n", "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.\n", "\n", "\n", "Let's create another map with different settings (location, bacground map, zoom levels etc). See documentation of the [Map() object](https://python-visualization.github.io/folium/modules.html#folium.folium.Map) for all avaiable options.\n", " \n", "``tiles`` -parameter is used for changing the background map provider and map style (see the [documentation](https://python-visualization.github.io/folium/modules.html#folium.folium.Map) for all in-built options).\n" ] }, { "cell_type": "code", "execution_count": 6, "metadata": { "collapsed": false, "deletable": true, "editable": true, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "data": { "text/html": [ "
Make this Notebook Trusted to load map: File -> Trust Notebook
" ], "text/plain": [ "" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Let's change the basemap style to 'Stamen Toner'\n", "m = folium.Map(\n", " location=[40.730610, -73.935242],\n", " tiles=\"Stamen Toner\",\n", " zoom_start=12,\n", " control_scale=True,\n", " prefer_canvas=True,\n", ")\n", "\n", "m" ] }, { "cell_type": "markdown", "metadata": { "deletable": true, "editable": true }, "source": [ "### Adding layers to the map\n", "\n", "Let's first have a look how we can add a simple [marker](https://python-visualization.github.io/folium/modules.html?highlight=marker#folium.map.Marker) on the webmap:" ] }, { "cell_type": "code", "execution_count": 7, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "data": { "text/html": [ "
Make this Notebook Trusted to load map: File -> Trust Notebook
" ], "text/plain": [ "" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Create a Map instance\n", "m = folium.Map(location=[60.20, 24.96], zoom_start=12, control_scale=True)\n", "\n", "# Add marker\n", "# Run: help(folium.Icon) for more info about icons\n", "folium.Marker(\n", " location=[60.20426, 24.96179],\n", " popup=\"Kumpula Campus\",\n", " icon=folium.Icon(color=\"green\", icon=\"ok-sign\"),\n", ").add_to(m)\n", "\n", "# Show map\n", "m" ] }, { "cell_type": "markdown", "metadata": { "deletable": true, "editable": true }, "source": [ "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. \n", "\n", "Let's see how we can plot data from a geodataframe using folium.\n", "\n" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "ename": "DriverError", "evalue": "data/addresses.shp: No such file or directory", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mCPLE_OpenFailedError\u001b[0m Traceback (most recent call last)", "\u001b[0;32mfiona/_shim.pyx\u001b[0m in \u001b[0;36mfiona._shim.gdal_open_vector\u001b[0;34m()\u001b[0m\n", "\u001b[0;32mfiona/_err.pyx\u001b[0m in \u001b[0;36mfiona._err.exc_wrap_pointer\u001b[0;34m()\u001b[0m\n", "\u001b[0;31mCPLE_OpenFailedError\u001b[0m: data/addresses.shp: No such file or directory", "\nDuring handling of the above exception, another exception occurred:\n", "\u001b[0;31mDriverError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m/tmp/ipykernel_221028/1565157085.py\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 3\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 4\u001b[0m \u001b[0;31m# Read the data\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 5\u001b[0;31m \u001b[0mpoints\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mgpd\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mread_file\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mpoints_fp\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 6\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 7\u001b[0m \u001b[0;31m# Check input data\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;32m~/.conda/envs/mamba/envs/python-gis-book/lib/python3.9/site-packages/geopandas/io/file.py\u001b[0m in \u001b[0;36m_read_file\u001b[0;34m(filename, bbox, mask, rows, **kwargs)\u001b[0m\n\u001b[1;32m 199\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 200\u001b[0m \u001b[0;32mwith\u001b[0m \u001b[0mfiona_env\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 201\u001b[0;31m \u001b[0;32mwith\u001b[0m \u001b[0mreader\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mpath_or_bytes\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0mfeatures\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 202\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 203\u001b[0m \u001b[0;31m# In a future Fiona release the crs attribute of features will\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;32m~/.conda/envs/mamba/envs/python-gis-book/lib/python3.9/site-packages/fiona/env.py\u001b[0m in \u001b[0;36mwrapper\u001b[0;34m(*args, **kwargs)\u001b[0m\n\u001b[1;32m 406\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mwrapper\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 407\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mlocal\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_env\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 408\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mf\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 409\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 410\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0misinstance\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mstr\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;32m~/.conda/envs/mamba/envs/python-gis-book/lib/python3.9/site-packages/fiona/__init__.py\u001b[0m in \u001b[0;36mopen\u001b[0;34m(fp, mode, driver, schema, crs, encoding, layer, vfs, enabled_drivers, crs_wkt, **kwargs)\u001b[0m\n\u001b[1;32m 254\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 255\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mmode\u001b[0m \u001b[0;32min\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0;34m'a'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m'r'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 256\u001b[0;31m c = Collection(path, mode, driver=driver, encoding=encoding,\n\u001b[0m\u001b[1;32m 257\u001b[0m layer=layer, enabled_drivers=enabled_drivers, **kwargs)\n\u001b[1;32m 258\u001b[0m \u001b[0;32melif\u001b[0m \u001b[0mmode\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;34m'w'\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;32m~/.conda/envs/mamba/envs/python-gis-book/lib/python3.9/site-packages/fiona/collection.py\u001b[0m in \u001b[0;36m__init__\u001b[0;34m(self, path, mode, driver, schema, crs, encoding, layer, vsi, archive, enabled_drivers, crs_wkt, ignore_fields, ignore_geometry, **kwargs)\u001b[0m\n\u001b[1;32m 160\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmode\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;34m'r'\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 161\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msession\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mSession\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 162\u001b[0;31m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msession\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mstart\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 163\u001b[0m \u001b[0;32melif\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmode\u001b[0m \u001b[0;32min\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0;34m'a'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m'w'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 164\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msession\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mWritingSession\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;32mfiona/ogrext.pyx\u001b[0m in \u001b[0;36mfiona.ogrext.Session.start\u001b[0;34m()\u001b[0m\n", "\u001b[0;32mfiona/_shim.pyx\u001b[0m in \u001b[0;36mfiona._shim.gdal_open_vector\u001b[0;34m()\u001b[0m\n", "\u001b[0;31mDriverError\u001b[0m: data/addresses.shp: No such file or directory" ] } ], "source": [ "# File path\n", "points_fp = \"data/addresses.shp\"\n", "\n", "# Read the data\n", "points = gpd.read_file(points_fp)\n", "\n", "# Check input data\n", "points.head()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "points.head()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "- conver the points to GeoJSON features using folium:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false, "deletable": true, "editable": true, "jupyter": { "outputs_hidden": false } }, "outputs": [], "source": [ "# Convert points to GeoJSON\n", "points_gjson = folium.features.GeoJson(points, name=\"Public transport stations\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Check the GeoJSON features\n", "# points_gjson.data.get('features')" ] }, { "cell_type": "markdown", "metadata": { "deletable": true, "editable": true }, "source": [ "Now we have our population data stored as GeoJSON format which basically contains the\n", "data as text in a similar way that it would be written in the ``.geojson`` -file." ] }, { "cell_type": "markdown", "metadata": { "deletable": true, "editable": true }, "source": [ "Add the points onto the Helsinki basemap:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false, "deletable": true, "editable": true, "jupyter": { "outputs_hidden": false } }, "outputs": [], "source": [ "# Create a Map instance\n", "m = folium.Map(\n", " location=[60.25, 24.8], tiles=\"cartodbpositron\", zoom_start=11, control_scale=True\n", ")\n", "\n", "# Add points to the map instance\n", "points_gjson.add_to(m)\n", "\n", "# Alternative syntax for adding points to the map instance\n", "# m.add_child(points_gjson)\n", "\n", "# Show map\n", "m" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Layer control\n", "\n", "We can also add a `LayerControl` object on our map, which allows the user to control which map layers are visible. See the [documentation](http://python-visualization.github.io/folium/docs-v0.5.0/modules.html#folium.map.LayerControl) for available parameters (you can e.g. change the position of the layer control icon)." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Create a layer control object and add it to our map instance\n", "folium.LayerControl().add_to(m)\n", "\n", "# Show map\n", "m" ] }, { "cell_type": "markdown", "metadata": { "deletable": true, "editable": true }, "source": [ "### Heatmap\n", "\n", "[Folium plugins](https://python-visualization.github.io/folium/plugins.html) allow us to use popular tools available in leaflet. One of these plugins is [HeatMap](https://python-visualization.github.io/folium/plugins.html#folium.plugins.HeatMap), which creates a heatmap layer from input points. \n", "\n", "Let's visualize a heatmap of the public transport stations in Helsinki using the addresses input data. [folium.plugins.HeatMap](https://python-visualization.github.io/folium/plugins.html#folium.plugins.HeatMap) requires a list of points, or a numpy array as input, so we need to first manipulate the data a bit:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "deletable": true, "editable": true }, "outputs": [], "source": [ "# Get x and y coordinates for each point\n", "points[\"x\"] = points[\"geometry\"].apply(lambda geom: geom.x)\n", "points[\"y\"] = points[\"geometry\"].apply(lambda geom: geom.y)\n", "\n", "# Create a list of coordinate pairs\n", "locations = list(zip(points[\"y\"], points[\"x\"]))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Check the data:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false, "deletable": true, "editable": true, "jupyter": { "outputs_hidden": false } }, "outputs": [], "source": [ "locations" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false, "deletable": true, "editable": true, "jupyter": { "outputs_hidden": false } }, "outputs": [], "source": [ "from folium.plugins import HeatMap\n", "\n", "# Create a Map instance\n", "m = folium.Map(\n", " location=[60.25, 24.8], tiles=\"stamentoner\", zoom_start=10, control_scale=True\n", ")\n", "\n", "# Add heatmap to map instance\n", "# 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)\n", "HeatMap(locations).add_to(m)\n", "\n", "# Alternative syntax:\n", "# m.add_child(HeatMap(points_array, radius=15))\n", "\n", "# Show map\n", "m" ] }, { "cell_type": "markdown", "metadata": { "editable": true }, "source": [ "### Clustered point map\n", "\n", "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](https://python-visualization.github.io/folium/plugins.html?highlight=marker%20cluster#folium.plugins.MarkerCluster) class." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "editable": true }, "outputs": [], "source": [ "from folium.plugins import MarkerCluster" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Create a Map instance\n", "m = folium.Map(\n", " location=[60.25, 24.8], tiles=\"cartodbpositron\", zoom_start=11, control_scale=True\n", ")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Following this example: https://github.com/python-visualization/folium/blob/master/examples/MarkerCluster.ipynb\n", "\n", "# Get x and y coordinates for each point\n", "points[\"x\"] = points[\"geometry\"].apply(lambda geom: geom.x)\n", "points[\"y\"] = points[\"geometry\"].apply(lambda geom: geom.y)\n", "\n", "# Create a list of coordinate pairs\n", "locations = list(zip(points[\"y\"], points[\"x\"]))" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Create a folium marker cluster\n", "marker_cluster = MarkerCluster(locations)\n", "\n", "# Add marker cluster to map\n", "marker_cluster.add_to(m)\n", "\n", "# Show map\n", "m" ] }, { "cell_type": "markdown", "metadata": { "deletable": true, "editable": true }, "source": [ "### Choropleth map" ] }, { "cell_type": "markdown", "metadata": { "deletable": true, "editable": true }, "source": [ "Next, let's check how we can overlay a population map on top of a basemap using [folium's choropleth method](http://python-visualization.github.io/folium/docs-v0.5.0/modules.html#folium.folium.Map.choropleth). This method is able to read the geometries and attributes directly from a geodataframe. \n", "This example is modified from the [Folium quicksart](https://python-visualization.github.io/folium/quickstart.html#Choropleth-maps).\n", "\n", "- First read in the population grid from HSY wfs like we did in [lesson 3](https://automating-gis-processes.github.io/site/notebooks/L3/spatial-join.html):" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false, "deletable": true, "editable": true, "jupyter": { "outputs_hidden": false } }, "outputs": [], "source": [ "import geopandas as gpd\n", "from pyproj import CRS\n", "import requests\n", "import geojson\n", "\n", "# Specify the url for web feature service\n", "url = \"https://kartta.hsy.fi/geoserver/wfs\"\n", "\n", "# Specify parameters (read data in json format).\n", "# Available feature types in this particular data source: http://geo.stat.fi/geoserver/vaestoruutu/wfs?service=wfs&version=2.0.0&request=describeFeatureType\n", "params = dict(\n", " service=\"WFS\",\n", " version=\"2.0.0\",\n", " request=\"GetFeature\",\n", " typeName=\"asuminen_ja_maankaytto:Vaestotietoruudukko_2018\",\n", " outputFormat=\"json\",\n", ")\n", "\n", "# Fetch data from WFS using requests\n", "r = requests.get(url, params=params)\n", "\n", "# Create GeoDataFrame from geojson\n", "data = gpd.GeoDataFrame.from_features(geojson.loads(r.content))\n", "\n", "# Check the data\n", "data.head()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from pyproj import CRS\n", "\n", "# Define crs\n", "data.crs = CRS.from_epsg(3879)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Re-project layer into WGS 84 (epsg: 4326)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Re-project to WGS84\n", "data = data.to_crs(epsg=4326)\n", "\n", "# Check layer crs definition\n", "print(data.crs)" ] }, { "cell_type": "markdown", "metadata": { "deletable": true, "editable": true }, "source": [ "Rename columns" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Change the name of a column\n", "data = data.rename(columns={\"asukkaita\": \"pop18\"})" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Create a Geo-id which is needed by the Folium (it needs to have a unique identifier for each row)\n", "data[\"geoid\"] = data.index.astype(str)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false, "deletable": true, "editable": true, "jupyter": { "outputs_hidden": false } }, "outputs": [], "source": [ "# Select only needed columns\n", "data = data[[\"geoid\", \"pop18\", \"geometry\"]]\n", "\n", "# Convert to geojson (not needed for the simple coropleth map!)\n", "# pop_json = data.to_json()\n", "\n", "# check data\n", "data.head()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Create an interactive choropleth map from the population grid:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false, "deletable": true, "editable": true, "jupyter": { "outputs_hidden": false } }, "outputs": [], "source": [ "# Create a Map instance\n", "m = folium.Map(\n", " location=[60.25, 24.8], tiles=\"cartodbpositron\", zoom_start=10, control_scale=True\n", ")\n", "\n", "# Plot a choropleth map\n", "# Notice: 'geoid' column that we created earlier needs to be assigned always as the first column\n", "folium.Choropleth(\n", " geo_data=data,\n", " name=\"Population in 2018\",\n", " data=data,\n", " columns=[\"geoid\", \"pop18\"],\n", " key_on=\"feature.id\",\n", " fill_color=\"YlOrRd\",\n", " fill_opacity=0.7,\n", " line_opacity=0.2,\n", " line_color=\"white\",\n", " line_weight=0,\n", " highlight=False,\n", " smooth_factor=1.0,\n", " # threshold_scale=[100, 250, 500, 1000, 2000],\n", " legend_name=\"Population in Helsinki\",\n", ").add_to(m)\n", "\n", "# Show map\n", "m" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Tooltips\n", "\n", "It is possible to add different kinds of pop-up messages and tooltips to the map. Here, it would be nice to see the population of each grid cell when you hover the mouse over the map. Unfortunately this functionality is not apparently implemented implemented in the Choropleth method we used before. \n", "\n", "Add tooltips, we can add tooltips to our map when plotting the polygons as GeoJson objects using the `GeoJsonTooltip` feature. (following examples from [here](http://nbviewer.jupyter.org/gist/jtbaker/57a37a14b90feeab7c67a687c398142c?flush_cache=true) and [here](https://nbviewer.jupyter.org/github/jtbaker/folium/blob/geojsonmarker/examples/GeoJsonMarkersandTooltips.ipynb))\n", "\n", "For a quick workaround, we plot the polygons on top of the coropleth map as a transparent layer, and add the tooltip to these objects. *Note: this is not an optimal solution as now the polygon geometry get's stored twice in the output!*" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Convert points to GeoJson\n", "folium.features.GeoJson(\n", " data,\n", " name=\"Labels\",\n", " style_function=lambda x: {\n", " \"color\": \"transparent\",\n", " \"fillColor\": \"transparent\",\n", " \"weight\": 0,\n", " },\n", " tooltip=folium.features.GeoJsonTooltip(\n", " fields=[\"pop18\"], aliases=[\"Population\"], labels=True, sticky=False\n", " ),\n", ").add_to(m)\n", "\n", "m" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Rember that you can also save the output as an html file: " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "outfp = \"choropleth_map.html\"\n", "m.save(outfp)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Extra: check out plotly express for an alternative way of plotting an interactive Choropleth map [in here](https://plotly.com/python/mapbox-county-choropleth/)." ] } ], "metadata": { "anaconda-cloud": {}, "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.9.7" } }, "nbformat": 4, "nbformat_minor": 4 }