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.lines import Line2D
import numpy as np
import pandas as pd
color_dict = {
"Norway": "#2B314D",
"Denmark": "#A54836",
"Sweden": "#5375D4",
}
xy_label_color, legend_color, arc_color = "#101628", "#101628", "#757C85"
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["ctry_code"] = df.countries.astype(str).str[:2].astype(str).str.upper()
df["year_lbl"] = "'" + df["year"].astype(str).str[-2:].astype(str)
df["sub_total"] = df.groupby("year")["sites"].transform("sum")
#custom sort
sort_order_dict = {"Denmark": 1, "Sweden": 3, "Norway": 2, 2004: 4, 2022: 5}
df = df.sort_values( by=[ "year", "countries",], key=lambda x: x.map(sort_order_dict),)
# map the colors of a dict to a dataframe
df["color"] = df.countries.map(color_dict)
df
| year | countries | sites | ctry_code | year_lbl | sub_total | color | |
|---|---|---|---|---|---|---|---|
| 0 | 2004 | Denmark | 4 | DE | '04 | 22 | #A54836 |
| 2 | 2004 | Norway | 5 | NO | '04 | 22 | #2B314D |
| 4 | 2004 | Sweden | 13 | SW | '04 | 22 | #5375D4 |
| 1 | 2022 | Denmark | 10 | DE | '22 | 33 | #A54836 |
| 3 | 2022 | Norway | 8 | NO | '22 | 33 | #2B314D |
| 5 | 2022 | Sweden | 15 | SW | '22 | 33 | #5375D4 |
Create a group by year in pandas to loop by it later:
groups = df.groupby('year')
arc_length = 2*np.pi/df.sub_total.min()
Plot the chart¶
We will use ax.plot() method and need the following parameters:
| Parameter | Description | Value |
|---|---|---|
| [x, x] | The angles in radians of each line | [angle, angle] |
| [y, y] | The length of each line | [0,1] |
To determine the angle between each bar, we divide the full circle (360 degrees / 2π radians) by the total number of sites per year — this gives us bar_angles . Now, we can calculate the specific angle for each line by using np.arange which creates an array of angles between 0 and 2 radians in bar_angles increments.
Finally ax.plot draws radial lines (like spokes on a wheel) from the center of the polar chart (radius = 0) out to the edge (radius = 1) at each angle in the angles array.
We will also add the labels and some styling:
fig, axes = plt.subplots(nrows=df.year.nunique(), ncols=1, figsize=(6, 6), subplot_kw=dict(polar=True))
fig.tight_layout(pad=3.0)
for (year, group), ax in zip(groups, axes.ravel()):
site = group.sites
#accumulate the sites values
sites_acc = [0] + np.cumsum(site).tolist()
# add color list for the bars sites*color
bar_colors = np.repeat(group.color, site).to_numpy()
# calculate the angles for each bar
bar_angles = 2 * np.pi / group.sub_total.max()
angles = np.arange(0, 2 * np.pi, bar_angles)
ax.plot([angles, angles], [0, 1], lw=4, c="#CC5A43")
# add bar colors
for i, j in enumerate(ax.lines):
j.set_color(bar_colors[i])
# add year labels
ax.text(
0.5,
0.5,
group.year.max(),
size = 10,
transform = ax.transAxes, #use the center of the axis as coordinates
ha = "center",
va = "center",
color = xy_label_color,
)
#styling
ax.set_rorigin(-4)
ax.set_theta_zero_location("N")
ax.set_theta_direction(-1)
ax.tick_params( labelbottom = False, labelleft = False)
ax.grid(False)
ax.set_rmax(1)
ax.spines[["polar", "inner"]].set_color("w")
Add the circular labels¶
Add the circular labels:
fig, axes = plt.subplots(nrows=df.year.nunique(), ncols=1, figsize=(6, 6), subplot_kw=dict(polar=True))
fig.tight_layout(pad=3.0)
for (year, group), ax in zip(groups, axes.ravel()):
site = group.sites
color = group.color
#accumulate the sites values
sites_acc = [0] + np.cumsum(site).tolist()
# add color list for the bars sites*color
bar_colors = np.repeat(color, site).to_numpy()
# calculate the angles for each bar
bar_angles = 2 * np.pi / group.sub_total.max()
angles = np.arange(0, 2 * np.pi, bar_angles)
ax.plot([angles, angles], [0, 1], lw=4, c="#CC5A43")
# add bar colors
for i, j in enumerate(ax.lines):
j.set_color(bar_colors[i])
# add year labels
ax.text(
0.5,
0.5,
group.year.max(),
size = 10,
transform = ax.transAxes, #use the center of the axis as coordinates
ha = "center",
va = "center",
color = xy_label_color,
)
#styling
ax.set_rorigin(-4)
ax.set_theta_zero_location("N")
ax.set_theta_direction(-1)
ax.tick_params( labelbottom = False, labelleft = False)
ax.grid(False)
ax.set_rmax(1)
ax.spines[["polar", "inner"]].set_color("w")
# add the annotation arrows by iterating through adjacent pairs of items in the site_Acc list
for i, color, site in zip(range(len(sites_acc) - 1), color, site):
#find the midpoint
angle_mid = np.median(
np.arange(
sites_acc[i] * bar_angles,
sites_acc[i + 1] * bar_angles,
bar_angles
)
)
#calculate the legth of the arch
angle_range = np.arange(
angle_mid - arc_length / 2,
angle_mid + arc_length / 2,
0.001
)
#calculate the radius
r = np.ones_like(angle_range) * 1.8
#plot the arc
ax.plot(
angle_range,
r,
lw = 1,
c = arc_color,
clip_on = False
)
#plot the perpendicular line
start_radius = 1.8
end_radius = 2.5
ax.plot(
[angle_mid, angle_mid],
[start_radius, end_radius],
lw=1,
c=arc_color,
clip_on=False
)
text_radius = 3.5
ax.text(
angle_mid,
text_radius,
site,
color = color,
ha = "center",
va = "center")
The final code¶
We just add the lengend and we are done!
import matplotlib.pyplot as plt
from matplotlib.lines import Line2D
import numpy as np
import pandas as pd
color_dict = {
"Norway": "#2B314D",
"Denmark": "#A54836",
"Sweden": "#5375D4",
}
xy_label_color, legend_color, arc_color = "#101628", "#101628", "#757C85"
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["ctry_code"] = df.countries.astype(str).str[:2].astype(str).str.upper()
df["year_lbl"] = "'" + df["year"].astype(str).str[-2:].astype(str)
df["sub_total"] = df.groupby("year")["sites"].transform("sum")
#custom sort
sort_order_dict = {"Denmark": 1, "Sweden": 3, "Norway": 2, 2004: 4, 2022: 5}
df = df.sort_values( by=[ "year", "countries",], key=lambda x: x.map(sort_order_dict),)
# map the colors of a dict to a dataframe
df["color"] = df.countries.map(color_dict)
groups = df.groupby('year')
# calculate the angle of each bar
arc_length = 2 * np.pi / df.sub_total.min()
# The we add the labels and do the styling before adding the outer annotations:
fig, axes = plt.subplots(nrows=df.year.nunique(), ncols=1, figsize=(6, 6), subplot_kw=dict(polar=True))
fig.tight_layout(pad=3.0)
for (year, group), ax in zip(groups, axes.ravel()):
site = group.sites
color = group.color
#accumulate the sites values
sites_acc = [0] + np.cumsum(site).tolist()
# add color list for the bars sites*color
bar_colors = np.repeat(color, site).to_numpy()
# calculate the angles for each bar
bar_angles = 2 * np.pi / group.sub_total.max()
angles = np.arange(0, 2 * np.pi, bar_angles)
ax.plot([angles, angles], [0, 1], lw=4, c="#CC5A43")
# add bar colors
for i, j in enumerate(ax.lines):
j.set_color(bar_colors[i])
# add year labels
ax.text(
0.5,
0.5,
group.year.max(),
size = 10,
transform = ax.transAxes, #use the center of the axis as coordinates
ha = "center",
va = "center",
color = xy_label_color,
)
#styling
ax.set_rorigin(-4)
ax.set_theta_zero_location("N")
ax.set_theta_direction(-1)
ax.tick_params( labelbottom = False, labelleft = False)
ax.grid(False)
ax.set_rmax(1)
ax.spines[["polar", "inner"]].set_color("w")
# add the annotation arrows by iterating through adjacent pairs of items in the site_Acc list
for i, color, site in zip(range(len(sites_acc) - 1), color, site):
#find the midpoint
angle_mid = np.median(
np.arange(
sites_acc[i] * bar_angles,
sites_acc[i + 1] * bar_angles,
bar_angles
)
)
#calculate the legth of the arch
angle_range = np.arange(
angle_mid - arc_length / 2,
angle_mid + arc_length / 2,
0.001
)
#calculate the radius
r = np.ones_like(angle_range) * 1.8
#plot the arc
ax.plot(
angle_range,
r,
lw = 1,
c = arc_color,
clip_on = False
)
#plot the perpendicular line
start_radius = 1.8
end_radius = 2.5
ax.plot(
[angle_mid, angle_mid],
[start_radius, end_radius],
lw=1,
c=arc_color,
clip_on=False
)
text_radius = 3.5
ax.text(
angle_mid,
text_radius,
site,
color = color,
ha = "center",
va = "center")
# add legend
lines = [ Line2D( [0], [0],color=c, linestyle="-", lw=4, ) for c in df.color ]
plt.legend(
lines,
df.countries.unique().tolist(),
labelcolor=legend_color,
bbox_to_anchor=(1.7, -0.25),
loc="lower center",
frameon=False,
fontsize=10,
)
<matplotlib.legend.Legend at 0x22c5a8775c0>