{"spec_id":"bar-grouped","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nbar-grouped: Grouped Bar Chart\nLibrary: altair 6.2.2 | Python 3.13.14\nQuality: 93/100 | Updated: 2026-08-05\n\"\"\"\n\nimport os\nimport sys\n\nimport pandas as pd\nfrom PIL import Image\n\n\n# Remove current directory from path to avoid local altair.py shadowing\nscript_dir = os.path.dirname(os.path.abspath(__file__))\nsys.path = [p for p in sys.path if p != script_dir and os.path.abspath(p) != script_dir]\n\nimport altair as alt\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\"\n\n# Imprint palette (positions 1->3) — Product is abstract, so canonical order applies\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\"]\n\n# Data: Quarterly revenue by product line\ndata = pd.DataFrame(\n    {\n        \"Quarter\": [\"Q1\", \"Q1\", \"Q1\", \"Q2\", \"Q2\", \"Q2\", \"Q3\", \"Q3\", \"Q3\", \"Q4\", \"Q4\", \"Q4\"],\n        \"Product\": [\"Software\", \"Hardware\", \"Services\"] * 4,\n        \"Revenue\": [\n            120,\n            85,\n            45,  # Q1\n            145,\n            78,\n            52,  # Q2\n            132,\n            92,\n            68,  # Q3\n            168,\n            105,\n            75,  # Q4\n        ],\n    }\n)\n\n# Legend-bound selection: click a product in the legend to isolate it, showcasing\n# altair's declarative interaction model (LM-01/LM-02) — resets to full opacity\n# when the same legend entry is clicked again (altair's built-in toggle behavior).\nproduct_selection = alt.selection_point(fields=[\"Product\"], bind=\"legend\")\n\nbars = (\n    alt.Chart(data)\n    .mark_bar(cornerRadiusTopLeft=3, cornerRadiusTopRight=3)\n    .encode(\n        x=alt.X(\n            \"Quarter:O\",\n            title=\"Quarter\",\n            axis=alt.Axis(labelAngle=0, labelFontSize=10, titleFontSize=12, labelColor=INK_SOFT, titleColor=INK),\n        ),\n        xOffset=alt.XOffset(\"Product:N\", sort=[\"Software\", \"Hardware\", \"Services\"]),\n        y=alt.Y(\n            \"Revenue:Q\",\n            title=\"Revenue (thousands USD)\",\n            axis=alt.Axis(labelFontSize=10, titleFontSize=12, labelColor=INK_SOFT, titleColor=INK),\n        ),\n        color=alt.Color(\n            \"Product:N\",\n            scale=alt.Scale(domain=[\"Software\", \"Hardware\", \"Services\"], range=IMPRINT),\n            legend=alt.Legend(\n                title=\"Product Line\", titleFontSize=10, labelFontSize=10, orient=\"bottom\", direction=\"horizontal\"\n            ),\n        ),\n        opacity=alt.condition(product_selection, alt.value(1.0), alt.value(0.25)),\n        tooltip=[\"Quarter\", \"Product\", \"Revenue\"],\n    )\n    .add_params(product_selection)\n)\n\n# Value labels above each bar — direct, forceful emphasis for precise comparison\n# (DE-03: data storytelling) rather than relying on bar height alone.\nlabels = (\n    alt.Chart(data)\n    .mark_text(dy=-6, fontSize=9, color=INK_SOFT)\n    .encode(\n        x=alt.X(\"Quarter:O\"),\n        xOffset=alt.XOffset(\"Product:N\", sort=[\"Software\", \"Hardware\", \"Services\"]),\n        y=alt.Y(\"Revenue:Q\"),\n        text=alt.Text(\"Revenue:Q\", format=\".0f\"),\n    )\n)\n\n\n# Callout on the standout bar (Q4 Software, the clear growth peak) — a deliberate\n# storytelling touch beyond the raw value labels (DE-03).\ncallout = pd.DataFrame({\"Quarter\": [\"Q4\"], \"Product\": [\"Software\"], \"Revenue\": [168], \"note\": [\"peak quarter\"]})\ncallout_text = (\n    alt.Chart(callout)\n    .mark_text(dy=-22, fontSize=9, fontWeight=\"bold\", color=IMPRINT[0])\n    .encode(\n        x=alt.X(\"Quarter:O\"),\n        xOffset=alt.XOffset(\"Product:N\", sort=[\"Software\", \"Hardware\", \"Services\"]),\n        y=alt.Y(\"Revenue:Q\"),\n        text=\"note:N\",\n    )\n)\n\nchart = (\n    (bars + labels + callout_text)\n    .properties(\n        width=620,\n        height=320,\n        background=PAGE_BG,\n        title=alt.Title(\"bar-grouped · python · altair · anyplot.ai\", fontSize=18, anchor=\"middle\"),\n    )\n    .configure_axis(domainColor=INK_SOFT, tickColor=INK_SOFT, gridColor=INK, gridOpacity=0.10)\n    .configure_view(fill=PAGE_BG, stroke=INK_SOFT, strokeWidth=0)\n    .configure_title(color=INK)\n    .configure_legend(fillColor=ELEVATED_BG, strokeColor=INK_SOFT, labelColor=INK_SOFT, titleColor=INK)\n)\n\n# Save as PNG (Canvas hard rule — see prompts/library/altair.md \"Canvas\")\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\n\nTW, TH = 3200, 1800\n_img = Image.open(f\"plot-{THEME}.png\").convert(\"RGB\")\n_w, _h = _img.size\nif _w > TW or _h > TH:\n    raise SystemExit(\n        f\"altair vl-convert produced {_w}x{_h}, exceeds target {TW}x{TH}. \"\n        f\"Shrink chart .properties(width=, height=) values and re-render.\"\n    )\nif _w < TW or _h < TH:\n    _canvas = Image.new(\"RGB\", (TW, TH), PAGE_BG)\n    _canvas.paste(_img, ((TW - _w) // 2, (TH - _h) // 2))\n    _canvas.save(f\"plot-{THEME}.png\")\n\n# Save as HTML (untouched by the pad step — only PNGs are gated)\nchart.save(f\"plot-{THEME}.html\")\n"}