Generate the data and import packages¶
First, we need to create the data. I'll start by defining it as a dictionary and then convert it into a pandas DataFrame, since pandas is commonly used in many projects for data manipulation.
import matplotlib.pyplot as plt
import matplotlib.patches as patches
import pandas as pd
import numpy as np
color_dict = {
"Norway": "#2B314D",
"Denmark": "#A54836",
"Sweden": "#5375D4",
}
xy_label_color, datalabels_color, grid_color = "#101628", "#FFFFFF", "#C8C9C9",
data = {
"year": [2004, 2022, 2004, 2022, 2004, 2022],
"countries": ["Denmark", "Denmark", "Norway", "Norway", "Sweden", "Sweden",],
"sites": [4, 10, 5, 8, 13, 15],
}
df = pd.DataFrame(data)
df = df.sort_values(["year"], ascending=False).reset_index(drop=True)
df["ctry_code"] = df.countries.astype(str).str[:2].astype(str).str.upper()
df["year_lbl"] = "'" + df["year"].astype(str).str[-2:].astype(str)
# map the colors of a dict to a dataframe
df["color"] = df.countries.map(color_dict)
# To ensure that the areas are really proportional, use the square root values as the length and height of the rectangles.
df["sq_sites"] = (df["sites"]) ** 0.5
df
| year | countries | sites | ctry_code | year_lbl | color | sq_sites | |
|---|---|---|---|---|---|---|---|
| 0 | 2022 | Denmark | 10 | DE | '22 | #A54836 | 3.162278 |
| 1 | 2022 | Norway | 8 | NO | '22 | #2B314D | 2.828427 |
| 2 | 2022 | Sweden | 15 | SW | '22 | #5375D4 | 3.872983 |
| 3 | 2004 | Denmark | 4 | DE | '04 | #A54836 | 2.000000 |
| 4 | 2004 | Norway | 5 | NO | '04 | #2B314D | 2.236068 |
| 5 | 2004 | Sweden | 13 | SW | '04 | #5375D4 | 3.605551 |
Lets create the x coordinates:
countries = df.countries.unique()
x = np.zeros(len(df), dtype=float)
offset = 0.5
x[0:len(countries)] += offset
df['x_position'] = x
df
| year | countries | sites | ctry_code | year_lbl | color | sq_sites | x_position | |
|---|---|---|---|---|---|---|---|---|
| 0 | 2022 | Denmark | 10 | DE | '22 | #A54836 | 3.162278 | 0.5 |
| 1 | 2022 | Norway | 8 | NO | '22 | #2B314D | 2.828427 | 0.5 |
| 2 | 2022 | Sweden | 15 | SW | '22 | #5375D4 | 3.872983 | 0.5 |
| 3 | 2004 | Denmark | 4 | DE | '04 | #A54836 | 2.000000 | 0.0 |
| 4 | 2004 | Norway | 5 | NO | '04 | #2B314D | 2.236068 | 0.0 |
| 5 | 2004 | Sweden | 13 | SW | '04 | #5375D4 | 3.605551 | 0.0 |
We will group the data by country to plot on each subplot:
groups = df.groupby("countries")
Plot the chart¶
To create this plot, we will use the rectangular patch method and we need the following parameters:
| Parameter | Description | Value |
|---|---|---|
| xy | The xy positions or the coordinates of the lower-left corner of the rectangle | x_position |
| width | The width of each rectangle | sq_site |
| height | The height of each rectangle | sq_site |
| rotation | The rotation angle of each rectangle |
+------------------+
|
height
|
(xy)---- width -----+
We could probably use also the ax.bar3d with a dy of zero, but ax.patches seems more straight forward.
We also need to add ax.set_aspect("equal") to scale x and y equally.
fig, axes = plt.subplots(ncols=(df.countries.nunique()), nrows=1, figsize=(13, 6), sharex=True, sharey=True, facecolor="white")
fig.tight_layout(pad=2.0)
for i, ((country, group), ax) in enumerate(zip(groups, axes.ravel())):
for row in group.itertuples(index=False):
sq_site = row.sq_sites
x = y = row.x_position
square = patches.Rectangle(
(x, y),
sq_site,
sq_site,
ec="w",
facecolor=row.color
)
ax.add_patch(square)
ax.set_aspect("equal") # This will scale x and y equally
ax.text(
sq_site + x - 0.3,
sq_site + x - 0.2,
row.sites,
fontsize=15,
color=datalabels_color,
ha="center",
va="top",
)
ax.set(
xlim = (-0.5, df.sq_sites.max() + 1),
ylim = (-0.5, df.sq_sites.max() + 1)
)
Add the custom legend¶
The custom legend will be another matplotlib axes, positioned at the bottom of the figure using fig.add_axes and then we add the two new rectangles with the legend labels.
But first, lets create the data manually:
# Rectangle data (each is a dict of position, size, color, etc.)
rectangle_data = [
{"xy": (0.1, 0.25), "width": 0.4, "height": 0.4, "color": "w", "ec": grid_color},
{"xy": (0, 0.1), "width": 0.25, "height": 0.25, "color": "w", "ec": grid_color},
]
# Text data (position, text, alignment, color)
text_data = [
{"x": -0.35, "y": 0.5, "s": df.year.max(), "va": "center", "color": grid_color},
{"x": -0.35, "y": 0.2, "s": df.year.min(), "va": "center", "color": grid_color},
]
and now add it to the figure:
legend_ax = fig.add_axes([0.9,- 0.2, 0.1, 0.2]) # left, bottom, width, height in figure coordinates
legend_ax.axis("off")
for rect in rectangle_data:
legend_ax.add_patch(patches.Rectangle(**rect))
for txt in text_data:
legend_ax.text(**txt)
fig
Put everything together¶
Let's add the styling and the legend and we are done:
import matplotlib.pyplot as plt
import matplotlib.patches as patches
import pandas as pd
color_dict = {
"Norway": "#2B314D",
"Denmark": "#A54836",
"Sweden": "#5375D4",
}
xy_label_color, datalabels_color = "#101628", "#FFFFFF"
data = {
"year": [2004, 2022, 2004, 2022, 2004, 2022],
"countries": ["Denmark", "Denmark", "Norway", "Norway", "Sweden", "Sweden",],
"sites": [4, 10, 5, 8, 13, 15],
}
df = pd.DataFrame(data)
df = df.sort_values(["year"], ascending=False).reset_index(drop=True)
df["ctry_code"] = df.countries.astype(str).str[:2].astype(str).str.upper()
df["year_lbl"] = "'" + df["year"].astype(str).str[-2:].astype(str)
# map the colors of a dict to a dataframe
df["color"] = df.countries.map(color_dict)
# To ensure that the areas are really proportional, use the square root values as the length and height of the rectangles.
df["sq_sites"] = (df["sites"]) ** 0.5
df["x_position"] = [0.5]*3 + [0]*3
# We will group the data by country to plot on each subplot:
groups = df.groupby("countries")
# Plot the chart
fig, axes = plt.subplots(ncols=(df.countries.nunique()), nrows=1, figsize=(13, 6), sharex=True, sharey=True, facecolor="white")
fig.tight_layout(pad=2.0)
groups = df.groupby("countries")
for i, ((country, group), ax) in enumerate(zip(groups, axes.ravel())):
for row in group.itertuples(index=False):
sq_site = row.sq_sites
x = y = row.x_position
square = patches.Rectangle(
(x, y),
sq_site,
sq_site,
ec="w",
facecolor=row.color
)
ax.add_patch(square)
ax.set_aspect("equal") # This will scale x and y equally
ax.text(
sq_site + x - 0.3,
sq_site + x - 0.2,
row.sites,
fontsize=15,
color=datalabels_color,
ha="center",
va="top",
)
ax.set(
xlim = (-0.5, df.sq_sites.max() + 1),
ylim = (-0.5, df.sq_sites.max() + 1)
)
ax.set_frame_on(False)
ax.set_xlabel(country, size=14, color=xy_label_color)
ax.tick_params(axis="both", which="both", length=0, labelbottom=False, labelleft=False)
#data for the legend
# Rectangle data (each is a dict of position, size, color, etc.)
rectangle_data = [
{"xy": (0.1, 0.25), "width": 0.4, "height": 0.4, "color": "w", "ec": grid_color},
{"xy": (0, 0.1), "width": 0.25, "height": 0.25, "color": "w", "ec": grid_color},
]
# Text data (position, text, alignment, color)
text_data = [
{"x": -0.35, "y": 0.5, "s": df.year.max(), "va": "center", "color": grid_color},
{"x": -0.35, "y": 0.2, "s": df.year.min(), "va": "center", "color": grid_color},
]
#add the legend
legend_ax = fig.add_axes([0.9,- 0.2, 0.1, 0.2]) # left, bottom, width, height in figure coordinates
legend_ax.axis("off")
for rect in rectangle_data:
legend_ax.add_patch(patches.Rectangle(**rect))
for txt in text_data:
legend_ax.text(**txt)