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.
#!pip install mpltern
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import mpltern
color_dict = {
(2022, "Norway"): "#464B64",
(2004, "Norway"): "#2B314D",
(2022, "Denmark"): "#D57968",
(2004, "Denmark"): "#A54836",
(2022, "Sweden"): "#7296E9",
(2004, "Sweden"): "#5375D4",
}
code_dict = {"Norway": "NO", "Denmark": "DK", "Sweden": "SE"}
xy_ticklabel_color, labels_color, grid_color, datalabels_color ='#757C85',"#101628", "#C8C9C9", "#FFFFFF"
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)
sort_order_dict = {"Denmark": 1, "Sweden": 2, "Norway": 3, 2004: 5, 2022: 4}
df = df.sort_values( by=["year", "countries"], key=lambda x: x.map(sort_order_dict))
# Add the color based on the color dictionary
df["color"] = df.set_index(["year", "countries"]).index.map(color_dict.get)
df['ctry_code'] = df.countries.map(code_dict)
df['year_lbl'] ="'"+df['year'].astype(str).str[-2:].astype(str)
df['sites_scaled'] = df.sites * 6000 #pick a scale that mathes the original image
df
| year | countries | sites | color | ctry_code | year_lbl | sites_scaled | |
|---|---|---|---|---|---|---|---|
| 1 | 2022 | Denmark | 10 | #D57968 | DK | '22 | 60000 |
| 5 | 2022 | Sweden | 15 | #7296E9 | SE | '22 | 90000 |
| 3 | 2022 | Norway | 8 | #464B64 | NO | '22 | 48000 |
| 0 | 2004 | Denmark | 4 | #A54836 | DK | '04 | 24000 |
| 4 | 2004 | Sweden | 13 | #5375D4 | SE | '04 | 78000 |
| 2 | 2004 | Norway | 5 | #2B314D | NO | '04 | 30000 |
First we create the coordinates for the terciary plot and then we scale the sites value so they display according to area in the plot, but before that, lets look at how a ternary plot works.
If we want to plot a dot on the axes, we do it by where it lands in percentage of all three corners. The sum has to be 1, as we are specifying ternary_sum of 1.
Coordinates of (0.4, 0.35, 0.35) will be placed as follows:
- A will be 40% of the area between the red line and B-C side
- B will be 35% of the area between the red line and A-C side
- C will be 35% of the area between the red line and A-B side
So the coordinates for our dots will be:
#coordinates of the dots
dk = np.array([1,0,0])
nw = np.array([0,1,0])
sw = np.array([0,0,1])
labels = {'t', 'l', 'r'}
Plot the chart¶
fig = plt.figure()
ax = fig.add_subplot(projection="ternary",ternary_sum=1.0)
ax.tick_params(labelrotation="horizontal")
for year, group in df.groupby("year", sort = False):
sizes= group.sites_scaled.tolist()
ax.scatter(
dk, nw, sw,
s= group.sites_scaled.tolist(),
c=group.color,
)
# Add data labels
for d, n, s, size, site in zip(dk, nw, sw, sizes, group.sites):
print(d, n, s)
if year == 2004:
offset = size/120000
else:
offset = size/95000
ax.text(d + offset, n + offset, s + offset, f"{site}",
fontsize=9, color= 'w', ha='center', va='center')
1 0 0 0 1 0 0 0 1 1 0 0 0 1 0 0 0 1
add the year labels:
# Specify tick positions manually.
tickst = [0.25, 0.35]
ticksl = [0.2, 0.4]
ticksr = [0.4, 0.5]
labels = df.year_lbl.unique()[::-1] #reverse order
color = df.color.unique()
ax.taxis.set_ticks(tickst, labels=labels, color = color[2])
ax.laxis.set_ticks(ticksl, labels=labels, color = color[0])
ax.raxis.set_ticks(ticksr, labels=labels, color = color[1])
#hide ticks
for axis in (ax.taxis, ax.laxis, ax.raxis):
axis.set_tick_params(size=0, )
fig
Style everything and add axis labels:
#change the grid color
for side in ['tside', 'lside', 'rside']:
ax.spines[side].set_color(grid_color)
# Axis labels
label_params = {'weight': 'light', 'size': 16}
ax.set_tlabel('DK', **label_params)
ax.set_llabel('SE', **label_params)
ax.set_rlabel('NO', **label_params)
#rotate the axis
for axis in [ax.laxis, ax.raxis]:
axis.set_label_rotation_mode("horizontal")
fig