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
import numpy as np
import pandas as pd
color_dict = {"Norway": "#2B314D", "Denmark": "#A54836", "Sweden": "#5375D4", }
xy_ticklabel_color, grand_totals_color, grid_color, datalabels_color ='#757C85',"#101628", "#C8C9C9", "#FFFFFF"
data = {
"year": [2004, 2022, 2004, 2022, 2004, 2022],
"countries" : ["Sweden", "Sweden", "Denmark", "Denmark", "Norway", "Norway"],
"sites": [13,15,4,10,5,8]
}
df= pd.DataFrame(data)
df = df.sort_values([ 'year'], ascending=True ).reset_index(drop=True)
df['new_sites'] = df.groupby(['countries'])['sites'].diff()
df['new_sites'] = df['new_sites'].fillna(df.sites)
#map the colors of a dict to a dataframe
df['color']= df.countries.map(color_dict)
df
| year | countries | sites | new_sites | color | |
|---|---|---|---|---|---|
| 0 | 2004 | Sweden | 13 | 13.0 | #5375D4 |
| 1 | 2004 | Denmark | 4 | 4.0 | #A54836 |
| 2 | 2004 | Norway | 5 | 5.0 | #2B314D |
| 3 | 2022 | Sweden | 15 | 2.0 | #5375D4 |
| 4 | 2022 | Denmark | 10 | 6.0 | #A54836 |
| 5 | 2022 | Norway | 8 | 3.0 | #2B314D |
Lets define first the position of the axes where the circles will be placed:
#add the bars
y_position = [0.31, 0.21]
height = [0.4, 0.6]
origins = [0, -2]
Plot the chart¶
# Function for generating sunflower pattern points
def sunflower_points(n, r_min=0, r_max=100):
points = []
phi = np.pi * (3 - np.sqrt(5)) # Golden angle
for i in range(n):
r = np.sqrt(i / n) * (r_max - r_min) + r_min
theta = i * phi
x = r * np.cos(theta)
y = r * np.sin(theta)
points.append((x, y))
return points
marker_size = 20
dot_radius = 0.34 # padding to prevent dot overlap
y_position = [-2, 2]
radius = [6.5, 9]
ang_degrees = [np.pi / 3 * i for i in range(1, 4)]
ang_degrees
[1.0471975511965976, 2.0943951023931953, 3.141592653589793]
fig, ax = plt.subplots(figsize=(6, 6))
ax.set(xlim = (-10, 10) , ylim = (-10, 10))
ax.set_aspect('equal')
ax.set_facecolor('white')
for i, (year, group) in enumerate(df.groupby("year")):
total = int(group['new_sites'].sum())
countries = group['countries'].unique()
colors = group['color'].unique()
r = radius[i] # assuming radius is defined elsewhere
#add circles
circle = plt.Circle((0, 0), r, color='#7A848A', fill=False, lw = .3, linestyle='-')
ax.add_artist(circle)
ax.text(0, r, year, color = "#7A848A", ha= "center", va= "center", bbox = dict(fc = "w", ec = "w", pad = .1))
if year == 2004:
#pack bubbles
r_min = 0 + dot_radius
r_max = radius[0] - .5
points = sunflower_points(total, r_min, r_max)
idx = 0
for _, row in group.iterrows():
country = row['countries']
count = int(row['new_sites'])
color = color_dict[country]
for _ in range(count):
x, y = points[idx]
ax.plot(x, y, 'o', color=color, markersize=marker_size, markeredgecolor='white')
idx += 1
elif year == 2022:
r_min = 6 + dot_radius
r_max = radius[1]
angle_step = 2 * np.pi / (group['new_sites'].sum() + 3) # calculate the angle step based on the total number of dots
current_dot = 0
for _, row in group.iterrows():
country = row['countries']
count = int(row['new_sites'])
color = color_dict[country]
dot_radius = (r_min + r_max) / 2
for i in range(count):
angle = current_dot * angle_step # Calculate the angle for this dot in the sequence
x = dot_radius * np.cos(angle)
y = dot_radius * np.sin(angle)
ax.plot(x, y, 'o', color=color, markersize=marker_size, markeredgecolor='white')
current_dot += 1
#add country labels
angle_per_country = 2*np.pi / len(countries)
for j, country in enumerate(countries):
# Calculate position
angle = j * angle_per_country
x = r * np.cos(angle)
y = r * np.sin(angle)
# Add text with rotation (90° offset to be perpendicular)
text = ax.text(x, y, country,
ha='center', va='center',
rotation=np.degrees(angle) - 90,
fontsize=10,
color = colors[j])
# Adjust text position slightly outward for better visibility
text.set_position((1.05*x, 1.05*y))
ax.axis('off')
(np.float64(-10.0), np.float64(10.0), np.float64(-10.0), np.float64(10.0))