How to step up your Folium Choropleth Map skills

Navid Mashinchi
Towards Data Science
10 min readFeb 24, 2021

--

Gif by author

The purpose of this tutorial is to take your Foilium skills to the next level. In this article, we will create a more advanced Choroplet map than usual. We will add customized functionalities to add a draggable legend, hover functionalities, and cross-hatching (crossing lines). If you would like to learn those skills, I highly recommend going over each step on your own with a sample dataset. You will learn tools that you can implement in your own projects and impress your boss or client. Some of the functionalities took me hours of research, and I thought it would be a good idea to share these skills for people to step up their folium skills. Sharing is caring, and I always like to share knowledge and pass it on to people that look to improve. So I hope you will enjoy this tutorial.

Data Cleaning:

The first step is to clean the data to plot our Choropleth map.

# We first import the libraries. 
import pandas as pd
import folium
from folium.plugins import StripePattern
import geopandas as gpd
import numpy as np
# Next we import the data.
df = pd.read_csv("sample_data")

# We grab the state and wills column
df = df[["state","wills"]]
df.head()
Image by author
# We check how many rows we have and the types of our data.
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 48 entries, 0 to 47
Data columns (total 2 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 state 48 non-null object
1 wills 32 non-null float64
dtypes: float64(1), object(1)
memory usage: 896.0+ bytes

As we can see above, we have 48 entries and 16 missing values in the wills column. The next step is to import the geoJson file. To create a choropleth, we need two things:

  • First, we need a geoJSON file that gives us the geographical coordinates for the layers. We can then use geopandas to read the coordinates into a data frame.
  • Second, we need the values to be displayed on the maps in different colors. In our example, we will use the “wills” column from our sample data.

For our example, we need the coordinates of the US states.

# We import the geoJSON file. 
url = ("https://raw.githubusercontent.com/python-visualization/folium/master/examples/data")
state_geo = f"{url}/us-states.json"

# We read the file and print it.
geoJSON_df = gpd.read_file(state_geo)
geoJSON_df.head()
Image by author

In the data frame above, you see the geometry column, which gives us the Choropleth map layers’ coordinates.

# Next we grab the states and put them in a list and check the length.
geoJSON_states = list(geoJSON_df.id.values)
len(geoJSON_states)
48

Our actual data has 48 states. Hence we are missing two states. Let’s figure out which states are missing because we need to merge the two datasets in the next steps to plot our Choropleth map. To find the missing states, we will use the NumPy setdiff1d function. The function finds the set difference of two arrays.

# Let's check which states are missing.
missing_states = np.setdiff1d(geoJSON_states,df_states_list)
missing_states
array(['AK', 'HI'], dtype='<U2')

The missing states are Alaska and Hawaii. The next step is to remove those two states from the geoJSON data frame, so we have the same amount of states in both data frames to merge both data frames. To merge the two data frames, we need to make sure the column names of both data frames with the state values have the same column name. When we merge the data frames, we need to merge them based on a specific key, as you will see below.

# we rename the column from id to state in the geoJSON_df so we can merge the two data frames.
geoJSON_df = geoJSON_df.rename(columns = {"id":"state"})
# Next we merge our sample data (df) and the geoJSON data frame on the key id.
final_df = geoJSON_df.merge(df, on = "state")
final_df.head()
Image by author

Voila, as you can see above, we have the merged data frame. Up to this point, we cleaned the data and are ready to work on the folium map and get to the fun part.

Choropleth Map:

To create a Folium map, we need to set the initial coordinates so we tell at which coordinates the map is centered at the beginning.

# Initialize folium map.
sample_map = folium.Map(location=[48, -102], zoom_start=4)
sample_map
Gif by author

You should see a map of the United States above. The next step is to create the Choropleth and add the layers to display the different colors based on the wills column from our sample data.

To set up the Choropleth map, we will use the foliumChoropleth() function. The most critical parameters that we need to set up correctly are the geo_data, data, columns, key_on, and fill_color. To get a better understanding of the parameters, we take a look at the documentation. According to the documentation, we learn the following:

  • geo_data (string/object) — URL, file path, or data (json, dict, geopandas, etc) to your GeoJSON geometries
  • data (Pandas DataFrame or Series, default None) — Data to bind to the GeoJSON.
  • columns (dict or tuple, default None) — If the data is a Pandas DataFrame, the columns of data to be bound. Must pass column 1 as the key, and column 2 the values.
  • key_on (string, default None) — Variable in the geo_data GeoJSON file to bind the data to. Must start with ‘feature’ and be in JavaScript objection notation. Ex: ‘feature.id’ or ‘feature.properties.statename’.

To learn more about the other parameters that you see above, please refer to the following link:

The next step is to set up the Choropleth map.

# Set up Choropleth map
folium.Choropleth(
geo_data=final_df,
data=final_df,
columns=['state',"wills"],
key_on="feature.properties.state",
fill_color='YlGnBu',
fill_opacity=1,
line_opacity=0.2,
legend_name="wills",
smooth_factor=0,
Highlight= True,
line_color = "#0000",
name = "Wills",
show=False,
overlay=True,
nan_fill_color = "White"
).add_to(sample_map)

sample_map
Gif by author

As you can see above, the white states represent null values in the data. In the code above, I set the nan_fill_color parameter to white.

Up to this point, this is pretty much how you create a basic Choropleth using folium. This might be satisfying, but we should probably inform the user what the white states really mean. The legend above doesn’t tell us the information since it’s just giving us the information about the wills column. To spice things up and take this plot to another level, we should probably add another legend to this plot so that the user can tell by just looking at the map that the white states represent null values. To add a secondary legend, I came across a good example online, which I highly recommend going over. See link below:

We are just not going to add a regular legend, but a draggable legend that makes your plot even more interactive. To add the draggable legend, you probably need to understand some basic HTML and JavaScript. Please see the code below for illustration.

# We import the required library:
from branca.element import Template, MacroElement

template = """
{% macro html(this, kwargs) %}

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>jQuery UI Draggable - Default functionality</title>
<link rel="stylesheet" href="//code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">

<script src="https://code.jquery.com/jquery-1.12.4.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>

<script>
$( function() {
$( "#maplegend" ).draggable({
start: function (event, ui) {
$(this).css({
right: "auto",
top: "auto",
bottom: "auto"
});
}
});
});

</script>
</head>
<body>


<div id='maplegend' class='maplegend'
style='position: absolute; z-index:9999; border:2px solid grey; background-color:rgba(255, 255, 255, 0.8);
border-radius:6px; padding: 10px; font-size:14px; right: 20px; bottom: 20px;'>

<div class='legend-title'>Legend (draggable!)</div>
<div class='legend-scale'>
<ul class='legend-labels'>
<li><span style='background:white;opacity:0.7;'></span>States that have Null values.</li>


</ul>
</div>
</div>

</body>
</html>

<style type='text/css'>
.maplegend .legend-title {
text-align: left;
margin-bottom: 5px;
font-weight: bold;
font-size: 90%;
}
.maplegend .legend-scale ul {
margin: 0;
margin-bottom: 5px;
padding: 0;
float: left;
list-style: none;
}
.maplegend .legend-scale ul li {
font-size: 80%;
list-style: none;
margin-left: 0;
line-height: 18px;
margin-bottom: 2px;
}
.maplegend ul.legend-labels li span {
display: block;
float: left;
height: 16px;
width: 30px;
margin-right: 5px;
margin-left: 0;
border: 1px solid #999;
}
.maplegend .legend-source {
font-size: 80%;
color: #777;
clear: both;
}
.maplegend a {
color: #777;
}
</style>
{% endmacro %}"""

macro = MacroElement()
macro._template = Template(template)

sample_map.get_root().add_child(macro)

sample_map
Gif by author

As you can see above, we have added the draggable legend. You can click on it and drag it to the position you wish. If you aren’t familiar with HTML, I suggest you copy and paste the code to your project and pay attention to the following line:

  • <li><span style=’background:white;opacity:0.7;’></span>States that have Null values.</li>

If you want to add multiple values in the legend, just copy-paste the line above and change the background color and the name.

Suppose your boss doesn’t like you to have two legends, and he asks you to come up with another solution. Another option would be to add cross-hatching (crossing lines) that represents the missing values. To add patterns to the layer, we need to use the folium plugin called StripePattern. Please see below the code:

# We create another map called sample_map2.
sample_map2 = folium.Map(location=[48, -102], zoom_start=4)

# Set up Choropleth map
folium.Choropleth(
geo_data=final_df,
data=final_df,
columns=['state',"wills"],
key_on="feature.properties.state",
fill_color='YlGnBu',
fill_opacity=1,
line_opacity=0.2,
legend_name="wills",
smooth_factor=0,
Highlight= True,
line_color = "#0000",
name = "Wills",
show=True,
overlay=True,
nan_fill_color = "White"
).add_to(sample_map2)



# Here we add cross-hatching (crossing lines) to display the Null values.
nans = final_df[final_df["wills"].isnull()]['state'].values
gdf_nans = final_df[final_df['state'].isin(nans)]
sp = StripePattern(angle=45, color='grey', space_color='white')
sp.add_to(sample_map2)
folium.features.GeoJson(name="Click for Wills NaN values",data=gdf_nans, style_function=lambda x :{'fillPattern': sp},show=True).add_to(sample_map2)

# We add a layer controller.
folium.LayerControl(collapsed=False).add_to(sample_map2)
sample_map2
Gif by author

To add the cross-hatching above, we look at the following lines in the code above:

  1. nans = final_df[final_df[“wills”].isnull()][‘state’].values
  2. gdf_nans = final_df[final_df[‘state’].isin(nans)]
  3. sp = StripePattern(angle=45, color=’grey’, space_color=’white’)
  4. sp.add_to(sample_map2)
  5. folium.features.GeoJson(name=”Click for earnings NaN values”,data=gdf_nans, style_function=lambda x :{‘fillPattern’: sp},show=True).add_to(sample_map2)

In number 1, we grab the states inside the wills column that have null values. In number 2, I am extracting the rows where the state is equal to the states that are inside the nans variable from number 1. In numbers 3 and 4, I set up the pattern that I would like to show for the NaN values. Lastly, in number 5, I add the NaN layer with the sp variable as the fillPattern. Note I am also adding the layer controller in the second last row.

The last topic that I will go over is the hover functionality. Suppose you want to hover over the states and display some data. In order to add this functionality, we will again use the folium. features.GeoJson() function as we did in the code above.

# Add hover functionality.
style_function = lambda x: {'fillColor': '#ffffff',
'color':'#000000',
'fillOpacity': 0.1,
'weight': 0.1}
highlight_function = lambda x: {'fillColor': '#000000',
'color':'#000000',
'fillOpacity': 0.50,
'weight': 0.1}
NIL = folium.features.GeoJson(
data = final_df,
style_function=style_function,
control=False,
highlight_function=highlight_function,
tooltip=folium.features.GeoJsonTooltip(
fields=['state','wills'],
aliases=['state','wills'],
style=("background-color: white; color: #333333; font-family: arial; font-size: 12px; padding: 10px;")
)
)
sample_map2.add_child(NIL)
sample_map2.keep_in_front(NIL)
sample_map2
Gif by author

First, we create a style_function and a highlight_function to finalize how we want our hover function to appear. Next, at the NIL variable, we need to pay attention to the right parameter values.

  • We set the data to the final_df data frame.
  • We set the style function to the style_function we created on the second line.
  • We set the highlight function to the highlight_function from line 6.
  • We use the GeoJsonTooltip function and set the fields and aliases parameters to the column names in our final_df data frame to display the desired data points when we hover over the states.

See the final code all together below. I have also added the light and dark mode option, to the layer controller at the bottom of the code. I am using the cartodbdark_matter layer for the light mode, and for the dark mode, I use the cartodbpositron layer.

# We create another map called sample_map2.
sample_map2 = folium.Map(location=[48, -102], zoom_start=4)

# Set up Choropleth map
folium.Choropleth(
geo_data=final_df,
data=final_df,
columns=['state',"wills"],
key_on="feature.properties.state",
fill_color='YlGnBu',
fill_opacity=1,
line_opacity=0.2,
legend_name="wills",
smooth_factor=0,
Highlight= True,
line_color = "#0000",
name = "Wills",
show=True,
overlay=True,
nan_fill_color = "White"
).add_to(sample_map2)

# Add hover functionality.
style_function = lambda x: {'fillColor': '#ffffff',
'color':'#000000',
'fillOpacity': 0.1,
'weight': 0.1}
highlight_function = lambda x: {'fillColor': '#000000',
'color':'#000000',
'fillOpacity': 0.50,
'weight': 0.1}
NIL = folium.features.GeoJson(
data = final_df,
style_function=style_function,
control=False,
highlight_function=highlight_function,
tooltip=folium.features.GeoJsonTooltip(
fields=['state','wills'],
aliases=['state','wills'],
style=("background-color: white; color: #333333; font-family: arial; font-size: 12px; padding: 10px;")
)
)
sample_map2.add_child(NIL)
sample_map2.keep_in_front(NIL)


# Here we add cross-hatching (crossing lines) to display the Null values.
nans = final_df[final_df["wills"].isnull()]['state'].values
gdf_nans = final_df[final_df['state'].isin(nans)]
sp = StripePattern(angle=45, color='grey', space_color='white')
sp.add_to(sample_map2)
folium.features.GeoJson(name="Click for Wills NaN values",data=gdf_nans, style_function=lambda x :{'fillPattern': sp},show=True).add_to(sample_map2)

# Add dark and light mode.
folium.TileLayer('cartodbdark_matter',name="dark mode",control=True).add_to(sample_map2)
folium.TileLayer('cartodbpositron',name="light mode",control=True).add_to(sample_map2)

# We add a layer controller.
folium.LayerControl(collapsed=False).add_to(sample_map2)
sample_map2
Gif by author

Conclusion:

Finally, let us go over the steps that we went over in this tutorial and summarize what we learned:

  • We used NumPy, Pandas, and GeoPandas to clean the data and created a final data frame by merging the geoJSON data and our sample data to create a Choropleth map using folium.
  • We learned how to add a draggable legend.
  • We also learned how to add cross-hatching (crossing lines) to display the null values.
  • Last but not least, we went over the hover functionality.

I hope you enjoyed this tutorial, and you will be able to implement these skills in your future projects to introduce your boss or client. If you have any questions on this topic or have some feedback, feel free to contact me. I’d be very grateful if you would share it on any social media platforms. Thank you and until next time️! ✌️

--

--

Data Scientist at Kohl’s | Adjunct Professor at University of Denver | Data Science Mentor at SharpestMinds