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
from matplotlib.colors import to_rgba
color_dict = { 2004: "#A54836", 2022: "#5375D4", }
xy_ticklabel_color, label_color, grid_color, datalabels_color ='#757C85',"#101628", "#C8C9C9", "#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['pct_change'] = df.groupby('countries', sort=False)['sites'].apply(
lambda x: x.pct_change()).to_numpy().round(3)*100
df['ctry_code'] = df.countries.astype(str).str[:2].astype(str).str.upper()
df['year_lbl'] ="'"+df['year'].astype(str).str[-2:].astype(str)
sort_order_dict = {"Denmark": 3, "Sweden": 2, "Norway": 1, 2004: 4, 2022: 5}
df = df.sort_values( by=["countries","year"], key=lambda x: x.map(sort_order_dict))
#map the colors of a dict to a dataframe
df['color']= df.year.map(color_dict)
df
| year | countries | sites | pct_change | ctry_code | year_lbl | color | |
|---|---|---|---|---|---|---|---|
| 2 | 2004 | Norway | 5 | NaN | NO | '04 | #A54836 |
| 3 | 2022 | Norway | 8 | 60.0 | NO | '22 | #5375D4 |
| 4 | 2004 | Sweden | 13 | NaN | SW | '04 | #A54836 |
| 5 | 2022 | Sweden | 15 | 15.4 | SW | '22 | #5375D4 |
| 0 | 2004 | Denmark | 4 | NaN | DE | '04 | #A54836 |
| 1 | 2022 | Denmark | 10 | 150.0 | DE | '22 | #5375D4 |
Create a function for plotting a beizer with a gradient area.¶
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 left bar
- x1: the x-coordinate of the starting position of the right bar
- y0: the y-coordinate of the bottom edge of both bars
- height0: the height of the left horizontal bar
- height1: the height of the right horizontal bar
def create_gradient_image(color1, color2, height=100, width=200):
"""Create a left-to-right horizontal gradient image."""
gradient = np.linspace(0, 1, width)
gradient_rgb = np.outer(1 - gradient, color1[:3]) + np.outer(gradient, color2[:3])
img = np.tile(gradient_rgb[:, None, :], (1, height, 1)).transpose(1, 0, 2)
return img
def draw_horizontal_beizer(ax, x0, x1, y0, height0, height1, color1, color2, alpha=1.0):
"""
Draws a smooth cubic bezier curve between two horizontal bars, and fills the area between the curve and the bars.
Parameters:
----------
ax : The Axes object where the Bézier curve and area will be drawn.
x0 : float, the x-coordinate of the starting position of the left bar.
x1 : float, the x-coordinate of the starting position of the right bar.
y0 : float, the y-coordinate of the bottom edge of both bars.
height0 : float, the height of the left horizontal bar.
height1 : float the height of the right horizontal bar.
color1 : str, the color of the first gradient.
color2 : str, the color of the second gradient.
alpha : float, the transparency of the Bézier curve and area.
"""
Path = mpath.Path
# Define the closed loop path with top and bottom parts
top_left = (x0, y0 + height0) # Top left of Bar 1
top_right = (x1, y0 + height1) # Top right of Bar 2
bottom_left = (x0, y0) # Bottom left of Bar 1
bottom_right = (x1, y0) # Bottom right of Bar 2
# Define control points for the area
ctrl1_top = (x0 + 0.5, y0 + height0)
ctrl2_top = (x1 - 0.5, y0 + height1)
# Define path for the area with Bézier curve
vertices_area = [
top_left,
ctrl1_top,
ctrl2_top,
top_right,
bottom_right,
bottom_left,
top_left # Close the loop
]
codes_area = [
Path.MOVETO, # Start at top_left of Bar 1
Path.CURVE4, # First control point for top curve
Path.CURVE4, # Second control point for top curve
Path.CURVE4, # Top right of Bar 2
Path.LINETO, # Bottom right of Bar 2
Path.LINETO, # Bottom left of Bar 1
Path.CLOSEPOLY # Close the loop (back to top_left)
]
path = Path(vertices_area, codes_area)
patch = patches.PathPatch(path, facecolor='none', edgecolor='none', alpha=0.3)
ax.add_patch(patch)
#####
# Create gradient image
#####
#convert hex to rgb
color1_rgb = np.array(to_rgba(color1))
color2_rgb = np.array(to_rgba(color2))
img = create_gradient_image(color1_rgb, color2_rgb, height=300, width=600)
# Define image extent to match bridge area
xmin = x0
xmax = x1 + 0.5
ymin = y0 - 2
ymax = max(y0 + height0, y0 + height1) + 2
ax.imshow(
img,
extent=[xmin, xmax, ymin, ymax],
origin='lower',
aspect='auto',
zorder=0,
alpha=alpha,
clip_path=patch,
clip_on=True
)
Plot the chart¶
fig, axes = plt.subplots(nrows=3, sharey=True, figsize=(8, 10))
plt.subplots_adjust(hspace=0.4, wspace= 0.6)
bar_width = 2
for (country, group),ax in zip(df.groupby("countries", sort= False), axes.ravel()):
#print(group)
sites = group['sites'].tolist()
color = group['color'].tolist()
ax.bar(
[1, 4],
sites,
width = bar_width,
color = color,
#align= "edge"
)
# draw Bézier curve between them
bars = ax.patches
for i in range(len(bars) - 1): # Loop through consecutive bars
bar1 = bars[i]
bar2 = bars[i + 1]
# coordinates for fisrt bar
x0 = bar1.get_x() + bar1.get_width() # Right edge of Bar 1
y0 = bar1.get_y()
height0 = bar1.get_height()
# for second bar
x1 = bar2.get_x() # Left edge of Bar 2
height1 = bar2.get_height()
#add the beizer
draw_horizontal_beizer(ax, x0, x1, y0, height0, height1, color[0], color[1], alpha=.6)
x0 = []
for bar, row in zip(bars, group.itertuples()):
x0_value = bar.get_x() + bar_width
x0.append(x0_value)
#add the data labels
ax.text(
bar.get_x() + bar_width/2,
df.sites.max() + 4,
f"{row.sites}",
ha= "center",
size=14,
weight = "bold",
va = "center"
)
#add the site numbers
ax.text(
bar.get_x() + bar_width/2,
df.sites.max() + 2,
"World Heritage Sites",
ha= "center"
)
#add year labels
if row.countries == "Norway":
ax.text(
bar.get_x() + bar_width/2,
df.sites.max() + 8,
row.year,
size = 12,
color= xy_ticklabel_color,
ha= "center",
)
ax.text(
2.5,
df.sites.max() + 8,
"Change",
size = 12,
color= xy_ticklabel_color,
ha= "center",
)
#add change values
if row.year == df.year.max():
pcts = row.pct_change
if pcts == int(pcts):
display_value = f"{int(pcts)}%"
else:
display_value = f"{pcts:.1f}%"
#add pct_change
ax.text(
2.5,
df.sites.max() + 4,
display_value,
ha= "center",
size=14,
weight = "bold",
va = "center"
)
#add increase
ax.text(
2.5,
df.sites.max() + 2,
"Increase",
ha= "center",
)
#add the country labels
ax.text(
bar.get_x() - 3.5,
df.sites.max() + 4,
f"{row.countries}",
ha= "center",
size=12,
va = "center",
color = xy_ticklabel_color
)
line_params = {
'ymin' : 0,
'ymax': 1.4,
'color' : 'lightgrey',
'lw': 1,
'clip_on' : False
}
line_pos = [0,2,3,5]
for pos in line_pos:
ax.axvline( pos, **line_params)
ax.set(xlim=(0,5), ylim=(0,15))
ax.set_frame_on(False)
ax.tick_params(length=0, labelleft = False, labelbottom = False)