Generate the data and import packages¶
First we need to create the data. I will do it using a dictionary and then converting it to a pandas dataframe as a lot projects use pandas to work with data.
import matplotlib.pyplot as plt
import pandas as pd
from matplotlib.offsetbox import OffsetImage, AnnotationBbox
color_dict_bar = {
(2022, "Norway"): "#9194A3",
(2004, "Norway"): "#2B314D",
(2022, "Denmark"): "#E2AFA5",
(2004, "Denmark"): "#CE5A43",
(2022, "Sweden"): "#C4D6F8",
(2004, "Sweden"): "#5375D4",
}
color_dict_edges = {
(2022, "Norway"): "#2C324F",
(2004, "Norway"): "#1B203A",
(2022, "Denmark"): "#CE5A43",
(2004, "Denmark"): "#B6493F",
(2022, "Sweden"): "#5375D4",
(2004, "Sweden"): "#4562C5",
}
xy_ticklabel_color, grand_totals_color, grid_color, datalabels_color = "#757C85", "#101628", "#C8C9C9", "#FFFFFF"
data = {
"year": [2004, 2022, 2004, 2022, 2004, 2022],
"countries": ["Sweden", "Sweden", "Denmark", "Denmark", "Norway", "Norway"],
"sites": [13, 15, 4, 10, 5, 8],
}
df = pd.DataFrame(data)
# Add the color based on the color dictionary
df["color_bars"] = df.set_index(["year", "countries"]).index.map(color_dict_bar.get)
df["color_edges"] = df.set_index(["year", "countries"]).index.map(color_dict_edges.get)
df["sub_total"] = df.groupby("countries")["sites"].transform("sum")
df = df.sort_values(["year", "sub_total"], ascending=False).reset_index(drop=True)
df
| year | countries | sites | color_bars | color_edges | sub_total | |
|---|---|---|---|---|---|---|
| 0 | 2022 | Sweden | 15 | #C4D6F8 | #5375D4 | 28 |
| 1 | 2022 | Denmark | 10 | #E2AFA5 | #CE5A43 | 14 |
| 2 | 2022 | Norway | 8 | #9194A3 | #2C324F | 13 |
| 3 | 2004 | Sweden | 13 | #5375D4 | #4562C5 | 28 |
| 4 | 2004 | Denmark | 4 | #CE5A43 | #B6493F | 14 |
| 5 | 2004 | Norway | 5 | #2B314D | #1B203A | 13 |
Get the flags:
img = [
plt.imread("../flags/sw-rd.png"),
plt.imread("../flags/de-rd.png"),
plt.imread("../flags/no-rd.png"),
]
countries = df.countries.unique()
Plot the chart¶
We start by plotting the bars
fig, ax = plt.subplots(figsize=(10, 4), facecolor="w")
ax.set(xlim=(-1, df.sites.max()+1))
for i, (year, group) in enumerate(df.groupby("year", sort = False)):
ax.barh(
group.countries,
group.sites,
height=0.3,
zorder=1,
)
if group.countries.iloc[0] == "Sweden":
ax.annotate(
year,
xy=(group.sites.iloc[0], -0.13),
xytext=(group.sites.iloc[0], -0.55),
color="black",
ha="center",
va="center",
bbox=dict(facecolor="none", edgecolor="#DEE2E4", boxstyle="round,pad=0.5"),
arrowprops=dict(arrowstyle="-", color="#DEE2E4"),
annotation_clip=False,
)
Plot the color of the bars and the data labels:
for bar, color in zip(ax.patches, df.color_bars):
y_center = bar.get_y() + bar.get_height() / 2
x = bar.get_x() + bar.get_width()
bar.set_facecolor(color)
ax.text(
x,
y_center,
round(bar.get_width()),
ha="center",
va="center",
color="w",
size=10,
)
fig
To plot all the circles at the start and end of the bars, we need to modify the original data:
x = df.sites.tolist() + [0] * len(countries)
y = list(range(len(countries))) * len(countries)
color_bar_end = df.color_bars.tolist() + df[df.year == df.year.min()]['color_bars'].tolist()
print(x,y, color_bar_end)
[15, 10, 8, 13, 4, 5, 0, 0, 0] [0, 1, 2, 0, 1, 2, 0, 1, 2] ['#C4D6F8', '#E2AFA5', '#9194A3', '#5375D4', '#CE5A43', '#2B314D', '#5375D4', '#CE5A43', '#2B314D']
ax.scatter(
x,
y,
marker="o",
s=620,
color=color_bar_end,
zorder=2
)
fig
Plot the data label circles:
First we need to create a new color sequence for the edge color of the small dots at the end of the bars.
color_edges_small = (["w"] + df.color_edges[1:3].to_list() + ["w"] + df.color_edges[4:6].to_list())
ax.scatter(
x[:6],
y[:6],
marker="o",
s=450,
color=df.color_edges,
ec=color_edges_small,
zorder=2,
)
fig
Ãdd the flags, y labels and final styling:
# set flags
ax.yaxis.set_ticks(
range(3),
countries,
weight="bold",
ha="left"
)
ax.tick_params(
axis="y",
which="major",
labelsize=14,
colors="#101628",
length=0,
pad=65
)
tick_labels = ax.yaxis.get_ticklabels()
for i, im in enumerate(img):
ib = OffsetImage(
im,
zoom=0.04
)
ab = AnnotationBbox(
ib, tick_labels[i].get_position(),
frameon=False,
box_alignment=(+7, +0.5)
)
ax.add_artist(ab)
ax.set_xticks([])
ax.set_frame_on(False)
fig