pythonmatplotlib

Create an arc between two points in matplotlib


I am trying to recreate the chart below using matplotlib: enter image description here

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()

Which gives me this: enter image description here

I have tried both mpatches.arc and patches and path but cant make it work.


Solution

  • Semicircle arc between two points

    To draw a semicircle between two points:

    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()
    

    matplotlib semicircles between two points

    Specific code for the data in the question

    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()
    

    matplotlib arcs between points