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_bars = {"Norway": "#2B314D", "Denmark": "#A54836", "Sweden": "#5375D4", }
color_beizer = {"Norway": "#61657B", "Denmark": "#DD9A8D", "Sweden": "#90B0F3", }
code_dict = {"Norway": "NO", "Denmark": "DK", "Sweden": "SE", }
xy_ticklabel_color ='#757C85'
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()*100
sort_order_dict = {"Denmark": 2, "Sweden": 1, "Norway": 3, 2004: 5, 2022: 4}
df = df.sort_values( by=["countries","year"], key=lambda x: x.map(sort_order_dict))
df['ctry_code'] = df.countries.map(code_dict)
#df['color']= df.countries.map(color_dict)
df
| year | countries | sites | pct_change | ctry_code | |
|---|---|---|---|---|---|
| 1 | 2022 | Sweden | 15 | 15.384615 | SE |
| 0 | 2004 | Sweden | 13 | NaN | SE |
| 3 | 2022 | Denmark | 10 | 150.000000 | DK |
| 2 | 2004 | Denmark | 4 | NaN | DK |
| 5 | 2022 | Norway | 8 | 60.000000 | NO |
| 4 | 2004 | Norway | 5 | NaN | NO |
Create a function for plotting a beizer between two bars.¶
To create a bezier area, we need the following points between the bars:
To simplify the creation and add the gradient we will use the function below that requires the following points:
- x0: the x-coordinate of the starting position of the bottom bar
- x1: the x-coordinate of the starting position of the top bar
- y0: the y-coordinate of the bottom edge of both bars
- y1: the y-coordinate of the top edge of both bars
- length0: the length of the bottom horizontal bar
- length1: the length of the top horizontal bar
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)
# Control points
ctrl1_left = (x0_position , y0 + .5 )
ctrl2_left = (x1_position , y1 - 0.5 )
ctrl1_right = (length1 , y1 - .5 )
ctrl2_right = (length0, y0 + 0.5 )
# 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",
fontsize=10,
ha="center",
va="center"
)
Plot the chart¶
fig,ax = plt.subplots(figsize=(5,6))
years = df.year.unique()
initial_bottom = np.array([0, 5])
bottom = initial_bottom.copy()
total_widths = np.zeros_like(bottom)
for country, group in df.groupby("countries", sort = False):
y = group["sites"].to_numpy()
# Compute middle of the bar segments for the year labels
total_widths += y
ax.barh(
range(len(years)),
y,
left = bottom,
height = 0.05,
color = color_bars[country]
)
bottom += y
ax.set(xlim=(-10, 40), ylim = (-.5,1.5))
print(initial_bottom)
[0 5]
Add the bezier areas
# Loop through the bars and draw a Bézier curve between each pair of consecutive bars
bars = ax.patches
for bar1, bar2, row0 in zip(bars[::2], bars[1::2], df[df.year == 2022].itertuples()):
#add the beizer curves
# For Bar 1 (first bar)
x0_bar1 = bar1.get_x() # Right edge of Bar 1
y0_bar1 = bar1.get_y() + bar1.get_height()
length_bar1 = x0_bar1 + bar1.get_width()
# For Bar 2 (second bar)
x0_bar2 = bar2.get_x() # Left edge of Bar 2
y0_bar2 = bar2.get_y()
length_bar2 = x0_bar2 + bar2.get_width()
draw_vertical_beizer(
ax,
x0_bar1,
x0_bar2,
y0=y0_bar1,
y1 = y0_bar2,
length0=length_bar1,
length1=length_bar2,
color1=color_beizer[row0.countries],
color2=color_beizer[row0.countries],
alpha=1,
text = f"{row0.ctry_code}\n{int(row0.pct_change)}%"
)
fig
Add the data labels to the bars:
direction = np.tile([-1,1],3)
for i, (row, bars) in enumerate(zip(df.itertuples(),ax.patches)):
ax.text(
bars.get_x() + bars.get_width()/2,
bars.get_y() + 0.1 * direction[i],
row.sites,
size = 10,
)
fig
Add the year labels. I dont know where the 77% comes from so I hardcoded it.
dir_y = [-1,1]
for i, total in enumerate(total_widths):
print()
ax.text(
initial_bottom[i] + total /2,
i + 0.3*dir_y[i],
years[i],
color = xy_ticklabel_color
)
if dir_y[i] == -1:
ax.text(
initial_bottom[i] + total /2,
i + 0.1,
"+77%",
color = "w",
size = 14,
weight = "bold",
ha = "center"
)
ax.set_axis_off()
fig