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 matplotlib.patches as patches
import pandas as pd
import numpy as np
color_dict = {
(2022, "Norway"): "#9194A3",
(2004, "Norway"): "#2B314D",
(2022, "Denmark"): "#E2AFA5",
(2004, "Denmark"): "#A54836",
(2022, "Sweden"): "#C4D6F8",
(2004, "Sweden"): "#5375D4",
}
xy_label_color, legend_color, datalabels_color, grid_color = "#101628", "#757C85", "#FFFFFF", "#C8C9C9"
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=False).reset_index(drop=True)
# Add the color based on the color dictionary
df["color"] = df.set_index(["year", "countries"]).index.map(color_dict.get)
# To ensure that the areas are really proportional, use the square root values as the radius of the circles.
df["radius"] = (df["sites"]) ** 0.5
#add the position of the datalabels
df["label_position"] = np.where(df['year'] == df['year'].max(), df['radius'] * 2, df['radius'])
df
| year | countries | sites | color | radius | label_position | |
|---|---|---|---|---|---|---|
| 0 | 2022 | Denmark | 10 | #E2AFA5 | 3.162278 | 6.324555 |
| 1 | 2022 | Norway | 8 | #9194A3 | 2.828427 | 5.656854 |
| 2 | 2022 | Sweden | 15 | #C4D6F8 | 3.872983 | 7.745967 |
| 3 | 2004 | Denmark | 4 | #A54836 | 2.000000 | 2.000000 |
| 4 | 2004 | Norway | 5 | #2B314D | 2.236068 | 2.236068 |
| 5 | 2004 | Sweden | 13 | #5375D4 | 3.605551 | 3.605551 |
Define the variables:
groups = df.groupby("countries")
Plot the chart¶
This is very similar to viz number 11 with squares.
We will use circular patch method and we need the following parameters:
| Parameter | Description | Value |
|---|---|---|
| xy | The xy positions of the center of the circle | |
| radius | The radius of the cirle |
We also need to add ax.set_aspect("equal") to scale x and y equally.
fig, axes = plt.subplots( ncols=1, nrows=df.countries.nunique(), figsize=(5, 5), sharex=True, sharey=True, facecolor="white",)
fig.tight_layout(pad=1.0)
for i, ((country, group), ax) in enumerate(zip(groups, axes.ravel())):
for row in group.itertuples(index=False):
radie = row.radius
circle = patches.Circle(
(0, radie),
radius = radie,
color = row.color,
)
ax.add_patch(circle)
ax.set_xlim(-5, 5)
ax.set_ylim(0, 10)
ax.set_aspect("equal")
#add data label
offset = .5
ax.text(
0,
row.label_position - offset,
row.sites,
fontsize = 9,
color = datalabels_color,
ha = "center",
va = "center"
)
Create the legend¶
We will use the same method as for viz number 11.
#data for the legend
legend_ax = fig.add_axes([.9, -0.1, 0.2, 0.2]) # left, bottom, width, height in figure coordinates
legend_ax.axis("off")
circle_data = [
{"xy": (0.5, 0.3), "radius": 0.2, "color": "w", "ec": grid_color},
{"xy": (0.5, 0.2), "radius": 0.08, "color": "w", "ec": grid_color},
]
# Text data (position, text, alignment, color)
text_data = [
{"x": -0.35, "y": 0.5, "s": df.year.max(), "va": "center", "color": grid_color},
{"x": -0.35, "y": 0.2, "s": df.year.min(), "va": "center", "color": grid_color},
]
for circle in circle_data:
legend_ax.add_patch(patches.Circle(**circle))
for txt in text_data:
legend_ax.text(**txt)
fig
The complete code¶
We only have left now to style, add the country labels and the legend.
Lets put everything together!
import matplotlib.pyplot as plt
import matplotlib.patches as patches
import pandas as pd
import numpy as np
color_dict = {
(2022, "Norway"): "#9194A3",
(2004, "Norway"): "#2B314D",
(2022, "Denmark"): "#E2AFA5",
(2004, "Denmark"): "#A54836",
(2022, "Sweden"): "#C4D6F8",
(2004, "Sweden"): "#5375D4",
}
xy_label_color, legend_color, datalabels_color, grid_color = "#101628", "#757C85", "#FFFFFF", "#C8C9C9"
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=False).reset_index(drop=True)
# Add the color based on the color dictionary
df["color"] = df.set_index(["year", "countries"]).index.map(color_dict.get)
# To ensure that the areas are really proportional, use the square root values as the radius of the circles.
df["radius"] = (df["sites"]) ** 0.5
#add the position of the datalabels
df["label_position"] = np.where(df['year'] == df['year'].max(), df['radius'] * 2, df['radius'])
groups = df.groupby("countries")
fig, axes = plt.subplots( ncols=1, nrows=df.countries.nunique(), figsize=(5, 5), sharex=True, sharey=True, facecolor="white",)
fig.tight_layout(pad=1.0)
for i, ((country, group), ax) in enumerate(zip(groups, axes.ravel())):
for row in group.itertuples(index=False):
radie = row.radius
circle = patches.Circle(
(0, radie),
radius = radie,
color = row.color,
)
ax.add_patch(circle)
ax.set_xlim(-5, 5)
ax.set_ylim(0, 10)
ax.set_aspect("equal")
#add data label
offset = .5
ax.text(
0,
row.label_position - offset,
row.sites,
fontsize = 9,
color = datalabels_color,
ha = "center",
va = "center"
)
#add the country labels
ax.set_xlabel(country, color=xy_label_color, size=10)
#add styling
ax.set_frame_on(False)
ax.tick_params(axis="both", which="both", length=0, labelbottom = False, labelleft = False)
#add the legend
legend_ax = fig.add_axes([.9, -0.1, 0.2, 0.2]) # left, bottom, width, height in figure coordinates
legend_ax.axis("off")
circle_data = [
{"xy": (0.5, 0.3), "radius": 0.2, "color": "w", "ec": grid_color},
{"xy": (0.5, 0.2), "radius": 0.08, "color": "w", "ec": grid_color},
]
# Text data (position, text, alignment, color)
text_data = [
{"x": -0.35, "y": 0.5, "s": df.year.max(), "va": "center", "color": grid_color},
{"x": -0.35, "y": 0.2, "s": df.year.min(), "va": "center", "color": grid_color},
]
for circle in circle_data:
legend_ax.add_patch(patches.Circle(**circle))
for txt in text_data:
legend_ax.text(**txt)