diff --git a/plots/bar-3d-categorical/implementations/python/letsplot.py b/plots/bar-3d-categorical/implementations/python/letsplot.py new file mode 100644 index 0000000000..656e2593d1 --- /dev/null +++ b/plots/bar-3d-categorical/implementations/python/letsplot.py @@ -0,0 +1,203 @@ +""" anyplot.ai +bar-3d-categorical: 3D Bar Chart for Categorical Comparison +Library: letsplot 4.9.0 | Python 3.13.13 +Quality: 83/100 | Created: 2026-05-15 +""" + +import os + +import numpy as np +import pandas as pd +from lets_plot import * + + +LetsPlot.setup_html() + +THEME = os.getenv("ANYPLOT_THEME", "light") +PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17" +ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420" +INK = "#1A1A17" if THEME == "light" else "#F0EFE8" +INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0" +GRID_COLOR = "#C8C7C0" if THEME == "light" else "#3A3A35" + +OKABE_ITO = ["#009E73", "#D55E00", "#0072B2"] + + +def shade(col, f): + """Adjust hex color brightness by factor f.""" + r = min(255, max(0, round(int(col[1:3], 16) * f))) + g = min(255, max(0, round(int(col[3:5], 16) * f))) + b = min(255, max(0, round(int(col[5:7], 16) * f))) + return f"#{r:02X}{g:02X}{b:02X}" + + +np.random.seed(42) +X_CATS = ["18–34", "35–49", "50–64", "65+"] +Y_CATS = ["High School", "Bachelor's", "Graduate"] # index 0 = furthest back + +BASE = {"High School": [6.1, 5.7, 5.4, 5.8], "Bachelor's": [7.0, 7.3, 6.8, 7.1], "Graduate": [7.7, 8.0, 7.5, 7.8]} + +NX = len(X_CATS) +NY = len(Y_CATS) + +vals = { + (ai, ei): round(BASE[edu][ai] + np.random.uniform(-0.15, 0.15), 1) + for ei, edu in enumerate(Y_CATS) + for ai in range(NX) +} + +# Cabinet projection: x3=age-group axis, y3=depth axis, z3=height +BW = 0.78 # bar width (x3 units; gap = 1 - BW) +BD = 0.65 # bar depth (y3 units) +DX = 0.52 # x2 shift per unit of y3 depth +DZ = 0.32 # z2 (upward) shift per unit of y3 depth + + +def prj(x3, y3, z3): + return (x3 + y3 * DX, z3 + y3 * DZ) + + +# ── Base-plane grid ────────────────────────────────────────────────── +seg_rows = [] + + +def seg(x1, y1, x2, y2): + seg_rows.append({"x": x1, "y": y1, "xend": x2, "yend": y2}) + + +# Lines along the depth direction (x edges of the grid) +for xi in range(NX + 1): + p0 = prj(xi, 0, 0) + p1 = prj(xi, NY - 1 + BD, 0) + seg(p0[0], p0[1], p1[0], p1[1]) + +# Lines along the x direction (depth edges of the grid) +for yi_f in [0.0, 1.0, 2.0, NY - 1 + BD]: + p0 = prj(0, yi_f, 0) + p1 = prj(NX, yi_f, 0) + seg(p0[0], p0[1], p1[0], p1[1]) + +df_seg = pd.DataFrame(seg_rows) + +# ── Z-axis (left-front vertical) ──────────────────────────────────── +z_segs = [] +ztick_rows = [] +Z_TICKS = [0, 2, 4, 6, 8, 10] + +ax0 = prj(0, 0, 0) +ax1 = prj(0, 0, 10) +z_segs.append({"x": ax0[0], "y": ax0[1], "xend": ax1[0], "yend": ax1[1]}) + +for zt in Z_TICKS: + pt = prj(0, 0, zt) + z_segs.append({"x": pt[0], "y": pt[1], "xend": pt[0] - 0.18, "yend": pt[1]}) + ztick_rows.append({"x": pt[0] - 0.25, "y": pt[1], "label": str(zt)}) + +df_zsegs = pd.DataFrame(z_segs) +df_zticks = pd.DataFrame(ztick_rows) + +# ── Category axis labels ───────────────────────────────────────────── +x_lbl = [] +for ai, age in enumerate(X_CATS): + pt = prj(ai + BW / 2, 0, 0) + x_lbl.append({"x": pt[0], "y": pt[1] - 0.55, "label": age}) +df_xlbl = pd.DataFrame(x_lbl) + +# ── Bar polygons (painter's order: back → front) ───────────────────── +poly_rows = [] +gc = [0] + + +def face(pts, fill_col): + gid = str(gc[0]) + gc[0] += 1 + for px, py in pts: + poly_rows.append({"x": px, "y": py, "g": gid, "fill": fill_col}) + + +for ei in range(NY - 1, -1, -1): # draw back→front; ei=0 (High School) drawn last = in front + c0 = OKABE_ITO[ei] + c_front = c0 + c_right = shade(c0, 0.60) + c_top = shade(c0, 1.32) + + for ai in range(NX): + h = vals[(ai, ei)] + x0, x1 = ai, ai + BW + y0, y1 = ei, ei + BD + + C = { + k: prj(*v) + for k, v in { + "bfl": (x0, y0, 0), + "bfr": (x1, y0, 0), + "bbl": (x0, y1, 0), + "bbr": (x1, y1, 0), + "tfl": (x0, y0, h), + "tfr": (x1, y0, h), + "tbl": (x0, y1, h), + "tbr": (x1, y1, h), + }.items() + } + + face([C["bfl"], C["bfr"], C["tfr"], C["tfl"]], c_front) # front + face([C["bfr"], C["bbr"], C["tbr"], C["tfr"]], c_right) # right + face([C["tfl"], C["tfr"], C["tbr"], C["tbl"]], c_top) # top + +df_poly = pd.DataFrame(poly_rows) + +# ── Value labels (centre-top of each bar) ─────────────────────────── +val_rows = [] +for ei in range(NY): + for ai in range(NX): + h = vals[(ai, ei)] + pt = prj(ai + BW / 2, ei + BD / 2, h) + val_rows.append({"x": pt[0], "y": pt[1] + 0.28, "label": f"{h:.1f}"}) +df_vals = pd.DataFrame(val_rows) + +# ── Manual legend inside chart (upper-left area, above bars) ───────── +LEG_X = 0.15 +LEG_Y0 = 10.8 +LEG_DY = 0.65 +leg_title = [{"x": LEG_X, "y": LEG_Y0 + 0.4, "label": "Education Level"}] +leg_rect = [] +leg_text = [] +for i, edu in enumerate(Y_CATS): + lx, ly = LEG_X, LEG_Y0 - i * LEG_DY + leg_rect.append({"xmin": lx, "xmax": lx + 0.32, "ymin": ly - 0.20, "ymax": ly + 0.20, "fill": OKABE_ITO[i]}) + leg_text.append({"x": lx + 0.42, "y": ly, "label": edu}) + +df_ltitle = pd.DataFrame(leg_title) +df_lrect = pd.DataFrame(leg_rect) +df_ltxt = pd.DataFrame(leg_text) + +# unique fill → itself (scale_fill_manual with identity mapping) +fill_vals = {c: c for c in df_poly["fill"].unique()} + +# ── Assemble plot ───────────────────────────────────────────────────── +plot = ( + ggplot() + + geom_segment(aes(x="x", y="y", xend="xend", yend="yend"), data=df_seg, color=GRID_COLOR, size=0.8) + + geom_segment(aes(x="x", y="y", xend="xend", yend="yend"), data=df_zsegs, color=INK_SOFT, size=0.7) + + geom_polygon(aes(x="x", y="y", group="g", fill="fill"), data=df_poly, color=PAGE_BG, size=0.3) + + scale_fill_manual(values=fill_vals, guide="none") + + geom_text(aes(x="x", y="y", label="label"), data=df_zticks, size=11, color=INK_SOFT, hjust=1) + + geom_text(aes(x="x", y="y", label="label"), data=df_xlbl, size=12, color=INK_SOFT, vjust=1) + + geom_text(aes(x="x", y="y", label="label"), data=df_vals, size=10, color=INK) + + geom_text(aes(x="x", y="y", label="label"), data=df_ltitle, size=14, color=INK, hjust=0, fontface="bold") + + geom_rect( + aes(xmin="xmin", xmax="xmax", ymin="ymin", ymax="ymax", fill="fill"), data=df_lrect, color=INK_SOFT, size=0.3 + ) + + geom_text(aes(x="x", y="y", label="label"), data=df_ltxt, size=13, color=INK_SOFT, hjust=0) + + labs(title="bar-3d-categorical · letsplot · anyplot.ai") + + theme_void() + + theme( + plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG), + plot_title=element_text(color=INK, size=24, hjust=0.5), + plot_margin=[30, 40, 30, 40], + ) + + ggsize(1600, 900) +) + +ggsave(plot, f"plot-{THEME}.png", path=".", scale=3) +ggsave(plot, f"plot-{THEME}.html", path=".") diff --git a/plots/bar-3d-categorical/metadata/python/letsplot.yaml b/plots/bar-3d-categorical/metadata/python/letsplot.yaml new file mode 100644 index 0000000000..8e6b041446 --- /dev/null +++ b/plots/bar-3d-categorical/metadata/python/letsplot.yaml @@ -0,0 +1,255 @@ +library: letsplot +language: python +specification_id: bar-3d-categorical +created: '2026-05-15T08:46:38Z' +updated: '2026-05-15T09:27:01Z' +generated_by: claude-sonnet +workflow_run: 25908551462 +issue: 5248 +python_version: 3.13.13 +library_version: 4.9.0 +preview_url_light: https://storage.googleapis.com/anyplot-images/plots/bar-3d-categorical/python/letsplot/plot-light.png +preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/bar-3d-categorical/python/letsplot/plot-dark.png +preview_html_light: https://storage.googleapis.com/anyplot-images/plots/bar-3d-categorical/python/letsplot/plot-light.html +preview_html_dark: https://storage.googleapis.com/anyplot-images/plots/bar-3d-categorical/python/letsplot/plot-dark.html +quality_score: 83 +review: + strengths: + - Sophisticated cabinet projection with per-face shading (front/right/top) creates + convincing 3D depth in a 2D library, demonstrating real visualization engineering + - 'Perfect spec compliance 15/15: correct plot type, all required features, accurate + data mapping, correct title and legend format' + - 'Full data quality 15/15: realistic survey context (education x age x satisfaction), + appropriate 0-10 scale, all 12 bars present' + - 'Perfect code quality 10/10: reproducible seed, clean imports, organized sections, + correct PNG+HTML outputs' + - Theme-adaptive chrome correctly applied in both renders — INK/INK_SOFT tokens + thread through to all geom_text color parameters, no dark-on-dark failures + weaknesses: + - 'Design excellence (11/20) is the primary gap: functionally solid but lacks visual + polish — no legend background fill, tight right edge for 65+/Graduate bars' + - 'No descriptive axis titles: z-axis lacks ''Satisfaction Score'' label, x-axis + lacks ''Age Group'' label — viewers must infer from context' + - Value labels (size=10) and z-tick labels (size=11) are small for the 4800x2700 + canvas; slightly larger sizes would improve immediate legibility + - Rightmost bars (65+/Graduate) are very close to the right canvas edge — adjust + DX or canvas width for breathing room + image_description: |- + Light render (plot-light.png): + Background: Warm off-white #FAF8F1 — correct, not pure white + Chrome: Title "bar-3d-categorical · letsplot · anyplot.ai" in dark ink at top center — clearly readable. Z-axis tick labels (0,2,4,6,8,10) in INK_SOFT dark. Age group labels (18-34, 35-49, 50-64, 65+) in INK_SOFT below bars. Legend title "Education Level" in bold INK. All text readable against light background. + Data: Three Okabe-Ito bar groups — High School (#009E73 green, front), Bachelor's (#D55E00 orange, middle), Graduate (#0072B2 blue, back). Cabinet projection shading: front/right/top faces. 12 value labels (5.5-8.1) above bar tops. Base-plane grid lines in GRID_COLOR. + Legibility verdict: PASS + + Dark render (plot-dark.png): + Background: Warm near-black #1A1A17 — correct, not pure black + Chrome: Title and all labels rendered in light/near-white tones (#F0EFE8 for primary, #B8B7B0 for secondary). Title, z-tick labels, age labels, legend text all clearly readable against dark background. No dark-on-dark failures — INK token flips to #F0EFE8, INK_SOFT to #B8B7B0, correctly applied via geom_text color parameters. + Data: Bar colors identical to light render — green, orange, blue Okabe-Ito colors unchanged. Value labels switch to light #F0EFE8 and are fully readable on dark background. + Legibility verdict: PASS + criteria_checklist: + visual_quality: + score: 25 + max: 30 + items: + - id: VQ-01 + name: Text Legibility + score: 7 + max: 8 + passed: true + comment: All text readable in both themes; value labels (size=10) and z-tick + labels (size=11) are small but legible at 4800x2700 + - id: VQ-02 + name: No Overlap + score: 5 + max: 6 + passed: true + comment: Minor overlap between back-row value labels and foreground bars in + 3D perspective; labels still readable as topmost layer + - id: VQ-03 + name: Element Visibility + score: 5 + max: 6 + passed: true + comment: All 12 bars visible with distinct colors and shaded faces; 65+/Graduate + bar tight at right edge but contained + - id: VQ-04 + name: Color Accessibility + score: 2 + max: 2 + passed: true + comment: Okabe-Ito palette, CVD-safe, no red-green sole signal + - id: VQ-05 + name: Layout & Canvas + score: 3 + max: 4 + passed: true + comment: Good 16:9 layout; 65+/Graduate bar's right face is very close to + canvas edge with little margin + - id: VQ-06 + name: Axis Labels & Title + score: 1 + max: 2 + passed: false + comment: Title correct; z-axis ticks and x-axis age labels present, but no + descriptive axis titles (Satisfaction Score, Age Group) + - id: VQ-07 + name: Palette Compliance + score: 2 + max: 2 + passed: true + comment: 'First series #009E73 correct; Okabe-Ito order maintained; backgrounds + #FAF8F1/#1A1A17 correct; colors identical between themes' + design_excellence: + score: 11 + max: 20 + items: + - id: DE-01 + name: Aesthetic Sophistication + score: 5 + max: 8 + passed: true + comment: 'Above default: cabinet projection with per-face shading creates + convincing 3D depth; intentional color hierarchy; functional rather than + elegantly polished' + - id: DE-02 + name: Visual Refinement + score: 3 + max: 6 + passed: true + comment: 'Above default: theme_void() cleans canvas; bar edges use PAGE_BG; + manually drawn grid and z-axis are tidy; no legend background fill' + - id: DE-03 + name: Data Storytelling + score: 3 + max: 6 + passed: true + comment: 'Above default: clear hierarchy (Graduate > Bachelor > HS) immediately + visible; value labels enable precise comparison; no focal emphasis' + spec_compliance: + score: 15 + max: 15 + items: + - id: SC-01 + name: Plot Type + score: 5 + max: 5 + passed: true + comment: Correct 3D bar chart with two categorical axes and height-encoded + values via cabinet projection + - id: SC-02 + name: Required Features + score: 4 + max: 4 + passed: true + comment: 'All spec features: 2 categorical dims, height encoding, bar spacing, + color encoding, base-plane grid, value labels, viewing angle, legend' + - id: SC-03 + name: Data Mapping + score: 3 + max: 3 + passed: true + comment: 'X: age groups (4); depth: education level (3); Z: satisfaction score; + all axes fully populated' + - id: SC-04 + name: Title & Legend + score: 3 + max: 3 + passed: true + comment: Title 'bar-3d-categorical · letsplot · anyplot.ai' correct; manual + legend with Education Level title and correct labels + data_quality: + score: 15 + max: 15 + items: + - id: DQ-01 + name: Feature Coverage + score: 6 + max: 6 + passed: true + comment: All 12 bars (4x3 grid); front/right/top faces; grid, z-axis, value + labels, and legend all present + - id: DQ-02 + name: Realistic Context + score: 5 + max: 5 + passed: true + comment: Survey satisfaction scores by age group x education level; plausible + 5.5-8.1 range on 0-10 scale; neutral data + - id: DQ-03 + name: Appropriate Scale + score: 4 + max: 4 + passed: true + comment: Values 5.5-8.1 on 0-10 axis; ticks 0,2,4,6,8,10 sensible; 4x3 grid + within spec's 3-10 per axis requirement + code_quality: + score: 10 + max: 10 + items: + - id: CQ-01 + name: KISS Structure + score: 3 + max: 3 + passed: true + comment: Small utility helpers (prj, shade, face, seg) justified by 3D complexity; + flat linear script otherwise + - id: CQ-02 + name: Reproducibility + score: 2 + max: 2 + passed: true + comment: np.random.seed(42) used + - id: CQ-03 + name: Clean Imports + score: 2 + max: 2 + passed: true + comment: os, numpy, pandas, lets_plot — all used, none redundant + - id: CQ-04 + name: Code Elegance + score: 2 + max: 2 + passed: true + comment: Well-organized sections; complexity matches task requirements; no + fake UI + - id: CQ-05 + name: Output & API + score: 1 + max: 1 + passed: true + comment: ggsave scale=3 PNG + HTML, plot-{THEME}.png/html paths correct + library_mastery: + score: 7 + max: 10 + items: + - id: LM-01 + name: Idiomatic Usage + score: 4 + max: 5 + passed: true + comment: Correct ggplot grammar, aes mappings, geom layers, scale_fill_manual + with identity mapping is idiomatic letsplot + - id: LM-02 + name: Distinctive Features + score: 3 + max: 5 + passed: true + comment: Creative use of geom_polygon for 3D faces with painter's-order z-sorting; + custom fill identity scale; theme_void for fully hand-crafted canvas; HTML+PNG + outputs + verdict: APPROVED +impl_tags: + dependencies: [] + techniques: + - layer-composition + - custom-legend + - manual-ticks + - 3d-projection + - html-export + patterns: + - data-generation + - iteration-over-groups + dataprep: [] + styling: + - minimal-chrome + - edge-highlighting