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.patches import FancyBboxPatch
from matplotlib.offsetbox import OffsetImage, AnnotationBbox
from matplotlib.colors import LinearSegmentedColormap
import pandas as pd
import numpy as np
color_dict = { 2022 : "#A54836", 2004: "#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['sub_total'] = df.groupby('countries')['sites'].transform('sum')
df = df.sort_values([ 'countries'], ascending=False ).reset_index(drop=True)
df['year_lbl'] ="'"+df['year'].astype(str).str[-2:].astype(str)
df['pct_change'] = (df.groupby('countries', sort=False)['sites'].apply(lambda x: x.pct_change()).to_numpy()).round(3)
#map the colors of a dict to a dataframe
df['color']= df.year.map(color_dict)
df
| year | countries | sites | sub_total | year_lbl | pct_change | color | |
|---|---|---|---|---|---|---|---|
| 0 | 2004 | Sweden | 13 | 28 | '04 | NaN | #5375D4 |
| 1 | 2022 | Sweden | 15 | 28 | '22 | 0.154 | #A54836 |
| 2 | 2004 | Norway | 5 | 13 | '04 | NaN | #5375D4 |
| 3 | 2022 | Norway | 8 | 13 | '22 | 0.600 | #A54836 |
| 4 | 2004 | Denmark | 4 | 14 | '04 | NaN | #5375D4 |
| 5 | 2022 | Denmark | 10 | 14 | '22 | 1.500 | #A54836 |
Function to add a gradient color to a line¶
The function multiColorLine will add a gradient color to a line:
def multiColorLine(xstart, xend, ystart, yend, npoints, line_thickness, colors, ax, ):
'''
This function colors a "line" in gradient colors
'''
x = np.linspace(xstart, xend, npoints)
y = [ystart]*(int(npoints/3)) + np.linspace(ystart,yend,int(npoints/3)).tolist() + [yend]*int(npoints/3)
cmap = LinearSegmentedColormap.from_list("", colors)
norm = plt.Normalize(x.min(), x.max())
line_colors = cmap(norm(x))
ax.scatter(
x,
y,
color=line_colors,
s=line_thickness
)
Get the flags:
img = [
plt.imread("../flags/sw-sq.png"),
plt.imread("../flags/no-sq.png"),
plt.imread("../flags/de-sq.png")
]
line_offset = .3
bar_width = .3
Plot the chart¶
fig, axes = plt.subplots(ncols = df.countries.nunique(), nrows = 1, figsize=(8,5),sharex=True, sharey=True, facecolor = "#FFFFFF", zorder= 1)
fig.tight_layout(pad=1.0)
for ax, (i,(country, group)) in zip(axes.ravel(), enumerate(df.groupby("countries"))):
colors = group.color
x = range(len(df.year.unique()))
ax.bar(
x,
height = group.sites,
width = bar_width,
color = group.color)
#add the multicolor lines
multiColorLine(0 - bar_width / 2, 1 + bar_width / 2, -0.5, -0.5, 900, 0.01, colors.tolist(), ax)
multiColorLine(0 - bar_width / 2, 1 + bar_width / 2, group.sites.iloc[0] + line_offset , group.sites.iloc[1] + line_offset, 900, 0.01, colors.tolist(), ax)
#add the grand totals at the top of the bars
for bar, site in zip(ax.patches, group.sites):
ax.text(
bar.get_x() + bar.get_width() / 2,
round(bar.get_height())+0.5, #height
site,
ha="center",
va="bottom",
size= 12,
color = label_color,
weight= "bold"
)
#add the image
image_box = OffsetImage(img[i], zoom = 0.05) #container for the image
ab = AnnotationBbox(
image_box,
(0.5,1),
xybox= (0.5, -4),
frameon = False) # the x coordinate is the year axis
ax.add_artist(ab)
value = (group['pct_change'].iloc[1])*100
if value == int(value):
display_value = f"{int(value)}%"
else:
display_value = f"{value:.1f}%"
#add data annotations
ax.annotate(
f'\u25B2\n{display_value}',
xy= (.5, .2),
size= 12,
color= label_color,
weight = "bold",
va = "center",
ha = "center",
xycoords="axes fraction"
)
#add styling
#round the edges of the bars
new_patches = []
for patch in reversed(ax.patches):
bb = patch.get_bbox()
color = patch.get_facecolor()
p_bbox = FancyBboxPatch(
(bb.xmin, bb.ymin),
abs(bb.width),
abs(bb.height),
boxstyle="round,pad=.005,rounding_size=.2",
ec="none",
fc=color,
mutation_aspect=0.2
)
patch.remove()
new_patches.append(p_bbox)
for patch in new_patches:
ax.add_patch(patch)
ax.set_frame_on(False)
ax.set_xlabel(
country,
size=14,
color = label_color,
labelpad = 58
)
ax.xaxis.set_ticks(
x,
labels =group.year_lbl,
color= label_color
)
ax.tick_params(
axis='both',
which='major',
length=0,
labelsize=11,
colors= label_color,
labelleft = False
)