{"spec_id":"bar-3d-categorical","library":"plotnine","language":"python","code":"\"\"\" anyplot.ai\nbar-3d-categorical: 3D Bar Chart for Categorical Comparison\nLibrary: plotnine 0.15.8 | Python 3.13.15\nQuality: 92/100 | Created: 2026-08-24\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom plotnine import (\n    aes,\n    coord_fixed,\n    element_rect,\n    element_text,\n    geom_path,\n    geom_point,\n    geom_polygon,\n    geom_text,\n    ggplot,\n    labs,\n    scale_color_manual,\n    scale_fill_identity,\n    theme,\n    theme_void,\n)\n\n\n# Theme tokens (see prompts/default-style-guide.md \"Background\" + \"Theme-adaptive Chrome\")\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Imprint palette — 8 hues, theme-independent, hybrid-v3 sort\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\n\n# Data — quarterly revenue across five regions (grid of 5 x 4 = 20 bars)\nnp.random.seed(42)\nregions = [\"North\", \"South\", \"East\", \"West\", \"Central\"]\nquarters = [\"Q1\", \"Q2\", \"Q3\", \"Q4\"]\nregion_baseline = {\"North\": 62, \"South\": 48, \"East\": 70, \"West\": 55, \"Central\": 80}\nquarter_growth = {\"Q1\": 0, \"Q2\": 4, \"Q3\": 9, \"Q4\": 15}\n\nrecords = []\nfor region in regions:\n    for quarter in quarters:\n        noise = np.random.normal(0, 3)\n        revenue = region_baseline[region] + quarter_growth[quarter] + noise\n        records.append({\"region\": region, \"quarter\": quarter, \"revenue\": round(revenue, 1)})\ndata = pd.DataFrame(records)\n\n# Isometric projection — elevation ~30 deg, azimuth ~45 deg (see specification.md \"Notes\")\nCOS30 = np.cos(np.radians(30))\nSIN30 = np.sin(np.radians(30))\n\n\ndef project(gx, gy, gz):\n    px = (gx - gy) * COS30\n    py = (gx + gy) * SIN30 + gz\n    return px, py\n\n\ndef hex_to_rgb(hex_color):\n    hex_color = hex_color.lstrip(\"#\")\n    return tuple(int(hex_color[i : i + 2], 16) / 255 for i in (0, 2, 4))\n\n\ndef rgb_to_hex(rgb):\n    clamped = (max(0.0, min(1.0, c)) for c in rgb)\n    return \"#{:02X}{:02X}{:02X}\".format(*(round(c * 255) for c in clamped))\n\n\ndef shade(hex_color, amount):\n    \"\"\"Lighten (amount > 0, toward white) or darken (amount < 0, toward black).\"\"\"\n    r, g, b = hex_to_rgb(hex_color)\n    if amount >= 0:\n        r, g, b = (r + (1 - r) * amount, g + (1 - g) * amount, b + (1 - b) * amount)\n    else:\n        r, g, b = (r * (1 + amount), g * (1 + amount), b * (1 + amount))\n    return rgb_to_hex((r, g, b))\n\n\n# Bar footprint & height scaling\nCELL, BAR_W, BAR_D = 1.0, 0.6, 0.6\nMARGIN = (CELL - BAR_W) / 2\nHEIGHT_SCALE = 3.5 / data[\"revenue\"].max()\n\n# Build bars far-to-near (painter's algorithm) so nearer bars occlude farther ones\nbars = []\nfor i, region in enumerate(regions):\n    for j, quarter in enumerate(quarters):\n        revenue = data.loc[(data.region == region) & (data.quarter == quarter), \"revenue\"].iloc[0]\n        bars.append({\"i\": i, \"j\": j, \"region\": region, \"quarter\": quarter, \"revenue\": revenue, \"depth_key\": i + j})\nbars.sort(key=lambda b: -b[\"depth_key\"])\n\nfaces = []\nvalue_labels = []\npoly_id = 0\nfor bar in bars:\n    i, j = bar[\"i\"], bar[\"j\"]\n    x0, x1 = i + MARGIN, i + MARGIN + BAR_W\n    y0, y1 = j + MARGIN, j + MARGIN + BAR_D\n    h = bar[\"revenue\"] * HEIGHT_SCALE\n    base_color = IMPRINT_PALETTE[i % len(IMPRINT_PALETTE)]\n    top_color = shade(base_color, 0.35)\n    left_color = shade(base_color, -0.10)\n    right_color = shade(base_color, -0.35)\n\n    # Top face — z = h\n    top_corners = [(x0, y0, h), (x1, y0, h), (x1, y1, h), (x0, y1, h)]\n    # Left face — x = x0 (nearest vertical edge is (x0, y0))\n    left_corners = [(x0, y0, 0), (x0, y1, 0), (x0, y1, h), (x0, y0, h)]\n    # Right face — y = y0\n    right_corners = [(x0, y0, 0), (x1, y0, 0), (x1, y0, h), (x0, y0, h)]\n\n    for corners, fill_hex in ((top_corners, top_color), (left_corners, left_color), (right_corners, right_color)):\n        for order, (gx, gy, gz) in enumerate(corners):\n            px, py = project(gx, gy, gz)\n            faces.append({\"poly_id\": poly_id, \"order\": order, \"px\": px, \"py\": py, \"fill_hex\": fill_hex})\n        poly_id += 1\n\n    # Value label above the bar top (only feasible for grids under 25 bars, see spec Notes)\n    top_cx = (x0 + x1) / 2\n    top_cy = (y0 + y1) / 2\n    label_px, label_py = project(top_cx, top_cy, h)\n    value_labels.append({\"px\": label_px, \"py\": label_py + 0.18, \"label\": f\"{bar['revenue']:.0f}\"})\n\nfaces_df = pd.DataFrame(faces)\nlabels_df = pd.DataFrame(value_labels)\n\n# Base-plane grid lines (relate bars to their categorical position)\ngrid_lines = []\nline_id = 0\nfor i in range(len(regions) + 1):\n    for gy in np.linspace(0, len(quarters), 2):\n        px, py = project(i, gy, 0)\n        grid_lines.append({\"line_id\": line_id, \"px\": px, \"py\": py})\n    line_id += 1\nfor j in range(len(quarters) + 1):\n    for gx in np.linspace(0, len(regions), 2):\n        px, py = project(gx, j, 0)\n        grid_lines.append({\"line_id\": line_id, \"px\": px, \"py\": py})\n    line_id += 1\ngrid_df = pd.DataFrame(grid_lines)\n\n# Category tick labels along the two front edges of the base plane\nregion_ticks = []\nfor i, region in enumerate(regions):\n    px, py = project(i + 0.5, -0.3, 0)\n    region_ticks.append({\"px\": px, \"py\": py, \"label\": region})\nregion_ticks_df = pd.DataFrame(region_ticks)\n\nquarter_ticks = []\nfor j, quarter in enumerate(quarters):\n    px, py = project(-0.3, j + 0.5, 0)\n    quarter_ticks.append({\"px\": px, \"py\": py, \"label\": quarter})\nquarter_ticks_df = pd.DataFrame(quarter_ticks)\n\n# Invisible legend-proxy layer — a real geom on the 'color' aesthetic (independent of the\n# 'fill' aesthetic used by the shaded faces) so the legend swatch shows the true region color\nregion_color_map = {region: IMPRINT_PALETTE[i % len(IMPRINT_PALETTE)] for i, region in enumerate(regions)}\nlegend_proxy = []\nfor i, region in enumerate(regions):\n    px, py = project(i + 0.5, MARGIN + BAR_D / 2, 0.05)\n    legend_proxy.append({\"px\": px, \"py\": py, \"region\": region})\nlegend_proxy_df = pd.DataFrame(legend_proxy)\n\n# Theme-adaptive chrome — bespoke isometric canvas (no meaningful cartesian axes)\nanyplot_theme = theme_void() + theme(\n    plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n    legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n    legend_text=element_text(color=INK_SOFT, size=8),\n    legend_title=element_text(color=INK, size=10),\n    plot_title=element_text(color=INK, size=12, weight=\"bold\", ha=\"center\"),\n    plot_caption=element_text(color=INK_MUTED, size=7, ha=\"center\"),\n    legend_position=\"right\",\n    figure_size=(8, 4.5),\n)\n\nplot = (\n    ggplot()\n    + geom_path(grid_df, aes(x=\"px\", y=\"py\", group=\"line_id\"), color=INK_SOFT, alpha=0.2, size=0.4)\n    + geom_polygon(\n        faces_df, aes(x=\"px\", y=\"py\", group=\"poly_id\", fill=\"fill_hex\"), color=PAGE_BG, size=0.3, show_legend=False\n    )\n    + geom_point(legend_proxy_df, aes(x=\"px\", y=\"py\", color=\"region\"), size=0.001, alpha=1)\n    + geom_text(labels_df, aes(x=\"px\", y=\"py\", label=\"label\"), color=INK, size=6, fontweight=\"bold\")\n    + geom_text(region_ticks_df, aes(x=\"px\", y=\"py\", label=\"label\"), color=INK_SOFT, size=7, angle=30)\n    + geom_text(quarter_ticks_df, aes(x=\"px\", y=\"py\", label=\"label\"), color=INK_SOFT, size=7, angle=-30)\n    + scale_fill_identity()\n    + scale_color_manual(values=region_color_map, name=\"Region\", breaks=regions)\n    + coord_fixed(ratio=1)\n    + labs(title=\"bar-3d-categorical · python · plotnine · anyplot.ai\", caption=\"Bar height = Quarterly revenue ($K)\")\n    + anyplot_theme\n)\n\nplot.save(f\"plot-{THEME}.png\", dpi=400, width=8, height=4.5, units=\"in\")\n"}