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.
# tutorial
import matplotlib.pyplot as plt
from matplotlib.patches import Arc
import numpy as np
import pandas as pd
color_dict = {
"Norway": "#2B314D",
"Denmark": "#A54836",
"Sweden": "#5375D4",
}
xy_ticklabel_color, xy_label_color, grid_color, datalabels_color = (
"#101628",
"#101628",
"#C8C9C9",
"#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 = df.sort_values(["year", "sites"], ascending=False).reset_index(drop=True)
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": 2, "Sweden": 1, "Norway": 3, 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 | color | |
|---|---|---|---|---|---|---|
| 3 | 2004 | Sweden | 13 | SW | '04 | #5375D4 |
| 5 | 2004 | Denmark | 4 | DE | '04 | #A54836 |
| 4 | 2004 | Norway | 5 | NO | '04 | #2B314D |
| 0 | 2022 | Sweden | 15 | SW | '22 | #5375D4 |
| 1 | 2022 | Denmark | 10 | DE | '22 | #A54836 |
| 2 | 2022 | Norway | 8 | NO | '22 | #2B314D |
df.sites[2::3]
4 5 2 8 Name: sites, dtype: int64
grouped = df.groupby("countries",sort = False)
Plot the chart¶
We are going to plot this as sine waves using ax.plot() method and need the following parameters:
| Parameter | Description | Value |
|---|---|---|
| x | The x position of each dot | x see notes below |
| y | The y position of each dot | y see notes below |
x will generate values over a range from 0 2π * max_year_sites and y will create the y values as a scaled sine wave with the height capped at 0.06
fig, ax = plt.subplots(figsize=(10, 3), sharex=True, sharey=True, facecolor="#FFFFFF", zorder=1)
smoothing = 100 # how many points are generated along the x-axis
for i, (country, group) in enumerate(grouped):
min_year_sites = group.iloc[0]["sites"] #get the values for 2004
max_year_sites = group.iloc[1]["sites"] #get the values for 2022
color = group.iloc[0]["color"]
year = group.year.iloc[0]
x = np.linspace(0, np.pi * max_year_sites * 2, smoothing * max_year_sites)
y = np.sin(x) * 0.06
ax.plot(
x,
y + i, # vertically stacked
linewidth=3,
solid_capstyle="round",
color=color,
label=country
)
ax.plot(
x[: smoothing * min_year_sites],
y[: smoothing * min_year_sites] + i
,
linewidth = 10,
solid_capstyle = "round",
color = color,
)
# add data labels
ax.text(
x[-1], #last point in the x list
y[-1] + i + 0.1,
max_year_sites,
size=12,
color=datalabels_color,
ha="center",
)
ax.text(
x[min_year_sites * smoothing], #last point in the mix_site list
y[min_year_sites * smoothing] + i + 0.1,
min_year_sites,
size=12,
color=datalabels_color,
ha="center",
va="bottom",
)
Add the year annotations¶
To add the year annotations we will use ax.annotate() method in combination with arrowprops and need the following parameters:
| Parameter | Description | Value |
|---|---|---|
| text | The annotation | year |
| xy | The start x and y positions of the line | |
| xytext | The end x and y positions of the line |
# add the year annotations
for site, year in zip(df.sites[2::3], df.year.unique()):
x = np.linspace(0, np.pi * site * 2, smoothing * site)
ax.annotate(
year,
xy=(x.max(), 2 + 0.35),
xytext=(x.max(), 2 + 0.85),
color="black",
size=12,
ha="center",
weight="bold",
arrowprops=dict(
arrowstyle="-",
color=datalabels_color,
),
annotation_clip=False,
)
fig
add the styling:
ax.yaxis.set_ticks(
range(3),
labels = reversed(df.countries.unique()),
weight = "bold")
ax.set_frame_on(False)
ax.tick_params(
axis = "both",
which = "major",
labelsize = 12,
length = 0,
color = xy_ticklabel_color
)
ax.set_xticks([])
fig
The complete code¶
import matplotlib.pyplot as plt
from matplotlib.patches import Arc
import numpy as np
import pandas as pd
color_dict = {
"Norway": "#2B314D",
"Denmark": "#A54836",
"Sweden": "#5375D4",
}
xy_ticklabel_color, xy_label_color, grid_color, datalabels_color = (
"#101628",
"#101628",
"#C8C9C9",
"#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 = df.sort_values(["year", "sites"], ascending=False).reset_index(drop=True)
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": 2, "Sweden": 1, "Norway": 3, 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)
grouped = df.groupby("countries",sort = False)
# ## Plot the chart
fig, ax = plt.subplots(figsize=(10, 3), sharex=True, sharey=True, facecolor="#FFFFFF", zorder=1)
smoothing = 100 # how many points are generated along the x-axis
for i, (country, group) in enumerate(grouped):
min_year_sites = group.iloc[0]["sites"] #get the values for 2004
max_year_sites = group.iloc[1]["sites"] #get the values for 2022
color = group.iloc[0]["color"]
year = group.year.iloc[0]
x = np.linspace(0, np.pi * max_year_sites * 2, smoothing * max_year_sites)
y = np.sin(x) * 0.06
ax.plot(
x,
y + i, # vertically stacked
linewidth=3,
solid_capstyle="round",
color=color,
label=country
)
ax.plot(
x[: smoothing * min_year_sites],
y[: smoothing * min_year_sites] + i
,
linewidth = 10,
solid_capstyle = "round",
color = color,
)
# add data labels
ax.text(
x[-1], #last point in the x list
y[-1] + i + 0.1,
max_year_sites,
size=12,
color=datalabels_color,
ha="center",
)
ax.text(
x[min_year_sites * smoothing], #last point in the mix_site list
y[min_year_sites * smoothing] + i + 0.1,
min_year_sites,
size=12,
color=datalabels_color,
ha="center",
va="bottom",
)
# add the year annotations
for site, year in zip(df.sites[2::3], df.year.unique()):
x = np.linspace(0, np.pi * site * 2, smoothing * site)
ax.annotate(
year,
xy=(x.max(), 2 + 0.35),
xytext=(x.max(), 2 + 0.85),
color="black",
size=12,
ha="center",
weight="bold",
arrowprops=dict(
arrowstyle="-",
color=datalabels_color,
),
annotation_clip=False,
)
# add the styling:
ax.yaxis.set_ticks(
range(3),
labels = reversed(df.countries.unique()),
weight = "bold")
ax.set_frame_on(False)
ax.tick_params(
axis = "both",
which = "major",
labelsize = 12,
length = 0,
color = xy_ticklabel_color
)
ax.set_xticks([])
[]