Skip to content

Commit e49d777

Browse files
feat(altair): implement bar-3d (#3148)
## Implementation: `bar-3d` - altair Implements the **altair** version of `bar-3d`. **File:** `plots/bar-3d/implementations/altair.py` **Parent Issue:** #2857 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20627538767)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 2d2aa07 commit e49d777

File tree

2 files changed

+237
-0
lines changed

2 files changed

+237
-0
lines changed
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
""" pyplots.ai
2+
bar-3d: 3D Bar Chart
3+
Library: altair 6.0.0 | Python 3.13.11
4+
Quality: 91/100 | Created: 2025-12-31
5+
"""
6+
7+
import altair as alt
8+
import numpy as np
9+
import pandas as pd
10+
11+
12+
# Data - Sales by product and quarter (grid of categorical dimensions)
13+
np.random.seed(42)
14+
15+
products = ["Product A", "Product B", "Product C", "Product D"]
16+
quarters = ["Q1", "Q2", "Q3", "Q4"]
17+
18+
# Generate realistic sales data with variation (in thousands $)
19+
sales_data = []
20+
base_sales = [85, 145, 70, 115] # Different base performance per product
21+
22+
for i, product in enumerate(products):
23+
for j, quarter in enumerate(quarters):
24+
seasonal = 1.0 + 0.2 * np.sin((j + 1) * np.pi / 2) # Seasonal variation
25+
trend = 1.0 + j * 0.08 # Growth trend
26+
noise = np.random.uniform(0.85, 1.15)
27+
sales = base_sales[i] * seasonal * trend * noise
28+
sales_data.append({"product": product, "quarter": quarter, "x_idx": i, "y_idx": j, "sales": sales})
29+
30+
df = pd.DataFrame(sales_data)
31+
32+
# 3D isometric projection parameters - adjusted for better alignment
33+
bar_width = 0.50
34+
iso_x_scale = 0.60 # Isometric x-shift per depth unit
35+
iso_y_scale = 0.30 # Isometric y-shift per depth unit
36+
spacing_x = 1.4 # Spacing between products
37+
38+
# Scale sales directly for visual height (using actual sales values)
39+
max_sales = df["sales"].max()
40+
min_sales = df["sales"].min()
41+
sales_range = max_sales - min_sales
42+
43+
# Create 3D bar faces (front face + top face + side face for each bar)
44+
bar_faces = []
45+
46+
for _, row in df.iterrows():
47+
# Calculate isometric position
48+
depth_idx = row["y_idx"] # Quarter determines depth
49+
x_base = row["x_idx"] * spacing_x
50+
x_shift = depth_idx * iso_x_scale # Shift right for depth
51+
y_shift = depth_idx * iso_y_scale # Shift up for depth
52+
53+
x_center = x_base + x_shift
54+
y_base = y_shift
55+
56+
# Bar height scaled from actual sales (preserving actual value relationship)
57+
normalized_sales = (row["sales"] - min_sales) / sales_range
58+
height = normalized_sales * 3.2 + 0.6 # Scale for visualization
59+
60+
# Depth for painter's algorithm (back rows first, left to right within row)
61+
base_depth = (3 - row["y_idx"]) * 100 + row["x_idx"]
62+
63+
# Front face (main bar) - full opacity, brightest
64+
bar_faces.append(
65+
{
66+
"x1": x_center - bar_width / 2,
67+
"x2": x_center + bar_width / 2,
68+
"y1": y_base,
69+
"y2": y_base + height,
70+
"sales": row["sales"],
71+
"product": row["product"],
72+
"quarter": row["quarter"],
73+
"depth": base_depth + 2,
74+
"face": "front",
75+
"brightness": 1.0,
76+
}
77+
)
78+
79+
# Top face (parallelogram) - aligned precisely with front face top edge
80+
top_depth = iso_x_scale * 0.8 # Depth extent of top face
81+
bar_faces.append(
82+
{
83+
"x1": x_center - bar_width / 2,
84+
"x2": x_center + bar_width / 2 + top_depth,
85+
"y1": y_base + height,
86+
"y2": y_base + height + iso_y_scale * 0.8,
87+
"sales": row["sales"],
88+
"product": row["product"],
89+
"quarter": row["quarter"],
90+
"depth": base_depth + 1,
91+
"face": "top",
92+
"brightness": 0.80,
93+
}
94+
)
95+
96+
# Right side face - aligned with front face right edge
97+
bar_faces.append(
98+
{
99+
"x1": x_center + bar_width / 2,
100+
"x2": x_center + bar_width / 2 + top_depth,
101+
"y1": y_base + iso_y_scale * 0.8,
102+
"y2": y_base + height + iso_y_scale * 0.8,
103+
"sales": row["sales"],
104+
"product": row["product"],
105+
"quarter": row["quarter"],
106+
"depth": base_depth,
107+
"face": "side",
108+
"brightness": 0.60,
109+
}
110+
)
111+
112+
df_faces = pd.DataFrame(bar_faces)
113+
114+
# Sort by depth (back to front for proper occlusion)
115+
df_faces = df_faces.sort_values("depth", ascending=True).reset_index(drop=True)
116+
df_faces["order"] = range(len(df_faces))
117+
118+
# Create color scale with brightness for shading effect
119+
bars = (
120+
alt.Chart(df_faces)
121+
.mark_rect(stroke="#2a2a2a", strokeWidth=1.2)
122+
.encode(
123+
x=alt.X("x1:Q", scale=alt.Scale(domain=[-0.5, 7.0]), axis=alt.Axis(title=None, labels=False, ticks=False)),
124+
x2=alt.X2("x2:Q"),
125+
y=alt.Y(
126+
"y1:Q",
127+
scale=alt.Scale(domain=[-0.8, 5.5]),
128+
axis=alt.Axis(
129+
title="Sales Revenue ($K)",
130+
labelFontSize=16,
131+
titleFontSize=20,
132+
values=[0, 1, 2, 3, 4, 5],
133+
labelExpr="datum.value * 40 + 60", # Map visual height to approximate sales values
134+
),
135+
),
136+
y2=alt.Y2("y2:Q"),
137+
color=alt.Color(
138+
"sales:Q",
139+
scale=alt.Scale(scheme="viridis", domain=[min_sales, max_sales]),
140+
legend=alt.Legend(
141+
title="Sales ($K)", titleFontSize=18, labelFontSize=14, orient="right", offset=15, format=".0f"
142+
),
143+
),
144+
opacity=alt.Opacity("brightness:Q", scale=alt.Scale(domain=[0.5, 1.0], range=[0.70, 1.0]), legend=None),
145+
order=alt.Order("order:Q"),
146+
tooltip=[
147+
alt.Tooltip("product:N", title="Product"),
148+
alt.Tooltip("quarter:N", title="Quarter"),
149+
alt.Tooltip("sales:Q", title="Sales ($K)", format=".1f"),
150+
],
151+
)
152+
)
153+
154+
# Product labels at bottom - positioned below the front row
155+
product_positions = [i * spacing_x + 0.15 for i in range(len(products))]
156+
product_labels_df = pd.DataFrame({"x": product_positions, "y": [-0.5] * len(products), "label": products})
157+
158+
product_text = (
159+
alt.Chart(product_labels_df)
160+
.mark_text(fontSize=18, fontWeight="bold", color="#333333")
161+
.encode(x="x:Q", y="y:Q", text="label:N")
162+
)
163+
164+
# Quarter labels integrated into the 3D perspective - along the depth axis
165+
# Position each quarter label at the back of each row, aligned with isometric projection
166+
quarter_labels_df = pd.DataFrame(
167+
{
168+
"x": [-0.5 + j * iso_x_scale for j in range(len(quarters))],
169+
"y": [j * iso_y_scale + 0.1 for j in range(len(quarters))],
170+
"label": quarters,
171+
}
172+
)
173+
174+
quarter_text = (
175+
alt.Chart(quarter_labels_df)
176+
.mark_text(fontSize=16, fontWeight="bold", color="#444444", align="right")
177+
.encode(x="x:Q", y="y:Q", text="label:N")
178+
)
179+
180+
# Depth axis indicator - angled to match isometric direction
181+
depth_arrow = pd.DataFrame({"x": [-0.3], "y": [1.5], "label": ["← Quarters"]})
182+
183+
depth_text = (
184+
alt.Chart(depth_arrow)
185+
.mark_text(fontSize=14, fontStyle="italic", color="#666666", angle=334, align="right")
186+
.encode(x="x:Q", y="y:Q", text="label:N")
187+
)
188+
189+
# Combine all layers with interactive pan/zoom
190+
chart = (
191+
alt.layer(bars, product_text, quarter_text, depth_text)
192+
.properties(
193+
width=1600,
194+
height=900,
195+
title=alt.Title(
196+
text="bar-3d · altair · pyplots.ai",
197+
subtitle="Quarterly Sales by Product (Isometric 3D Projection)",
198+
fontSize=28,
199+
subtitleFontSize=18,
200+
subtitleColor="#666666",
201+
),
202+
)
203+
.configure_axis(grid=True, gridOpacity=0.2, gridDash=[4, 4])
204+
.configure_view(strokeWidth=0)
205+
.interactive()
206+
)
207+
208+
# Save outputs
209+
chart.save("plot.png", scale_factor=3.0)
210+
chart.save("plot.html")

plots/bar-3d/metadata/altair.yaml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
library: altair
2+
specification_id: bar-3d
3+
created: '2025-12-31T21:36:39Z'
4+
updated: '2025-12-31T21:45:13Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 20627538767
7+
issue: 2857
8+
python_version: 3.13.11
9+
library_version: 6.0.0
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/bar-3d/altair/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/bar-3d/altair/plot_thumb.png
12+
preview_html: https://storage.googleapis.com/pyplots-images/plots/bar-3d/altair/plot.html
13+
quality_score: 91
14+
review:
15+
strengths:
16+
- Excellent creative solution using isometric projection to simulate 3D in a 2D
17+
library
18+
- Good painter's algorithm implementation for proper depth ordering of bar faces
19+
- Viridis colormap provides clear value encoding and is colorblind-accessible
20+
- Interactive tooltips show exact product, quarter, and sales values
21+
- Realistic business data with seasonal variation and growth trends
22+
- Clean code structure following KISS principles
23+
weaknesses:
24+
- Y-axis label says Sales Revenue (Relative Height) but axis tick values do not
25+
correspond to actual sales values
26+
- Quarter labels positioning could be clearer - they overlap with depth axis indicator
27+
- Spec suggests semi-transparent bars for revealing occluded bars which is not implemented

0 commit comments

Comments
 (0)