[Plotly + Mapbox] Interactive Choropleth Visualization Tutorial
By: Yash Sanghvi
Tags: Choropleth, Mapbox, Base Map, Plotly, Geospatial

Getting started with the tutorial:

GitHub repo: https://github.com/carnot-technologies/MapVisualizations

Relevant notebook: PlotlyWithMapbox (Choropleth Plot)

View notebook on NBViewer: Click Here

Import relevant packages:

import numpy as np

import pandas as pd

import shapefile as shp

from datetime import datetime, timedelta

import geopandas as gpd

import json

import plotly.express as px

Convert Shapefile to GeoJSON:

The methods for converting shapefile to GeoJSON were discussed in the Plotly tutorial. Refer to that tutorial for details. Here, we will use the quicker method that uses GeoPandas.

# set the filepath and load

fp = "shape_files\\India_Districts_2020_compressed\\India_Districts_2020.shp" #reading the file stored in variable fp

map_df = gpd.read_file(fp) map_df_up = map_df[map_df['stname'] == 'UTTAR PRADESH'] map_df_up.to_file("json_files\\up_districts_2020_compressed.json", driver='GeoJSON')

Load and compress GeoJSON

Now that we have prepared the GeoJSON, let’s load and examine it

with open('json_files\\up_districts_2020_compressed.json') as f:

up_districts = json.load(f)

Let us check the keys of this JSON:

#Check the keys of the json

up_districts["features"][0].keys() >> dict_keys(['type', 'properties', 'geometry'])

The properties key should contain the district name. Let’s verify that:

up_districts["features"][0]['properties'].keys() >> dict_keys(['dtname', 'stname', 'stcode11', 'dtcode11', 'year_stat', 'Dist_LGD', 'State_LGD', 'JID', 'FID'])

dtname contains the district names. You can see that almost all columns of the GeoPandas dataframe are present under ‘properties’.

Now let us shift focus to the geometry and check the precision of the coordinates.

up_districts["features"][0]['geometry']['coordinates'][0][0] >> [78.0731, 29.0824]

As you can see, the coordinates are accurate up to 4 decimal places, which translates to about 11 m. We don’t need that much accuracy. We will round off the coordinates to two decimal places (1.1 km accuracy). That’ll help reduce the size of the visualization. As discussed in the Plotly tutorial, the size of the visualization is proportional to the size of the GeoJSON. For your reference, if we don’t perform the below compression, the resulting HTML is about 14% larger in size.

#Round off the locations to 2 decimal places (about 1.1 km accuracy) for i in range(0, len(up_districts["features"])):

for j in range(0,len(up_districts["features"][i]['geometry']['coordinates'])):

try:

up_districts["features"][i]['geometry']['coordinates'][j] = np.round(np.array(up_districts["features"][i]['geometry']['coordinates'][j]),2)

except:

print(i,j)

If you now check the coordinates again, you’ll see that they have been rounded off to 2 decimal places:

up_districts["features"][0]['geometry']['coordinates'][0][0] >> array([78.07, 29.08])

Load and Process Data:

Here, we will use the same data CSV that we used for the GeoPandas tutorial.

df = pd.read_csv('data\\UP_dummy_data.csv')

Now, let’s remove the time from the ‘Installed On’ column since we are concerned only with the date.

df['Installed On']=df['Installed On'].apply(lambda x: x.split('T')[0]) df['Installed On']=pd.to_datetime(df['Installed On'],format="%Y-%m-%d")

Now, we will go back to our shapefile and extract only the district column. This is to ensure that all districts are present in the final dataframe. At the beginning of this tutorial, it was told that we will take advantage of the missing data for certain districts to eliminate those districts from the visualization, and thus narrate the story much better. However, this means keeping the NaN value unchanged. We will still need at least one row corresponding to each district. This is because we are creating an animation, which is essentially a collection of different visualizations. If one visualization has X number of rows in the data, and the other has Y number of rows, it confuses plotly when stitching the visualizations together and you get some incomprehensible output. So we will not give plotly and ourselves a hard time. We will make sure an entry (which may be NaN) is present for each district.

