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
from mpl_toolkits.mplot3d import Axes3D
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
import numpy as np
import pandas as pd
code_dict = {"Norway": "NO", "Denmark": "DK", "Sweden": "SE", }
color_dict = {"Norway": "#2B314D", "Denmark": "#A54836", "Sweden": "#5375D4" }
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['ctry_code'] = df.countries.map(code_dict)
df['color']=df.countries.map(color_dict)
df["pct_change"] = (
df.groupby("countries", sort=False)["sites"]
.apply(lambda x: x.pct_change())
.to_numpy()
.round(3)
* 100
)
df = df.sort_values(['sites' ], ascending=False ).reset_index(drop=True)
df
| year | countries | sites | ctry_code | color | pct_change | |
|---|---|---|---|---|---|---|
| 0 | 2022 | Sweden | 15 | SE | #5375D4 | 15.4 |
| 1 | 2004 | Sweden | 13 | SE | #5375D4 | NaN |
| 2 | 2022 | Denmark | 10 | DK | #A54836 | 150.0 |
| 3 | 2022 | Norway | 8 | NO | #2B314D | 60.0 |
| 4 | 2004 | Norway | 5 | NO | #2B314D | NaN |
| 5 | 2004 | Denmark | 4 | DK | #A54836 | NaN |
Create the 3D bars¶
fig = plt.figure(figsize=(6,6))
ax = fig.add_subplot(1, 1, 1, projection="3d",computed_zorder=False)
ax.set(xlim = [1,8], ylim = [1,6], zlim = [0,16])
y = [2,4]*3
x = [2,2,4,4,6,6]
z = [0]*6
width = [1]*6 #width
depth = [1]*6 #depth
height=[] #height
for country in df.countries.unique():
sites = df[df.countries == country].sort_values("year", ascending =True)["sites"].values
height.extend(sites.tolist())
ax.bar3d(x, y, z, width, depth, height, color=(0, 0, 1, 0), ec= "lightgrey",lw=0.5, zorder=0) #ec= "#F0F2F3"
<mpl_toolkits.mplot3d.art3d.Poly3DCollection at 0x22500602120>
Define the vertices of each bar to create its surfaces¶
def get_bar_vertices(x, y, z, width, depth, height):
"""
Get the vertices of each bar
"""
blocks = []
for i in range(len(x)):
x_left = x[i]
x_right = x[i] + width[i]
y_front = y[i]
y_back = y[i] + depth[i]
z_bottom = z[i]
z_top = z[i] + height[i]
blocks.append([x_left, x_right, y_front, y_back, z_bottom, z_top])
return blocks
Find the coordinates of the different surfaces¶
blocks = get_bar_vertices(x, y, z, width, depth, height)
print(blocks)
[[2, 3, 2, 3, 0, 13], [2, 3, 4, 5, 0, 15], [4, 5, 2, 3, 0, 4], [4, 5, 4, 5, 0, 10], [6, 7, 2, 3, 0, 5], [6, 7, 4, 5, 0, 8]]
pct_changes = df['pct_change'].dropna().unique()
pct_changes
array([ 15.4, 150. , 60. ])
colors = df['color'].unique()
pct_changes = df['pct_change'].dropna().unique()
for i in range(0, len(blocks), 2):
current_color = colors[(i // 2) % len(colors)]
x_left, x_right, y_front, y_back, z_bottom, z_top = blocks[i] #front block
X_Left, X_Right, Y_Front, Y_Back, Z_Bottom, Z_Top = blocks[i + 1] #black block
block = np.array([
#first surface
[[x_left, y_front, z_top],
[x_right, y_front, z_top],
[x_right, y_back, z_top],
[x_left, y_back, z_top]],
#middle surface
[[x_left, y_back, z_top],
[x_right, y_back, z_top],
[X_Right, Y_Front, Z_Top],
[X_Left, Y_Front, Z_Top]],
#last surface
[[X_Left, Y_Front, Z_Top],
[X_Right, Y_Front, Z_Top],
[X_Right, Y_Back, Z_Top],
[X_Left, Y_Back, Z_Top]]
])
pc = Poly3DCollection(block, facecolors=current_color, shade=True, alpha=1 )
ax.add_collection3d(pc)
#Add text middle surface
xc = (x_left + x_right + X_Left + X_Right) / 4
yc = (y_back + Y_Front) / 2
zc = (z_top + Z_Top) / 2
pct = pct_changes[i // 2]
ax.text(
xc, yc, zc,
f"+{int(pct)}%",
ha="center", va="center", color="white", size=8, zorder=3, weight = "bold"
)
ax.xaxis._axinfo["grid"].update({"linewidth":0, "color" : "w"}) #color gridline
ax.xaxis.set_ticks(np.arange(2, 8, 2), labels = ["SE","DK","NO"])
ax.set_yticks([])
ax.set_zticks([])
label_colors = [ "#5375D4", "#A54836", "#2B314D"]
for xtick, color in zip(ax.get_xticklabels(), label_colors):
xtick.set_color(color)
for axis in [ax.xaxis, ax.yaxis, ax.zaxis]:
axis._axinfo['tick']['inward_factor'] = 0
axis._axinfo['tick']['outward_factor'] = 0 #remove ticks
axis.set_pane_color("w")
# Transparent spines
ax.xaxis.line.set_color("w")
ax.yaxis.line.set_color("w")
ax.zaxis.line.set_color("w")
fig
Add remaining text¶
for i, block in enumerate(blocks):
x1, x2, y1, y2, z1, z2 = block
#calculate the center of the surfaces
xc = (x1 + x2) / 2
yc = (y1 + y2) / 2
print(i, xc,yc,z2, xc + 0.2, yc + 0.2, z2)
ax.text(
xc, yc, z2,
f"{z2}",
ha="center", va="center", color="white", zorder=3
)
fig
0 2.5 2.5 13 2.7 2.7 13 1 2.5 4.5 15 2.7 4.7 15 2 4.5 2.5 4 4.7 2.7 4 3 4.5 4.5 10 4.7 4.7 10 4 6.5 2.5 5 6.7 2.7 5 5 6.5 4.5 8 6.7 4.7 8