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.path import Path
from matplotlib.patches import PathPatch
import numpy as np
import pandas as pd
color_dict = {
"Norway": "#2B314D",
"Denmark": "#A54836",
"Sweden": "#5375D4",
}
code_dict = {
"Norway": "NO",
"Denmark": "DK",
"Sweden": "SE",
}
xy_ticklabel_color, xy_lablel_color, grid_color, datalabels_color = (
"#C8C9C9",
"#9BA0A6",
"#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 = df.sort_values(["countries"], ascending=True).reset_index(drop=True)
# map the colors of a dict to a dataframe
df["color"] = df.countries.map(color_dict)
df["ctry_code"] = df.countries.map(code_dict)
df
| year | countries | sites | color | ctry_code | |
|---|---|---|---|---|---|
| 0 | 2004 | Denmark | 4 | #A54836 | DK |
| 1 | 2022 | Denmark | 10 | #A54836 | DK |
| 2 | 2004 | Norway | 5 | #2B314D | NO |
| 3 | 2022 | Norway | 8 | #2B314D | NO |
| 4 | 2004 | Sweden | 13 | #5375D4 | SE |
| 5 | 2022 | Sweden | 15 | #5375D4 | SE |
Create the variables that we will reuse:
colors = df.color.unique()
countries = df.countries.unique()
Plot the chart¶
First, lets turn the plot upside down and the limits of the plot:
fig, ax = plt.subplots(figsize=(6,6), facecolor = "#FFFFFF")
ax.xaxis.tick_top()
ax.xaxis.set_label_position("top")
ax.invert_yaxis()
ax.set(
xlim=[0, 16],
ylim=[15, 0]
)
[(0.0, 16.0), (15.0, 0.0)]
Add the arcs¶
We will first group the data by color and sites:
grouped_sites = df.groupby("color")["sites"].apply(np.array)
grouped_sites
color #2B314D [5, 8] #5375D4 [13, 15] #A54836 [4, 10] Name: sites, dtype: object
We are going to use path and PathPatch to draw a Bezier curve on the plot.
This creates a curved path from point A → B → C, where the middle point (B) acts as a control point for a Bézier curve between A and C.
verts are the points used to draw a quadratic Bézier curve: So, Start at (y, 0), curve towards (0, x) with (y, x) as the control point.
codes are Path codes that tell matplotlib how to interpret the points:
- Path.MOVETO move to the first point without drawing
- Path.CURVE3 draw a quadratic Bézier curve using a control point. The second point is the control point and the third point is the curve’s endpoint
for color, group in grouped_sites.items():
x = group[0]
y = group[1]
print(group, x, y, color)
verts = [
(y, 0), # Start point (A)
(y, x), # Control point (B)
(0, x) # End point (C)
]
codes = [Path.MOVETO, Path.CURVE3, Path.CURVE3]
ax.add_patch(
PathPatch(
Path(
verts,
codes
),
fc = "none",
color = color,
lw = 4
)
)
fig
[5 8] 5 8 #2B314D [13 15] 13 15 #5375D4 [ 4 10] 4 10 #A54836
Add the labels¶
I am positionig the labels manually to place them in the same position as the original chart.
text_coord = [
{"x":12, "y":10, "s":"SE", "weight": "bold", "color": colors[2] },
{"x":3, "y":3.5, "s":"DK", "weight": "bold", "color": colors[0] },
{"x":5, "y":5, "s":"NO", "weight": "bold", "color": colors[1]},
]
for coord in text_coord:
ax.text(**coord)
fig
Add the final styling¶
ax.set_xlabel(
df.year.max(),
size = 14,
color = xy_lablel_color,
weight = "bold"
)
ax.set_ylabel(
df.year.min(),
size = 12,
color = xy_lablel_color,
weight = "bold"
)
ax.spines[["left", "top"]].set_color(grid_color)
ax.spines[["bottom", "right"]].set_color("w")
ax.tick_params(
axis = "both",
which = "major",
length = 0,
labelsize = 12,
colors = xy_ticklabel_color
)
ax.xaxis.set_ticks(np.arange(0, 20, 5))
ax.yaxis.set_ticks(np.arange(0, 20, 5))
fig
The final code¶
import matplotlib.pyplot as plt
from matplotlib.path import Path
from matplotlib.patches import PathPatch
import numpy as np
import pandas as pd
color_dict = {
"Norway": "#2B314D",
"Denmark": "#A54836",
"Sweden": "#5375D4",
}
code_dict = {
"Norway": "NO",
"Denmark": "DK",
"Sweden": "SE",
}
xy_ticklabel_color, xy_lablel_color, grid_color, datalabels_color = (
"#C8C9C9",
"#9BA0A6",
"#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 = df.sort_values(["countries"], ascending=True).reset_index(drop=True)
# map the colors of a dict to a dataframe
df["color"] = df.countries.map(color_dict)
df["ctry_code"] = df.countries.map(code_dict)
# Create the variables that we will reuse:
colors = df.color.unique()
countries = df.countries.unique()
# ## Plot the chart
fig, ax = plt.subplots(figsize=(6,6), facecolor = "#FFFFFF")
ax.xaxis.tick_top()
ax.xaxis.set_label_position("top")
ax.invert_yaxis()
ax.set(
xlim=[0, 16],
ylim=[15, 0]
)
# ## Add the arcs
grouped_sites = df.groupby("color")["sites"].apply(np.array)
for color, group in grouped_sites.items():
x = group[0]
y = group[1]
verts = [
(y, 0), # Start point (A)
(y, x), # Control point (B)
(0, x) # End point (C)
]
codes = [Path.MOVETO, Path.CURVE3, Path.CURVE3]
ax.add_patch(
PathPatch(
Path(
verts,
codes
),
fc = "none",
color = color,
lw = 4
)
)
# ## Add the labels
text_coord = [
{"x":12, "y":10, "s":"SE", "weight": "bold", "color": colors[2] },
{"x":3, "y":3.5, "s":"DK", "weight": "bold", "color": colors[0] },
{"x":5, "y":5, "s":"NO", "weight": "bold", "color": colors[1]},
]
for coord in text_coord:
ax.text(**coord)
# ### Add the final styling
ax.set_xlabel(
df.year.max(),
size = 14,
color = xy_lablel_color,
weight = "bold"
)
ax.set_ylabel(
df.year.min(),
size = 12,
color = xy_lablel_color,
weight = "bold"
)
ax.spines[["left", "top"]].set_color(grid_color)
ax.spines[["bottom", "right"]].set_color("w")
ax.tick_params(
axis = "both",
which = "major",
length = 0,
labelsize = 12,
colors = xy_ticklabel_color
)
ax.xaxis.set_ticks(np.arange(0, 20, 5))
ax.yaxis.set_ticks(np.arange(0, 20, 5))
[<matplotlib.axis.YTick at 0x2973ddbc2c0>, <matplotlib.axis.YTick at 0x2971afbc0e0>, <matplotlib.axis.YTick at 0x2973fe5a9c0>, <matplotlib.axis.YTick at 0x2973fe5b3b0>]