#Get only district field from shape file

df_d = map_df_up[['dtname']]

Now that a dataframe containing all the district names is ready, we will add data for one animation frame (which corresponds to the installation date in our context) to this dataframe. Then repeat this for another animation frame and so on. Our final dataframe will be a concatenated sum of all these dataframes. We will take a split our time-frame into intervals of 30 days. We can take shorter intervals, but that will increase the file size accordingly.

#Generate the cumulative dataframe

date_min = df['Installed On'].min()

#Generate the cumulative dataframe

date_min = df['Installed On'].min()

df_final = pd.DataFrame(columns=['dtname','count','date']) #Generate cumulative data for for each month (approximated as 30 days), starting from date_min, and upto 6 months

for i in range(0,180,30):

date = date_min+timedelta(days=i)

#Get the cumulative df

df_c = df[df['Installed On'] <= date]



#Create a temporary df to store the value_counts

df_t = df_c['installation_district'].value_counts().to_frame()

df_t.reset_index(inplace=True)

df_t.columns = ['dist','count']

merged = df_d.set_index('dtname').join(df_t.set_index('dist'))

# merged['count'].fillna(0,inplace=True)

merged.reset_index(inplace=True)

merged['date'] = date

df_final=pd.concat([df_final,merged], ignore_index = True) df_final['dt_str'] = df_final['date'].apply(lambda x: x.strftime("%d-%b-%Y"))

You will notice that we have commented our the fillna line because we want to keep the NaN values as they are and render only those districts for which some data is available. We are also creating an additional column called dt_str which contains the installation date in the string format. This is because the animation frame needs to be a string column.

Create the Visualization:

With the final dataframe ready, creating the visualization is very easy. You just need to call px.choropleth_mapbox instead of px.choropleth.

max_count = df_final['count'].max()

fig = px.choropleth_mapbox(df_final, geojson=up_districts,

locations='dtname',

color='count',

color_continuous_scale="Viridis",

range_color=(0, max_count),

featureidkey="properties.dtname",

mapbox_style="carto-positron",

opacity=0.5,

center = {"lat": 26.8467, "lon": 80.9462},

zoom=5,

animation_frame='dt_str') fig.update_geos(fitbounds="locations",visible=False)

fig.update_layout(margin={"r":0,"t":0,"l":0,"b":0},) fig.show()

While most of the arguments are similar to those discussed in the Plotly tutorial, some new ones are described below:

mapbox_style determines which style of Mapbox will be used

determines which style of Mapbox will be used opacity determines how opaque the shapes will be. If you want to base map labels to be more visible, you will keep the opacity to a lower value.

determines how opaque the shapes will be. If you want to base map labels to be more visible, you will keep the opacity to a lower value. center determines the location of the coordinates around which we want to center the visualization. Here, the visualization concerns only the state of Uttar Pradesh. Therefore, an appropriate location has been hard-coded as the center

determines the location of the coordinates around which we want to center the visualization. Here, the visualization concerns only the state of Uttar Pradesh. Therefore, an appropriate location has been hard-coded as the center zoom determines, as the name suggests, how zoomed in the map will be. Higher the value, more zoomed in the map will be. You can experiment with this field to determine the best fit for your use case.

determines, as the name suggests, how zoomed in the map will be. Higher the value, more zoomed in the map will be. You can experiment with this field to determine the best fit for your use case. animation_frame determines which column of the dataframe will be used for grouping the data into frames. Here, we are specifying that all rows having the same value of dt_str column will be a part of a single frame and as the dt_str value changes, the frame will change.

Congratulations! You’ve created your first live animation, fully loaded with buttons and slider. Spend your time and play around with your visualization.

If you want to share it with your friends without sharing the source code, export it as a stand-alone HTML file:

fig.write_html('html_files\\plotly_mapbox_choro.html')

We will discuss how to upload visualizations to chart_studio and get the direct embed link in the [Plotly + Mapbox] Scatter Plot tutorial.

When to use this library:

The same guide which applies to Plotly also applies to Plotly+Mapbox.

References: