{"spec_id":"bar-3d-categorical","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\nbar-3d-categorical: 3D Bar Chart for Categorical Comparison\nLibrary: letsplot 4.9.0 | Python 3.13.13\nQuality: 83/100 | Created: 2026-05-15\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom lets_plot import *\n\n\nLetsPlot.setup_html()\n\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\"\nGRID_COLOR = \"#C8C7C0\" if THEME == \"light\" else \"#3A3A35\"\n\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\"]\n\n\ndef shade(col, f):\n    \"\"\"Adjust hex color brightness by factor f.\"\"\"\n    r = min(255, max(0, round(int(col[1:3], 16) * f)))\n    g = min(255, max(0, round(int(col[3:5], 16) * f)))\n    b = min(255, max(0, round(int(col[5:7], 16) * f)))\n    return f\"#{r:02X}{g:02X}{b:02X}\"\n\n\nnp.random.seed(42)\nX_CATS = [\"18–34\", \"35–49\", \"50–64\", \"65+\"]\nY_CATS = [\"High School\", \"Bachelor's\", \"Graduate\"]  # index 0 = furthest back\n\nBASE = {\"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]}\n\nNX = len(X_CATS)\nNY = len(Y_CATS)\n\nvals = {\n    (ai, ei): round(BASE[edu][ai] + np.random.uniform(-0.15, 0.15), 1)\n    for ei, edu in enumerate(Y_CATS)\n    for ai in range(NX)\n}\n\n# Cabinet projection: x3=age-group axis, y3=depth axis, z3=height\nBW = 0.78  # bar width (x3 units; gap = 1 - BW)\nBD = 0.65  # bar depth (y3 units)\nDX = 0.52  # x2 shift per unit of y3 depth\nDZ = 0.32  # z2 (upward) shift per unit of y3 depth\n\n\ndef prj(x3, y3, z3):\n    return (x3 + y3 * DX, z3 + y3 * DZ)\n\n\n# ── Base-plane grid ──────────────────────────────────────────────────\nseg_rows = []\n\n\ndef seg(x1, y1, x2, y2):\n    seg_rows.append({\"x\": x1, \"y\": y1, \"xend\": x2, \"yend\": y2})\n\n\n# Lines along the depth direction (x edges of the grid)\nfor xi in range(NX + 1):\n    p0 = prj(xi, 0, 0)\n    p1 = prj(xi, NY - 1 + BD, 0)\n    seg(p0[0], p0[1], p1[0], p1[1])\n\n# Lines along the x direction (depth edges of the grid)\nfor yi_f in [0.0, 1.0, 2.0, NY - 1 + BD]:\n    p0 = prj(0, yi_f, 0)\n    p1 = prj(NX, yi_f, 0)\n    seg(p0[0], p0[1], p1[0], p1[1])\n\ndf_seg = pd.DataFrame(seg_rows)\n\n# ── Z-axis (left-front vertical) ────────────────────────────────────\nz_segs = []\nztick_rows = []\nZ_TICKS = [0, 2, 4, 6, 8, 10]\n\nax0 = prj(0, 0, 0)\nax1 = prj(0, 0, 10)\nz_segs.append({\"x\": ax0[0], \"y\": ax0[1], \"xend\": ax1[0], \"yend\": ax1[1]})\n\nfor zt in Z_TICKS:\n    pt = prj(0, 0, zt)\n    z_segs.append({\"x\": pt[0], \"y\": pt[1], \"xend\": pt[0] - 0.18, \"yend\": pt[1]})\n    ztick_rows.append({\"x\": pt[0] - 0.25, \"y\": pt[1], \"label\": str(zt)})\n\ndf_zsegs = pd.DataFrame(z_segs)\ndf_zticks = pd.DataFrame(ztick_rows)\n\n# ── Category axis labels ─────────────────────────────────────────────\nx_lbl = []\nfor ai, age in enumerate(X_CATS):\n    pt = prj(ai + BW / 2, 0, 0)\n    x_lbl.append({\"x\": pt[0], \"y\": pt[1] - 0.55, \"label\": age})\ndf_xlbl = pd.DataFrame(x_lbl)\n\n# ── Bar polygons (painter's order: back → front) ─────────────────────\npoly_rows = []\ngc = [0]\n\n\ndef face(pts, fill_col):\n    gid = str(gc[0])\n    gc[0] += 1\n    for px, py in pts:\n        poly_rows.append({\"x\": px, \"y\": py, \"g\": gid, \"fill\": fill_col})\n\n\nfor ei in range(NY - 1, -1, -1):  # draw back→front; ei=0 (High School) drawn last = in front\n    c0 = IMPRINT[ei]\n    c_front = c0\n    c_right = shade(c0, 0.60)\n    c_top = shade(c0, 1.32)\n\n    for ai in range(NX):\n        h = vals[(ai, ei)]\n        x0, x1 = ai, ai + BW\n        y0, y1 = ei, ei + BD\n\n        C = {\n            k: prj(*v)\n            for k, v in {\n                \"bfl\": (x0, y0, 0),\n                \"bfr\": (x1, y0, 0),\n                \"bbl\": (x0, y1, 0),\n                \"bbr\": (x1, y1, 0),\n                \"tfl\": (x0, y0, h),\n                \"tfr\": (x1, y0, h),\n                \"tbl\": (x0, y1, h),\n                \"tbr\": (x1, y1, h),\n            }.items()\n        }\n\n        face([C[\"bfl\"], C[\"bfr\"], C[\"tfr\"], C[\"tfl\"]], c_front)  # front\n        face([C[\"bfr\"], C[\"bbr\"], C[\"tbr\"], C[\"tfr\"]], c_right)  # right\n        face([C[\"tfl\"], C[\"tfr\"], C[\"tbr\"], C[\"tbl\"]], c_top)  # top\n\ndf_poly = pd.DataFrame(poly_rows)\n\n# ── Value labels (centre-top of each bar) ───────────────────────────\nval_rows = []\nfor ei in range(NY):\n    for ai in range(NX):\n        h = vals[(ai, ei)]\n        pt = prj(ai + BW / 2, ei + BD / 2, h)\n        val_rows.append({\"x\": pt[0], \"y\": pt[1] + 0.28, \"label\": f\"{h:.1f}\"})\ndf_vals = pd.DataFrame(val_rows)\n\n# ── Manual legend inside chart (upper-left area, above bars) ─────────\nLEG_X = 0.15\nLEG_Y0 = 10.8\nLEG_DY = 0.65\nleg_title = [{\"x\": LEG_X, \"y\": LEG_Y0 + 0.4, \"label\": \"Education Level\"}]\nleg_rect = []\nleg_text = []\nfor i, edu in enumerate(Y_CATS):\n    lx, ly = LEG_X, LEG_Y0 - i * LEG_DY\n    leg_rect.append({\"xmin\": lx, \"xmax\": lx + 0.32, \"ymin\": ly - 0.20, \"ymax\": ly + 0.20, \"fill\": IMPRINT[i]})\n    leg_text.append({\"x\": lx + 0.42, \"y\": ly, \"label\": edu})\n\ndf_ltitle = pd.DataFrame(leg_title)\ndf_lrect = pd.DataFrame(leg_rect)\ndf_ltxt = pd.DataFrame(leg_text)\n\n# unique fill → itself (scale_fill_manual with identity mapping)\nfill_vals = {c: c for c in df_poly[\"fill\"].unique()}\n\n# ── Assemble plot ─────────────────────────────────────────────────────\nplot = (\n    ggplot()\n    + geom_segment(aes(x=\"x\", y=\"y\", xend=\"xend\", yend=\"yend\"), data=df_seg, color=GRID_COLOR, size=0.8)\n    + geom_segment(aes(x=\"x\", y=\"y\", xend=\"xend\", yend=\"yend\"), data=df_zsegs, color=INK_SOFT, size=0.7)\n    + geom_polygon(aes(x=\"x\", y=\"y\", group=\"g\", fill=\"fill\"), data=df_poly, color=PAGE_BG, size=0.3)\n    + scale_fill_manual(values=fill_vals, guide=\"none\")\n    + geom_text(aes(x=\"x\", y=\"y\", label=\"label\"), data=df_zticks, size=11, color=INK_SOFT, hjust=1)\n    + geom_text(aes(x=\"x\", y=\"y\", label=\"label\"), data=df_xlbl, size=12, color=INK_SOFT, vjust=1)\n    + geom_text(aes(x=\"x\", y=\"y\", label=\"label\"), data=df_vals, size=10, color=INK)\n    + geom_text(aes(x=\"x\", y=\"y\", label=\"label\"), data=df_ltitle, size=14, color=INK, hjust=0, fontface=\"bold\")\n    + geom_rect(\n        aes(xmin=\"xmin\", xmax=\"xmax\", ymin=\"ymin\", ymax=\"ymax\", fill=\"fill\"), data=df_lrect, color=INK_SOFT, size=0.3\n    )\n    + geom_text(aes(x=\"x\", y=\"y\", label=\"label\"), data=df_ltxt, size=13, color=INK_SOFT, hjust=0)\n    + labs(title=\"bar-3d-categorical · letsplot · anyplot.ai\")\n    + theme_void()\n    + theme(\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        plot_title=element_text(color=INK, size=24, hjust=0.5),\n        plot_margin=[30, 40, 30, 40],\n    )\n    + ggsize(1600, 900)\n)\n\nggsave(plot, f\"plot-{THEME}.png\", path=\".\", scale=3)\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n"}