Interactive Choropleth Maps With Plotly | by Benedikt Droste | Towards Data Science

Recently, I wanted to visualize data from the last federal election. I live in Duesseldorf, Germany and wanted to know in which districts which party had relative strengths.Surprisingly, it was a little harder than expected. Therefore I would like to share my experiences here. We will go through the following steps together:
0. Preparation1. Reading Data Over An Api In GeoJSON- And JSON-Format2. Extracting The Relevant Features From The GeoJSON-Data3. Customizing Our Map With Colorscales and Mouseovers4. Putting Everything Together And Adding Interactivity
You can follow this tutorial with your own dataset to create your individual map. The easiest way to do this would be to have the data in GeoJSON or JSON format. Here you can take a look at the data from this tutorial:
https://opendata.duesseldorf.de/api/action/datastore/search.json?resource_id=6893a12e-3d75-4b2f-bb8b-708982bea7b7
https://opendata.duesseldorf.de/sites/default/files/Stadtteile_WGS84_4326.geojson
You also need a mapbox account for this tutorial. Mapbox provides a flexible geodata API. Using the Mapbox API, we can map our individual data to a scalable world map. You can create an account at www.mapbox.com. You need a individual token to use the mapbox services which be be found under the account settings:
GeoJSON is open standard format for geographical features. Within this standard, storage of data follows a particular structure. Features can be points, lines, polygons or even combinations of the three types. For example, a polygon, a closed area inside a specific room, might look like this:
{"type": "Polygon","coordinates": [[[35, 10], [45, 45], [15, 40], [10, 20], [35, 10]],[[20, 30], [35, 35], [30, 20], [20, 30]]]}
In our example, we will also access GeoJSON data and combine it with JSON data. On the website https://opendate.duesseldorf.de there are both geodata and election data for the districts in Dusseldorf. However, the data is separated and we must merge it later.
First, we import all necessary libraries:
import pandas as pdimport numpy as npimport urllib.request, jsonimport requests
Now we can read the contents for the URLs:
url_wahl_2017 = 'https://opendata.duesseldorf.de/api/action/datastore/search.json?resource_id=6893a12e-3d75-4b2f-bb8b-708982bea7b7'url_stadtteile = 'https://opendata.duesseldorf.de/sites/default/files/Stadtteile_WGS84_4326.geojson'geo_data = requests.get(url_stadtteile).json()data_wahl = requests.get(url_wahl_2017).json()
Both data sources are now available as a nested dictionary. With data_wahl['result']['records'] the election results of the individual districts can be displayed:
We can now import the relevant election data from the dictionary into a pandas dataframe:
df_wahl = pd.DataFrame.from_dict(data_wahl['result']['records'])
Now let’s look at the geodata. For further processing with Plotly, we now extract the relevant features from the geodata request:
sources=[{"type": "FeatureCollection", 'features': [feat]} for feat in geo_data['features']]
The list sources contains the coordinates for all parts of the city. We will later hand over this list to the Mapbox object in order to present the respective districts.The IDs of the districts are also extracted from the geodata:
tx_ids=[geo_data['features'][k]['properties']['Stadtteil'] for k in range(len(geo_data['features']))]
We need the IDs later to allocate the correct colorscales for each party and each district.
In order to be able to dynamically color the individual parts of the city later on, we need the range of the respective percentages for each party. The lightest shade is assigned the lowest value and the highest value the darkest shade. For this we determine for each party and each district the lowest and highest value:
parties = ['Wahlsieger_proz','CDU_Proz','SPD_Proz','DIE LINKE_Proz','GRÜNE_Proz','AfD_Proz','FDP_Proz']for n in range(0,len(rate_list)):dct[rate_list[n]] = [df.loc[stadtteil, parties[n]] for stadtteil in tx_ids]dct_min[mins_list[n]] = min(dct[rate_list[n]])dct_max[maxs_list[n]] = max(dct[rate_list[n]])
Now we create for each party its own colorscale. Above all, I focused on the colors of the parties:
#Winnerpl_colorscale= [[0.0, ‘rgb(255, 255, 204)’],[0.35, ‘rgb(161, 218, 180)’],[0.5, ‘rgb(65, 182, 196)’],[0.6, ‘rgb(44, 127, 184)’],[0.7, ‘rgb(8, 104, 172)’],[1.0, ‘rgb(37, 52, 148)’]]#CDUcdu_colorscale= [[0.0, ‘rgb(224, 224, 224)’],[0.35, ‘rgb(192, 192, 192)’],[0.5, ‘rgb(160, 160, 160)’],[0.6, ‘rgb(128, 128, 128)’],[0.7, ‘rgb(96, 96, 96)’],[1.0, ‘rgb(64, 64, 64)’]]#SPDspd_colorscale= [[0.0, ‘rgb(255, 153, 153)’],[0.35, ‘rgb(255, 102, 102)’],[0.5, ‘rgb(255, 51, 51)’],[0.6, ‘rgb(255, 0, 0)’],[0.7, ‘rgb(204, 0, 0)’],[1.0, ‘rgb(153, 0, 0)’]]#Die Grünengruene_colorscale= [[0.0, ‘rgb(153, 255, 204)’],[0.35, ‘rgb(102, 255, 178)’],[0.5, ‘rgb(51, 255, 153)’],[0.6, ‘rgb(0, 255, 128)’],[0.7, ‘rgb(0, 204, 102)’],[1.0, ‘rgb(0, 153, 76)’]]#Die Linkelinke_colorscale= [[0.0, ‘rgb(255, 153, 204)’],[0.35, ‘rgb(255, 102, 178)’],[0.5, ‘rgb(255, 51, 153)’],[0.6, ‘rgb(255, 0, 128)’],[0.7, ‘rgb(204, 0, 102)’],[1.0, ‘rgb(153, 0, 76)’]]#AFDafd_colorscale= [[0.0, ‘rgb(153, 255, 255)’],[0.35, ‘rgb(102, 255, 255)’],[0.5, ‘rgb(51, 255, 255)’],[0.6, ‘rgb(0, 255, 255)’],[0.7, ‘rgb(0, 204, 204)’],[1.0, ‘rgb(0, 153, 153)’]]#FDPfdp_colorscale=[[0.0, ‘rgb(255, 255, 204)’],[0.35, ‘rgb(255, 255, 153)’],[0.5, ‘rgb(255, 255, 102)’],[0.6, ‘rgb(255, 255, 51)’],[0.7, ‘rgb(255, 255, 0)’],[1.0, ‘rgb(204, 204, 0)’]]
The following function I have taken from a tutorial, which is highly recommended:
Now we use this function for our previously created mini- and max-dictionaries to create the individual colorscales:
We want to identify the respective election winner on one card and the individual parties on the other. For this we adapt the mouseover texts:
text_win=[c+’<br>Election winner was the ‘+ w + ‘ with ‘ + ‘{:0.2f}’.format(r)+’%’ for c, r, w in zip(counties, dct[rate_list[0]], wahlsieger_c)]text_cdu=[c+’<br>The CDU had ‘ + ‘{:0.2f}’.format(r)+’%’ for c, r in zip(counties, dct[rate_list[1]])]text_spd=[c+’<br>The SPD had ‘ + ‘{:0.2f}’.format(r)+’%’ for c, r in zip(counties, dct[rate_list[2]])]text_linke=[c+’<br>The Linke had ‘ + ‘{:0.2f}’.format(r)+’%’ for c, r in zip(counties, dct[rate_list[3]])]text_gruene=[c+’<br>The Grünen had ‘ + ‘{:0.2f}’.format(r)+’%’ for c, r in zip(counties, dct[rate_list[4]])]text_afd=[c+’<br>The AfD had ‘ + ‘{:0.2f}’.format(r)+’%’ for c, r in zip(counties, dct[rate_list[5]])]text_fdp=[c+’<br>The FDP had ‘ + ‘{:0.2f}’.format(r)+’%’ for c, r in zip(counties, dct[rate_list[6]])]
For Plotly we must now note the following. The data includes the geodata to represent the shapes for the districts, and the mouseover texts. But we want flexible colorscales depending on the selection in the dropdown menu and relative strength of a party in each district. These colorscales are represented by the layer. The data can be assigned to a list. Depending on the selection of the dropdown, we set the visibility for individual elements from the list to True or False. For example, if we have a list of three records and want to render the first one, we give a list of the entries [True,False,False] as an argument for visible.
We first create the data list. We extract the longitude and latitude elements for each district and add them to a list:
The layers are also collected in a list:
Now we add the drop down menu. First, we will complete the labels for the entries and the respective Visible conditions. Finally, we submit the data and layer lists:
In the visible list, we define which element of the data list is displayed. In order for the colorscales to adapt accordingly, we must also define our layers list under "mapbox.layers" .
If we put everything together we have the following code:
If you want to run the code, you need to modify the mapbox_access_token in line 10.
Your result should look like this:
It’s very easy to process GeoJSON- and JSON-formats with Python. With Plotly you can create beautiful Choropleth maps from the geo information. It does get a bit trickier if you want to add interactivity using a dropdown, as you need individual colorscales for each layer. However, the args provides the needed flexibility. In a next step, it would still be nice to add an individual colorbar legend. In addition, it would be interesting to illustrate even more socio-demographic characteristics. So be creative!
Notebook:https://github.com/bd317/geodata/blob/master/API-Duesseldorf-Github%20(1).ipynb
General documentation for choropleth Plotly maps:
https://plot.ly/python/choropleth-maps/
A great notebook by empet as introduction to Plotly choropleth maps:
plot.ly
An update of the same user for the new Plotly trace-type choroplethmapbox:
plot.ly
If you enjoy Medium and Towards Data Science and didn’t sign up yet, feel free to use my referral link to join the community.