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
from matplotlib.offsetbox import OffsetImage, AnnotationBbox
import numpy as np
import pandas as pd
color_dict = {"Norway": "#2B314D", "Denmark": "#A54836", "Sweden": "#5375D4" }
xy_ticklabel_color, grand_totals_color, spines_color, datalabels_color ='#757C85',"#101628", "#828696", "#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)
df['sub_total'] = df.groupby('year')['sites'].transform('sum')
df['diff'] = df.groupby(['countries'])['sites'].diff()
#custom sort
df = df.sort_values(by=['year', 'countries'])
#map the colors of a dict to a dataframe
df['color']= df.countries.map(color_dict)
#we conly need the data from the last year
df
| year | countries | sites | sub_total | diff | color | |
|---|---|---|---|---|---|---|
| 2 | 2004 | Denmark | 4 | 22 | NaN | #A54836 |
| 4 | 2004 | Norway | 5 | 22 | NaN | #2B314D |
| 0 | 2004 | Sweden | 13 | 22 | NaN | #5375D4 |
| 3 | 2022 | Denmark | 10 | 33 | 6.0 | #A54836 |
| 5 | 2022 | Norway | 8 | 33 | 3.0 | #2B314D |
| 1 | 2022 | Sweden | 15 | 33 | 2.0 | #5375D4 |
Mapt the color and sites for plotting later for the last year:
df_last_year = df.dropna()
#map colors and sites
style_map = df_last_year.set_index('sites')['color'].to_dict()
style_map
{10: '#A54836', 8: '#2B314D', 15: '#5375D4'}
start = df[df.year==df.year.min()]['sites'].values
end = df[df.year==df.year.max()]['diff'].values
print(start, end)
[ 4 5 13] [6. 3. 2.]
Get the flags:
img = [
plt.imread("../flags/de-sq-transparent.png"),
plt.imread("../flags/no-sq-transparent.png"),
plt.imread("../flags/sw-sq-transparent.png"),
plt.imread("../flags/de-sq.png"),
plt.imread("../flags/no-sq.png"),
plt.imread("../flags/sw-sq.png")]
flag_map = dict(zip(df.sites, img))
Create the matrix for the stepping numbers:
#create the axis
mosaic = np.zeros((15, 15), dtype=int)
for j in range(15):
mosaic[j, j] = j + 1
Plot the chart¶
We are going to use subplot_mosaic to create a diagonal with axes and then we will change the figure coordinates bottom-to-top (y0 increasing), and right-to-left (x0 decreasing).
fig, axes = plt.subplot_mosaic(
np.fliplr(mosaic), #mirrow the mosaic
empty_sentinel=0, # no subplots if value = 0
gridspec_kw={
"wspace": 0,
"hspace": 0,
}
)
Add some styling to create the stair effect and the numbers:
for i , (key, ax) in enumerate(reversed(axes.items()), start = 1):
#print(i)
if i in style_map:
fontweight = 'bold'
color = style_map[i]
else:
fontweight = 'light'
color = 'black' # or 'gray', etc.
#add numbers
ax.text(
0.5, #pos inside the axes
0.5,
i ,
ha="center",
va="center",
fontsize=10,
color=color,
weight=fontweight
)
#styling
ax.tick_params(length = 0, labelleft = False, labelbottom = False)
ax.spines[['bottom','right']].set_visible(False)
ax.spines[['top','left']].set_color(spines_color)
# Add image if position matches
if i in flag_map:
ib = OffsetImage(flag_map[i], zoom=.03)
ab = AnnotationBbox(
ib,
(0,0),
xybox=(0.5, 1.6),
alpha=0.5,
frameon=False,
)
ax.add_artist(ab)
#add the arrows
x_arr = .5
y_arr = 2.5
rad = 0.6
if i in set(start):
index = list(start).index(i) #Find the index of i in start
ax.annotate(
"",
xy=(x_arr, y_arr),
xytext=(end[index] + x_arr , end[index] + y_arr),
annotation_clip= False,
arrowprops=dict(
arrowstyle='-',
connectionstyle=f'arc3,rad={rad}',
color=df.color.unique()[index],
linewidth=2,
linestyle='-',
antialiased=True
))
#add the labels
p1 = np.array([x_arr , y_arr])
p2 = np.array([end[index] + x_arr , end[index] + y_arr])
mid = (p1 + p2) / 2 #The center of the chord between the points
vec = p2 - p1 #the direction of the chord
length = np.linalg.norm(vec) #the length of the chord
perp = np.array([-vec[1], vec[0]]) / length #the direction fo the arc (upwards, downwards)
arc_height = rad * (length / 2) #The offset along the perpendicular, controlled by rad
arc_mid = mid + arc_height * perp #The actual midpoint of the arc, i.e. highest point of the curve
label_x, label_y = arc_mid
ax.text(
label_x ,
label_y + 1,
f"+{int(end[index])}",
color = df.color.unique()[index],
weight = "bold",
fontsize = 10
)
fig