About

This plot is a choropleth map combined with a barplot for the legend.

It has been originally designed by Vinicius Oike Reginatto in R. Here is a reproduction in Python by Joseph Barbier.

As a teaser, here is the plot we’re gonna try building:

Combine a choropleth map and a barplot in Python: About

Libraries

For creating this chart, we will need a whole bunch of libraries!

import geopandas as gpd
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
from pypalettes import load_cmap
from highlight_text import fig_text, ax_text
from pyfonts import load_google_font

Dataset

We need two datasets:

path = "https://raw.githubusercontent.com/holtzy/The-Python-Graph-Gallery/master/static/data/saopaulo.geojson"
path = "../../static/data/saopaulo.geojson"
df = gpd.read_file(path)
df.head()
code_udhHDIpopgeometry
013550308010010.86620388.0POLYGON ((-46.53789 -23.56245, -46.53799 -23.5...
113550308010020.87021937.0POLYGON ((-46.54193 -23.54086, -46.54212 -23.5...
213550308010030.79010536.0POLYGON ((-46.51514 -23.55757, -46.51534 -23.5...
313550308010040.81627925.0POLYGON ((-46.5326 -23.5737, -46.53276 -23.574...
413550308010050.8206817.0POLYGON ((-46.50676 -23.57095, -46.50697 -23.5...

Now we create the dataset for the barplot:

atlas = df[["HDI", "pop"]].copy()

bins = [0.0, 0.65, 0.699, 0.749, 0.799, 0.849, 0.899, 0.949, 1.0]
labels = [
    "0.650 or less",
    "0.650 to 0.699",
    "0.700 to 0.749",
    "0.750 to 0.799",
    "0.800 to 0.849",
    "0.850 to 0.899",
    "0.900 to 0.949",
    "0.950 or more",
]

atlas["group_hdi"] = pd.cut(atlas["HDI"], bins=bins, include_lowest=True, labels=labels)

pop_hdi = atlas.groupby("group_hdi", observed=True)["pop"].sum().reset_index()
pop_hdi["share"] = (pop_hdi["pop"] / pop_hdi["pop"].sum()) * 100

pop_hdi["y_text"] = pop_hdi["share"] / 2
pop_hdi["label"] = pop_hdi["share"].round(1).astype(str) + "%"

pop_hdi
group_hdipopsharey_textlabel
00.650 or less415953.03.7106611.8553313.7%
10.650 to 0.6991620623.014.4573627.22868114.5%
20.700 to 0.7492106520.018.7919849.39599218.8%
30.750 to 0.7992460948.021.95378910.97689522.0%
40.800 to 0.8491576054.014.0597687.02988414.1%
50.850 to 0.8991417249.012.6430906.32154512.6%
60.900 to 0.9491409668.012.5754606.28773012.6%
70.950 or more202658.01.8078850.9039431.8%

Choropleth map

Creating the choropleth map is the easiest part:

  • load the YlGnBu colormap and specify that we want a continuous colormap
  • create a matplotlib figure with plt.subplots().
  • remove the axis
  • use the plot() method on the df geodataframe and specify the column to represent for the colors (here it's HDI)

Combine a choropleth map and a barplot in Python: Choropleth map

palette_name = "YlGnBu"
cmap = load_cmap(palette_name, cmap_type="continuous")

fig, ax = plt.subplots(figsize=(10, 10), dpi=300)
ax.axis("off")
df.plot(ax=ax, column="HDI", cmap=cmap, edgecolor="lightgrey", linewidth=0.2, alpha=0.9)
plt.show()

Barplot

Adding the bar chart may seem a little tricky, but it's not.

The main thing is to add a mini graph inside the choropleth. To do this, we create it with :

subax = ax.inset_axes(bounds=(0.58, 0.22, 0.15, 0.25), transform=fig.transFigure)

  • bounds designates the location of this minichart
  • transform specifies that the coordinates of bounds are relative to the figure. You can read more about this in this article

Once we've done that, we use the barplot() function from seaborn and use a for loop to add labels with specific positions/colours to the barplot.

Combine a choropleth map and a barplot in Python: Barplot

palette_name = "YlGnBu"
cmap = load_cmap(palette_name, cmap_type="continuous")

fig, ax = plt.subplots(figsize=(10, 10), dpi=300)
ax.axis("off")

# choropleth
df.plot(ax=ax, column="HDI", cmap=cmap, edgecolor="lightgrey", linewidth=0.2, alpha=0.9)

# barplot
mini_ax = ax.inset_axes(bounds=(0.58, 0.22, 0.15, 0.25), transform=fig.transFigure)
mini_ax.axis("off")
sns.barplot(
    data=pop_hdi,
    x="share",
    y="group_hdi",
    palette=palette_name,
    ax=mini_ax,
    hue="group_hdi",
    legend=False,
)
mini_ax.axvline(x=0, color="black")
mini_ax.invert_yaxis()

for index, row in pop_hdi.iterrows():
    x_pos = row["share"] - 6 if row["share"] > 4 else row["share"] + 1
    y_pos = index
    color = "white" if index in [5, 6] else "black"
    mini_ax.text(x_pos, y_pos, row["label"], color=color, size=7, va="center")
    mini_ax.text(-10, y_pos, row["group_hdi"], color="black", size=6, va="center")
plt.show()

Annotations

The only thing missing here is some good annotations with a nice font.

For this we'll use the Roboto font and load it with pyfonts. Then we'll basically use the fig.text() function to add the text we want.

Combine a choropleth map and a barplot in Python: Annotations

font = load_google_font("Roboto")
boldfont = load_google_font("Roboto", weight="bold")

fig, ax = plt.subplots(figsize=(10, 10), dpi=300)
ax.axis("off")

df.plot(ax=ax, column="HDI", cmap=cmap, edgecolor="lightgrey", linewidth=0.2, alpha=0.9)

subax = ax.inset_axes(bounds=(0.58, 0.22, 0.15, 0.25), transform=fig.transFigure)
subax.set_axis_off()
sns.barplot(
    data=pop_hdi,
    x="share",
    y="group_hdi",
    palette=palette_name,
    ax=subax,
    hue="group_hdi",
    legend=False,
)
subax.axvline(x=0, color="black")
subax.invert_yaxis()

for index, row in pop_hdi.iterrows():
    x_pos = row["share"] - 6 if row["share"] > 4 else row["share"] + 1
    y_pos = index
    color = "white" if index in [5, 6] else "black"
    subax.text(x_pos, y_pos, row["label"], color=color, size=7, va="center", font=font)
    subax.text(
        -10, y_pos, row["group_hdi"], color="black", size=6, va="center", font=font
    )

fig.text(
    x=0.5, y=0.87, s="HDI in Sao Paulo, BR (2010)", ha="center", size=25, font=font
)
fig.text(x=0.55, y=0.48, s="Population share by HDI group", size=9, font=font)
fig_text(
    x=0.6,
    y=0.14,
    s="<Source>: Atlas Brasil",
    size=10,
    font=font,
    highlight_textprops=[{"font": boldfont}],
)

plt.savefig(
    "../../static/graph/web-combine-choropleth-map-with-barplot.png",
    bbox_inches="tight",
    dpi=300,
)
plt.show()

Going further

You might be interested in: