Visualizing Data on a Map with Python

python
Author

Martin Řanda

Published

April 24, 2026

A brief walkthrough of how to plot data on a map in Python

Animation of a map of federal states of Germany with changing colors

GeoJSON map file from SimpleMaps.com under CC BY 4.0

From life expectancy1 to measures of democracy2, publications that busy people tend to read are full of colorful maps displaying all sorts of statistics. These visualizations are called choropleth maps.3 Let’s take a look at how we can plot these in Python.

Typically, you will need a GeoJSON file of the region you’re trying to visualize your data on. If you look inside a GeoJSON file, you mostly see lists and lists of coordinates that define the boundaries of whatever map you’re trying to draw. To get these files, you may need to poke around the internet for a while, find out whether the map file you downloaded can actually be used for your purposes, and, if so, under which license, and so on. From what I’ve found, SimpleMaps.com provides a wide range of maps under the CC BY 4.0 license (e.g., German states), which is good enough for me.

Prerequisites

To give you a very specific idea of what you actually need in order to plot your data on a map, let me go through an illustrative example using the German federal states map mentioned earlier. My goal in this example is to plot the number of characters each state’s name consists of. For example, Bayern is made up of 6 letters, while Baden-Württemberg consists of 16 letters and a hyphen (17 characters).

In fact, here’s the full “dataset” we’ll be using:

name n_letters
Sachsen 7
Bayern 6
Rheinland-Pfalz 15
Saarland 8
Schleswig-Holstein 18
Niedersachsen 13
Nordrhein-Westfalen 19
Baden-Württemberg 17
Brandenburg 11
Mecklenburg-Vorpommern 22
Bremen 6
Hamburg 7
Hessen 6
Thüringen 9
Sachsen-Anhalt 14
Berlin 6

Now that we have a dataset containing our statistic and a column tying each row’s value to a geographical location, we’ll need the following:

  1. A GeoJSON file of federal states of Germany
  2. Python 3.10 or newer
  3. The geopandas & matplotlib packages for a static map, or geopandas & plotly for an interactive map

(Skip to the end of the post to get the full Python script)

Load packages & data

We’ve downloaded the de.json file, placed it into our working directory, and now we want to read it using geopandas.

import geopandas as gpd
import pandas as pd
import matplotlib.pyplot as plt
import plotly.express as px


geometry = gpd.read_file("de.json")

We also need to join our statistics and the map geometry into a single dataframe for convenient plotting. Naturally, the names of the regions we want to plot have to be the same in both tables.

statistics = pd.DataFrame({
    "name": [
        "Sachsen",
        "Bayern",
        "Rheinland-Pfalz",
        "Saarland",
        "Schleswig-Holstein",
        "Niedersachsen",
        "Nordrhein-Westfalen",
        "Baden-Württemberg",
        "Brandenburg",
        "Mecklenburg-Vorpommern",
        "Bremen",
        "Hamburg",
        "Hessen",
        "Thüringen",
        "Sachsen-Anhalt",
        "Berlin",
    ],
    "n_letters": [7, 6, 15, 8, 18, 13, 19, 17, 11, 22, 6, 7, 6, 9, 14, 6],
})

geometry = geometry.merge(statistics, on="name", how="inner")

For both the static and interactive maps, we’ll be using the same geometry dataframe.

Static map

Let’s start with the static image using matplotlib. I included some useful defaults, such as ax.axis("off"). Feel free to change these as needed.

fig, ax = plt.subplots(1, 1, figsize=(6, 7))
ax.axis("off")
ax.set_title("German States by Name Length (in German)")
geometry.plot(column="n_letters", ax=ax, linewidth=0.5, edgecolor="k", legend=True)
fig.savefig("map_static.png")

Here’s the resulting map

GeoJSON map file from SimpleMaps.com under CC BY 4.0

Interactive map

To produce the interactive map, we’ll be using plotly. The resulting choropleth map will be generated as an HTML element ready to be embedded in a website.

Note

