{"spec_id":"bar-spine","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nbar-spine: Spine Plot for Two-Variable Proportions\nLibrary: altair 6.3.0 | Python 3.13.15\nQuality: 88/100 | Updated: 2026-09-27\n\"\"\"\n\nimport os\nimport sys\n\n\n# Prevent this file from shadowing the installed altair package\n_script_dir = os.path.dirname(os.path.abspath(__file__))\nsys.path = [p for p in sys.path if os.path.abspath(p or \".\") != _script_dir]\ndel _script_dir\n\nimport altair as alt\nimport pandas as pd\nfrom PIL import Image\n\n\n# Theme tokens\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\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\"]\n# Per-segment percentage-label text color, chosen for >=4.5:1 contrast against\n# each fixed Imprint fill (data colors don't change with theme).\nLABEL_TEXT_COLOR = {\n    \"Very Comfortable\": \"#FFFFFF\",\n    \"Comfortable\": \"#1A1A17\",\n    \"Neutral\": \"#FFFFFF\",\n    \"Uncomfortable\": \"#1A1A17\",\n}\n\n# Data — technology comfort survey by age group\nage_groups = [\"18-24\", \"25-34\", \"35-44\", \"45-54\", \"55-64\", \"65+\"]\ncomfort_cats = [\"Very Comfortable\", \"Comfortable\", \"Neutral\", \"Uncomfortable\"]\n\nraw_counts = {\n    \"18-24\": [280, 180, 60, 30],\n    \"25-34\": [420, 310, 90, 40],\n    \"35-44\": [380, 320, 120, 60],\n    \"45-54\": [200, 290, 180, 110],\n    \"55-64\": [120, 240, 200, 160],\n    \"65+\": [60, 150, 170, 200],\n}\n\nrecords = []\nfor age in age_groups:\n    for cat, cnt in zip(comfort_cats, raw_counts[age], strict=False):\n        records.append({\"age_group\": age, \"comfort\": cat, \"count\": cnt})\ndf = pd.DataFrame(records)\n\n# Marginal totals → proportional bar widths\ntotals = df.groupby(\"age_group\")[\"count\"].sum().reindex(age_groups)\ngrand_total = int(totals.sum())\nx_widths = totals / grand_total\nx_ends = x_widths.cumsum()\nx_starts = x_ends - x_widths\nx_mids = (x_starts + x_ends) / 2\n\n# Pre-compute rectangle boundaries for each (age_group, comfort) combination\nspine_records = []\nfor age in age_groups:\n    group = df[df[\"age_group\"] == age].set_index(\"comfort\").reindex(comfort_cats)\n    total = int(totals[age])\n    y_acc = 0.0\n    for cat in comfort_cats:\n        cnt = int(group.loc[cat, \"count\"])\n        prop = cnt / total\n        y0 = y_acc\n        y1 = y_acc + prop\n        spine_records.append(\n            {\n                \"age_group\": age,\n                \"comfort\": cat,\n                \"x_start\": float(x_starts[age]),\n                \"x_end\": float(x_ends[age]),\n                \"x_mid\": float(x_mids[age]),\n                \"y_start\": y0,\n                \"y_end\": y1,\n                \"y_mid\": (y0 + y1) / 2,\n                \"proportion\": prop,\n                \"count\": cnt,\n                \"total\": total,\n                \"pct_label\": f\"{prop:.0%}\" if prop >= 0.10 else \"\",\n                \"label_color\": LABEL_TEXT_COLOR[cat],\n            }\n        )\n        y_acc = y1\n\nspine_df = pd.DataFrame(spine_records)\n\n# X-axis tick positions at bar midpoints with custom labels\nx_mid_list = [round(float(x_mids[a]), 6) for a in age_groups]\nlabel_expr = \" : \".join(\n    [f\"abs(datum.value - {xm}) < 0.01 ? '{age}'\" for age, xm in zip(age_groups, x_mid_list, strict=False)] + [\"''\"]\n)\n\n# Spine bars\nbars = (\n    alt.Chart(spine_df)\n    .mark_rect(stroke=PAGE_BG, strokeWidth=0.5)\n    .encode(\n        x=alt.X(\n            \"x_start:Q\",\n            scale=alt.Scale(domain=[0, 1]),\n            axis=alt.Axis(\n                values=x_mid_list,\n                labelExpr=label_expr,\n                labelAngle=0,\n                title=\"Age Group\",\n                titleFontSize=12,\n                labelFontSize=10,\n                domainColor=INK_SOFT,\n                tickColor=INK_SOFT,\n                labelColor=INK_SOFT,\n                titleColor=INK,\n                grid=False,\n                tickSize=4,\n            ),\n        ),\n        x2=\"x_end:Q\",\n        y=alt.Y(\n            \"y_start:Q\",\n            scale=alt.Scale(domain=[0, 1]),\n            axis=alt.Axis(\n                format=\"%\",\n                title=\"Proportion of Respondents\",\n                titleFontSize=12,\n                labelFontSize=10,\n                domainColor=INK_SOFT,\n                tickColor=INK_SOFT,\n                labelColor=INK_SOFT,\n                titleColor=INK,\n                gridColor=INK,\n                gridOpacity=0.10,\n                grid=True,\n            ),\n        ),\n        y2=\"y_end:Q\",\n        color=alt.Color(\n            \"comfort:N\",\n            scale=alt.Scale(domain=comfort_cats, range=IMPRINT),\n            legend=alt.Legend(\n                title=\"Technology Comfort\",\n                titleFontSize=10,\n                labelFontSize=10,\n                fillColor=ELEVATED_BG,\n                strokeColor=INK_SOFT,\n                titleColor=INK,\n                labelColor=INK_SOFT,\n                orient=\"right\",\n                padding=6,\n            ),\n        ),\n        tooltip=[\n            alt.Tooltip(\"age_group:N\", title=\"Age Group\"),\n            alt.Tooltip(\"comfort:N\", title=\"Comfort Level\"),\n            alt.Tooltip(\"proportion:Q\", title=\"Proportion\", format=\".1%\"),\n            alt.Tooltip(\"count:Q\", title=\"Count\"),\n        ],\n    )\n)\n\n# Percentage labels inside segments wide enough to fit text\npct_labels = (\n    alt.Chart(spine_df[spine_df[\"pct_label\"] != \"\"])\n    .mark_text(align=\"center\", baseline=\"middle\", fontSize=10, fontWeight=\"bold\")\n    .encode(\n        x=alt.X(\"x_mid:Q\", scale=alt.Scale(domain=[0, 1])),\n        y=alt.Y(\"y_mid:Q\", scale=alt.Scale(domain=[0, 1])),\n        text=\"pct_label:N\",\n        color=alt.Color(\"label_color:N\", scale=None, legend=None),\n    )\n)\n\n# Compose and configure\nchart = (\n    alt.layer(bars, pct_labels)\n    .properties(\n        width=595,\n        height=320,\n        background=PAGE_BG,\n        title=alt.TitleParams(\n            \"Technology Comfort by Age Group · bar-spine · python · altair · anyplot.ai\",\n            subtitle=\"Comfort with technology collapses sharply past age 55\",\n            fontSize=16,\n            subtitleFontSize=11,\n            color=INK,\n            subtitleColor=INK_SOFT,\n            anchor=\"start\",\n            offset=10,\n        ),\n    )\n    .configure_view(fill=PAGE_BG, stroke=None, continuousWidth=595, continuousHeight=320)\n    .configure_title(color=INK, fontSize=16)\n)\n\n# Save — hard target: 3200 x 1800 (landscape). See prompts/library/altair.md \"Canvas\".\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\nchart.save(f\"plot-{THEME}.html\")\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"}