I am trying to recreate the chart below using matplotlib:
I have most of it done but, I just cant figure out how to create the arcs between the years:
import matplotlib.pyplot as plt
from scipy.interpolate import interp1d
import numpy as np
import pandas as pd
colors = ["#CC5A43","#2C324F","#5375D4",]
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'], ascending=True ).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)
sites = df.sites
lbl1 = df.year_lbl
fig, ax = plt.subplots( figsize=(6,6),sharex=True, sharey=True, facecolor = "#FFFFFF", zorder= 1)
ax.scatter(sites, sites, s= 340, c= colors*2 , zorder = 1)
ax.set_xlim(0, sites.max()+3)
ax.set_ylim(0, sites.max()+3)
ax.axline([ax.get_xlim()[0], ax.get_ylim()[0]], [ax.get_xlim()[1], ax.get_ylim()[1]], zorder = 0, color ="#DBDEE0" )
for i, l1 in zip(range(0,6), lbl1) :
ax.annotate(l1, (sites[i], sites[i]), color = "w",va= "center", ha = "center")
ax.set_axis_off()
I have tried both mpatches.arc and patches and path but cant make it work.
To draw a semicircle between two points:
width
and height
both need to be set to the diameter; that diameter is the distance between the two points (square root of sum of squares of the x and y differences)Encapsulated in a function, together with a little test:
import matplotlib.pyplot as plt
from matplotlib.patches import Arc
import numpy as np
def draw_semicircle(x1, y1, x2, y2, color='black', lw=1, ax=None):
'''
draw a semicircle between the points x1,y1 and x2,y2
the semicircle is drawn to the left of the segment
'''
ax = ax or plt.gca()
# ax. Scatter([x1, x2], [y1, y2], s=100, c=color)
startangle = np.degrees(np.arctan2(y2 - y1, x2 - x1))
diameter = np.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) # Euclidian distance
ax.add_patch(Arc(((x1 + x2) / 2, (y1 + y2) / 2), diameter, diameter, theta1=startangle, theta2=startangle + 180,
edgecolor=color, facecolor='none', lw=lw, zorder=0))
angle = np.linspace(0, 38, 80)
x = angle * np.cos(angle)
y = - angle * np.sin(angle)
fig, ax = plt.subplots()
for x1, y1, x2, y2 in zip(x[:-1], y[:-1], x[1:], y[1:]):
draw_semicircle(x1, y1, x2, y2, color='fuchsia', lw=2)
ax.set_aspect('equal') # show circles without deformation
ax.autoscale_view() # fit the arc into the data limits
ax.axis('off')
plt.show()
Here is an adaption of the code for your case (180º arc on a 45º line). The text can be positioned using the x coordinate of the first and the y coordinate of the second point.
import matplotlib.pyplot as plt
from matplotlib.patches import Arc
import pandas as pd
import math
colors = ["#CC5A43", "#2C324F", "#5375D4"]
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'], ascending=True).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)
sites = df.sites
lbl1 = df.year_lbl
countries = df.ctry_code
fig, ax = plt.subplots(figsize=(6, 6), sharex=True, sharey=True, facecolor="#FFFFFF", zorder=1)
ax.scatter(sites, sites, s=340, c=colors * 2, zorder=1)
ax.set_xlim(0, sites.max() + 3)
ax.set_ylim(0, sites.max() + 3)
ax.set_aspect('equal')
ax.axline([ax.get_xlim()[0], ax.get_ylim()[0]], [ax.get_xlim()[1], ax.get_ylim()[1]], zorder=0, color="#DBDEE0")
for site, l1 in zip(sites, lbl1):
ax.annotate(l1, (site, site), color="w", va="center", ha="center")
for x1, x2, color, country in zip(sites[:len(sites) // 2], sites[len(sites) // 2:], colors, countries):
center = (x1 + x2) / 2
diameter = math.sqrt((x2 - x1) ** 2 + (x2 - x1) ** 2) # Euclidian distance
ax.add_patch(Arc((center, center), diameter, diameter, theta1=45, theta2=225,
edgecolor=color, facecolor='none', lw=2))
ax.annotate(country, (x1, x2), color=color, va="center", ha="center",
bbox=dict(boxstyle="round, pad=0.5", facecolor="aliceblue", edgecolor=color, lw=2))
ax.set_axis_off()
plt.show()