In this tutorial, the necessary plotly JS libraries are set up to be served from the project’s CDN. Some reports4 suggest that displaying interactive maps offline is currently not possible.

In the interactive case, there are more attributes to be set. I’ve also included a modified on-hover tooltip, and saved the map as an embeddable HTML element using full_html=False (should still be displayable using a browser).

fig = px.choropleth(
    geometry,
    geojson=geometry,
    locations="name",
    featureidkey="properties.name",
    color="n_letters",
    projection="mercator",
    hover_data={"name": True, "n_letters": True},
    width=900,
    height=600,
    labels={"n_letters": "Number of letters"},
)
fig.update_geos(fitbounds="locations", visible=False)
# Change the default on-hover tooltip
fig.update_traces(
    hovertemplate="<b>%{customdata[0]}</b><br>"
    + "Number of letters: <b>%{customdata[1]}</b>",
    customdata=geometry[["name", "n_letters"]].values,
)
fig.write_html(
    "map_interactive.html",
    include_plotlyjs="cdn",
    full_html=False
)

This code produces the following visualization (GIF below):

GeoJSON map file from SimpleMaps.com under CC BY 4.0

Conclusion

Plotting choropleth maps in Python is not difficult. Plotting beautiful choropleth maps in Python takes a bit more effort, though. My goal in this post was to show you how to get started.

Below is the full code. Save it to script.py, don’t forget to download the map file, and use uv to generate both maps by running uv run script.py.

# /// script
# dependencies = [
#    "geopandas>=1.1.3",
#    "matplotlib>=3.10.8",
#    "plotly>=6.7.0",
# ]
# ///

import geopandas as gpd
import pandas as pd
import matplotlib.pyplot as plt
import plotly.express as px


# See https://simplemaps.com/gis/country/de
geometry = gpd.read_file("de.json")

statistics = pd.DataFrame({
    "name": [
        "Sachsen",
        "Bayern",
        "Rheinland-Pfalz",
        "Saarland",
        "Schleswig-Holstein",
        "Niedersachsen",
        "Nordrhein-Westfalen",
        "Baden-Württemberg",
        "Brandenburg",
        "Mecklenburg-Vorpommern",
        "Bremen",
        "Hamburg",
        "Hessen",
        "Thüringen",
        "Sachsen-Anhalt",
        "Berlin",
    ],
    "n_letters": [7, 6, 15, 8, 18, 13, 19, 17, 11, 22, 6, 7, 6, 9, 14, 6],
})

geometry = geometry.merge(statistics, on="name", how="inner")

#### STATIC ####

fig, ax = plt.subplots(1, 1, figsize=(6, 7))
ax.axis("off")
ax.set_title("German States by Name Length (in German)")
geometry.plot(column="n_letters", ax=ax, linewidth=0.5, edgecolor="k", legend=True)
fig.savefig("map_static.png")

#### INTERACTIVE ####

fig = px.choropleth(
    geometry,
    geojson=geometry,
    locations="name",
    featureidkey="properties.name",
    color="n_letters",
    projection="mercator",
    hover_data={"name": True, "n_letters": True},
    width=900,
    height=600,
    labels={"n_letters": "Number of letters"},
)
fig.update_geos(fitbounds="locations", visible=False)
fig.update_traces(
    hovertemplate="<b>%{customdata[0]}</b><br>"
    + "Number of letters: <b>%{customdata[1]}</b>",
    customdata=geometry[["name", "n_letters"]].values,
)
fig.write_html(
    "map_interactive.html",
    include_plotlyjs="cdn",
    full_html=False
)


Comment on GitHub

Footnotes

  1. https://ourworldindata.org/grapher/life-expectancy?tab=map↩︎

  2. https://www.economist.com/interactive/democracy-index-2025↩︎

  3. https://en.wikipedia.org/wiki/Choropleth_map↩︎

  4. https://community.plotly.com/t/plotting-maps-offline-bypass-the-cdn/79753↩︎

Reuse

Content of this blogpost is licensed under Creative Commons Attribution CC BY 4.0. Any content used from other sources is indicated and is not covered by this license. (View License)