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.art3d import Poly3DCollection
import numpy as np
import pandas as pd
color_dict = {"Norway": "#2B314D", "Denmark": "#A54836", "Sweden": "#5375D4" }
code_dict = {"Norway": "NO", "Denmark": "DK", "Sweden": "SE"}
xy_ticklabel_color, line_colors, datalabels_color ='#757C85', "#757C85", "#757C85"
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['ctry_code'] = df.countries.map(code_dict)
df['year_lbl'] ="'"+df['year'].astype(str).str[-2:].astype(str)
# custom sort
sort_order_dict = {"Denmark": 3, "Sweden": 1, "Norway": 2, 2004: 4, 2022: 5}
df = df.sort_values(by=[ "countries", "year"], key=lambda x: x.map(sort_order_dict))
#map the colors of a dict to a dataframe
df['color']= df.countries.map(color_dict)
df
| year | countries | sites | ctry_code | year_lbl | color | |
|---|---|---|---|---|---|---|
| 4 | 2004 | Sweden | 13 | SE | '04 | #5375D4 |
| 5 | 2022 | Sweden | 15 | SE | '22 | #5375D4 |
| 2 | 2004 | Norway | 5 | NO | '04 | #2B314D |
| 3 | 2022 | Norway | 8 | NO | '22 | #2B314D |
| 0 | 2004 | Denmark | 4 | DK | '04 | #A54836 |
| 1 | 2022 | Denmark | 10 | DK | '22 | #A54836 |
Function to create 3d bars with incline top faces¶
def create_inclined_bar(x_start, y_start, width, depth, height_front, height_back, color):
"""
Adds a inclined-top 3D bar to the given axes.
The top is inclined along the y-axis.
"""
# Define the 8 vertices
vertices = [
# Bottom face
[x_start, y_start, 0],
[x_start + width, y_start, 0],
[x_start + width, y_start + depth, 0],
[x_start, y_start + depth, 0],
# Top face (inclined)
[x_start, y_start, height_front],
[x_start + width, y_start, height_front],
[x_start + width, y_start + depth, height_back],
[x_start, y_start + depth, height_back]
]
# Define faces
faces = [
[vertices[0], vertices[1], vertices[2], vertices[3]], # Bottom
[vertices[4], vertices[5], vertices[6], vertices[7]], # Top
[vertices[0], vertices[1], vertices[5], vertices[4]], # Front
[vertices[1], vertices[2], vertices[6], vertices[5]], # Right
[vertices[2], vertices[3], vertices[7], vertices[6]], # Back
[vertices[3], vertices[0], vertices[4], vertices[7]] # Left
]
poly = Poly3DCollection(faces, facecolors=color, shade = True)
ax.add_collection3d(poly)
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
fig, ax = plt.subplots(figsize=(5,8), subplot_kw={'projection':'3d'})
# Common dimensions
depth = 0.6
bar_width = 0.2
y_start = 0
ax.set(xlim = [0,1.1],ylim=[0, 1], zlim=[0, df.sites.max()])
# Variable dimensions
x_positions = np.linspace(0, .8, len(df.countries.unique()))
grouped_sites = df.groupby("color", sort=False)['sites'].apply(list)
for (color, site_pair), x_start in zip(grouped_sites.items(), x_positions):
height_front, height_back = site_pair
create_inclined_bar(
x_start=x_start,
y_start=y_start,
width=bar_width,
depth=depth,
height_front=height_front,
height_back=height_back,
color=color
)
ax.view_init(
elev=30, # looking from 30 degrees above the x-y plane (higher/ lower above the plot)
azim=-50, # rotates camera around z-axis (left/ right around the plot)
roll=0) # tilts the view
Add data labels¶
# midpoint of each block in the x coordinates
x_midpoint = x_positions + bar_width / 2
xs = np.repeat(x_midpoint, 2)
# y coordinates with offset
offset = .1
ys = [y_start - offset, y_start + depth + offset] * (len(xs) // 2)
z_positions = df['sites'].tolist()
# alternate sites values for 2022 with zeros
zs = [0 if i % 2 == 0 else z for i, z in enumerate(z_positions)]
# texts: same as zs
texts = z_positions
# Print results
print("xs =", xs)
print("ys =", ys)
print("zs =", zs)
print("texts =", texts)
#add data labels
for text, x, y, z in zip( texts, xs, ys, zs):
ax.text(
x,
y,
z,
text,
va="center",
color = datalabels_color,
ha="right",
clip_on=True
)
fig
xs = [0.1 0.1 0.5 0.5 0.9 0.9] ys = [-0.1, 0.7, -0.1, 0.7, -0.1, 0.7] zs = [0, 15, 0, 8, 0, 10] texts = [13, 15, 5, 8, 4, 10]
Add horizontal lines¶
# Define axis line start and end points
axis_lines = {
'start': {'x': [0, 1.1], 'y': [depth, depth], 'z': [0, 0]},
'end': {'x': [0, 1.1], 'y': [0, 0], 'z': [0, 0]},
}
# Plot axis lines
for axis, coords in axis_lines.items():
ax.plot(
coords['x'], # x-coordinates
coords['y'], # y-coordinates
zs = coords['z'], # z-coordinates
color = line_colors,
lw = 1
)
fig
Add country and year labels¶
#add country labels
ax.xaxis.set_ticks(x_midpoint -.1, labels = df.ctry_code.unique())
ax.tick_params(
axis='x',
which='major',
length=0,
labelsize=11,
pad =10
)
for xtick, color in zip(ax.get_xticklabels(), df.color.unique()):
xtick.set_color(color)
#add year labels
ax.yaxis.set_ticks(
[0 + offset /2, depth + offset /2],
labels = df.year_lbl.unique(),
color = xy_ticklabel_color
)
ax.zaxis.set_ticks([])
fig
Add styling¶
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") # Make panes transparent
axis.line.set_linewidth(0)
ax.grid(False)
fig