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 matplotlib.path as mpath
import numpy as np
import pandas as pd
color_dict = {"Norway": "#2B314D", "Denmark": "#A54836", "Sweden": "#5375D4", }
color_beizer = {"Norway": "#61657B", "Denmark": "#DD9A8D", "Sweden": "#90B0F3", }
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)
df['pct_change'] = df.groupby('countries', sort=False)['sites'].apply(
lambda x: x.pct_change()).to_numpy().round(3)*100
#custom sort a dataframe
sort_order_dict = {"Denmark":2, "Sweden":3, "Norway":1, 2004:5, 2022:4}
df = df.sort_values(by=['countries','year',], key=lambda x: x.map(sort_order_dict))
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)
df
| year | countries | sites | pct_change | year_lbl | color | |
|---|---|---|---|---|---|---|
| 5 | 2022 | Norway | 8 | 60.0 | '22 | #2B314D |
| 4 | 2004 | Norway | 5 | NaN | '04 | #2B314D |
| 3 | 2022 | Denmark | 10 | 150.0 | '22 | #A54836 |
| 2 | 2004 | Denmark | 4 | NaN | '04 | #A54836 |
| 1 | 2022 | Sweden | 15 | 15.4 | '22 | #5375D4 |
| 0 | 2004 | Sweden | 13 | NaN | '04 | #5375D4 |
separation = 10 #distance between the plots
width_of_bar = 2
middle_of_plot = df.sites.max()
fig, ax = plt.subplots(figsize=(12,12))
for i, row in enumerate(df.itertuples()):
x= middle_of_plot - row.sites / 2
height = separation * i
ax.broken_barh(
[(x, row.sites) ],
(height, width_of_bar),
facecolors=row.color
)
#add year label
ax.text(
x - 1,
height + width_of_bar / 2,
row.year_lbl,
size = 10,
color = "#777F87",
va= "center"
)
#number of sites
ax.text(
middle_of_plot,
height + width_of_bar / 2,
row.sites,
color = "w",
weight = "bold",
va= "center"
)
if row.year == df.year.min():
#countries
ax.text(
middle_of_plot,
height + 3,
row.countries,
size = 12,
color = "#777F87",
clip_on=False,
ha = "center"
)
def draw_vertical_beizer(ax, x0_position, x1_position, y0, y1, length0, length1, color1, color2, alpha, offset_text=0, text = None):
"""
Draws a smooth cubic Bézier curve (bridge) between two vertical bars.
Parameters:
----------
ax : The Axes object to which the bars and Bézier curve will be added.
x0 : float, the x-coordinate of the starting position of the bottom horizontal bar.
x1 : float, the x-coordinate of the starting position of the top horizontal bar.
y0 : float, The y-coordinate of the starting position of the bottom horizontal bar.
y1 : float, The y-coordinate of the starting position of the top horizontal bar.
length0 : float, the height of the first horizontal bar.
length1 : float, the height of the second horizontal bar.
facecolor : str, the color of the beizer area.
edgecolor : str, the color of the edge area
alpha : float, the transparency of the Bézier curve.
offset_text : str, pffset the x coodinate for the text.
text : str, add text at the center of the beizer aera.
"""
Path = mpath.Path
# Define main points
top_left = (x1_position, y1 )
top_right = (length1, y1 )
bottom_right = ( length0, y0)
bottom_left = (x0_position, y0)
offset_pts = 2
# Control points
ctrl1_left = (x0_position , y0 + offset_pts )
ctrl2_left = (x1_position , y1 - offset_pts )
ctrl1_right = (length1 , y1 - offset_pts )
ctrl2_right = (length0, y0 + offset_pts )
# Define path
vertices = [
bottom_left,
ctrl1_left,
ctrl2_left,
top_left,
top_right,
ctrl1_right,
ctrl2_right,
bottom_right,
bottom_left # CLOSEPOLY requires repeating first point
]
codes = [
Path.MOVETO, # top_left
Path.CURVE4,
Path.CURVE4,
Path.CURVE4, # top_right
Path.LINETO, # bottom_right (start new subpath)
Path.CURVE4,
Path.CURVE4,
Path.LINETO,
Path.CLOSEPOLY # close the full loop
]
path = Path(vertices, codes)
patch = patches.PathPatch(path, facecolor=color1, edgecolor=color2, alpha = alpha)
ax.add_patch(patch)
#######
#add text at the center of the area
###########
# Find the bounding box of the Bézier path
xs = [p[0] for p in vertices]
ys = [p[1] for p in vertices]
# Calculate the center of the bounding box
x_min, x_max = min(xs), max(xs)
y_min, y_max = min(ys), max(ys)
center_x = (x_min + x_max) / 2
center_y = (y_min + y_max) / 2
# Add text in the center of the bounding box
if text:
ax.text(
center_x + offset_text,
center_y,
text,
color = "w",
weight= "bold",
ha= "center",
va = "center",
bbox= dict(
fc= "#151C32", ec = "#151C32", boxstyle = 'round, pad=0.5'))
for bars1, bars2, row in zip(ax.collections[::2], ax.collections[1::2], df[df.year==df.year.max()].itertuples()):
for bar_1, bar_2 in zip(bars1.get_paths(), bars2.get_paths()):
#add the beizer curves
bar1 = bar_1.vertices
x0_bar1 = bar1[0][0]
y0_bar1 = bar1[0][1] +2
length_bar1 = x0_bar1 + bar1[2][0] - bar1[0][0]
bar2 = bar_2.vertices
x0_bar2 = bar2[0][0]
y0_bar2 = bar2[0][1]
length_bar2 = x0_bar2 + bar2[2][0] - bar2[0][0]
pcts = row.pct_change
if pcts == int(pcts):
display_value = f"{int(pcts)}%"
else:
display_value = f"{pcts:.1f}%"
draw_vertical_beizer(
ax,
x0_bar1,
x0_bar2,
y0 = y0_bar1,
y1 = y0_bar2,
length0=length_bar1,
length1=length_bar2,
color1=color_beizer[row.countries],
color2=color_beizer[row.countries],
alpha=1,
text = f"{display_value}"
)
ax.set_axis_off()
fig