{"spec_id":"bar-diverging-likert","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nbar-diverging-likert: Likert Scale Diverging Bar Chart\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 90/100 | Updated: 2026-06-01\n\"\"\"\n\nimport os\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\n# Imprint diverging palette for Likert — anchored at position 5 (#AE3030) and 3 (#4467A3)\n# with interpolated intermediates; fixed across themes (only chrome adapts)\nLIKERT_COLORS = [\"#AE3030\", \"#C87070\", \"#9C9B94\", \"#7A93B8\", \"#4467A3\"]\ncategories = [\"Strongly Disagree\", \"Disagree\", \"Neutral\", \"Agree\", \"Strongly Agree\"]\n\n# Data — Employee engagement survey, 10 questions on 5-point Likert scale\ndata = pd.DataFrame(\n    {\n        \"question\": [\n            \"I feel valued at work\",\n            \"My manager supports my growth\",\n            \"I have the tools I need\",\n            \"Work-life balance is respected\",\n            \"Communication is transparent\",\n            \"I see career advancement opportunities\",\n            \"The company culture is inclusive\",\n            \"My compensation is fair\",\n            \"I would recommend this workplace\",\n            \"I feel motivated daily\",\n        ],\n        \"Strongly Disagree\": [3, 5, 2, 8, 12, 15, 4, 18, 6, 10],\n        \"Disagree\": [7, 10, 5, 15, 18, 22, 8, 25, 10, 16],\n        \"Neutral\": [12, 15, 10, 14, 20, 18, 12, 15, 14, 18],\n        \"Agree\": [45, 38, 48, 35, 30, 28, 42, 28, 40, 32],\n        \"Strongly Agree\": [33, 32, 35, 28, 20, 17, 34, 14, 30, 24],\n    }\n)\n\n# Sort by net agreement (agree + strongly agree − disagree − strongly disagree)\ndata[\"net_agreement\"] = data[\"Agree\"] + data[\"Strongly Agree\"] - data[\"Disagree\"] - data[\"Strongly Disagree\"]\ndata = data.sort_values(\"net_agreement\").reset_index(drop=True)\nquestion_order = data[\"question\"].tolist()\n\n# Build diverging segments — neutral split evenly across the zero midpoint\nrows = []\nfor _, row in data.iterrows():\n    half_neutral = row[\"Neutral\"] / 2\n    positions = {\n        \"Strongly Disagree\": (\n            -(row[\"Strongly Disagree\"] + row[\"Disagree\"] + half_neutral),\n            -(row[\"Disagree\"] + half_neutral),\n        ),\n        \"Disagree\": (-(row[\"Disagree\"] + half_neutral), -half_neutral),\n        \"Neutral\": (-half_neutral, half_neutral),\n        \"Agree\": (half_neutral, half_neutral + row[\"Agree\"]),\n        \"Strongly Agree\": (half_neutral + row[\"Agree\"], half_neutral + row[\"Agree\"] + row[\"Strongly Agree\"]),\n    }\n    for cat in categories:\n        x_start, x_end = positions[cat]\n        val = row[cat]\n        rows.append(\n            {\n                \"question\": row[\"question\"],\n                \"x_start\": x_start,\n                \"x_end\": x_end,\n                \"category\": cat,\n                \"value\": val,\n                \"x_mid\": (x_start + x_end) / 2,\n                \"label\": f\"{int(val)}%\",\n            }\n        )\n\nsegments_df = pd.DataFrame(rows)\n\n# Title — scale font size linearly if longer than the 67-char baseline\ntitle_str = \"bar-diverging-likert · python · altair · anyplot.ai\"\ntitle_fs = max(11, round(16 * 67 / len(title_str))) if len(title_str) > 67 else 16\n\n# Bars\nbars = (\n    alt.Chart(segments_df)\n    .mark_bar(stroke=\"white\", strokeWidth=0.8)\n    .encode(\n        x=alt.X(\n            \"x_start:Q\",\n            title=\"Percentage (%)\",\n            axis=alt.Axis(titleFontSize=12, labelFontSize=10),\n            scale=alt.Scale(domain=[-55, 90]),\n        ),\n        x2=\"x_end:Q\",\n        y=alt.Y(\"question:N\", title=None, sort=question_order, axis=alt.Axis(labelFontSize=11, labelLimit=280)),\n        color=alt.Color(\n            \"category:N\",\n            scale=alt.Scale(domain=categories, range=LIKERT_COLORS),\n            legend=alt.Legend(title=None, labelFontSize=10, symbolSize=200, orient=\"bottom\", direction=\"horizontal\"),\n        ),\n        tooltip=[\n            alt.Tooltip(\"question:N\", title=\"Question\"),\n            alt.Tooltip(\"category:N\", title=\"Response\"),\n            alt.Tooltip(\"value:Q\", title=\"%\", format=\".0f\"),\n        ],\n    )\n)\n\n# In-bar labels — white text on dark segments (Strongly Disagree / Strongly Agree)\ndark_segs = segments_df[\n    (segments_df[\"value\"] >= 10) & segments_df[\"category\"].isin([\"Strongly Disagree\", \"Strongly Agree\"])\n]\nlabels_white = (\n    alt.Chart(dark_segs)\n    .mark_text(fontSize=13, fontWeight=\"bold\", color=\"white\")\n    .encode(x=\"x_mid:Q\", y=alt.Y(\"question:N\", sort=question_order), text=\"label:N\")\n)\n\n# In-bar labels — dark text on light segments (Disagree / Neutral / Agree)\nlight_segs = segments_df[(segments_df[\"value\"] >= 10) & segments_df[\"category\"].isin([\"Disagree\", \"Neutral\", \"Agree\"])]\nlabels_dark = (\n    alt.Chart(light_segs)\n    .mark_text(fontSize=13, fontWeight=\"bold\", color=INK)\n    .encode(x=\"x_mid:Q\", y=alt.Y(\"question:N\", sort=question_order), text=\"label:N\")\n)\n\n# Zero baseline\nzero_line = alt.Chart(pd.DataFrame({\"x\": [0]})).mark_rule(color=INK_SOFT, strokeWidth=1.5).encode(x=\"x:Q\")\n\n# Compose and apply theme-adaptive chrome\nchart = (\n    (bars + labels_white + labels_dark + zero_line)\n    .properties(\n        width=580,\n        height=340,\n        background=PAGE_BG,\n        title=alt.Title(title_str, fontSize=title_fs, anchor=\"middle\", color=INK),\n    )\n    .configure_view(strokeWidth=0, fill=PAGE_BG)\n    .configure_axisX(\n        gridOpacity=0.15, gridColor=INK, domainColor=INK_SOFT, tickColor=INK_SOFT, labelColor=INK_SOFT, titleColor=INK\n    )\n    .configure_axisY(grid=False, domainColor=INK_SOFT, tickColor=INK_SOFT, labelColor=INK_SOFT)\n    .configure_legend(fillColor=ELEVATED_BG, strokeColor=INK_SOFT, labelColor=INK_SOFT, titleColor=INK)\n)\n\n# Save — PAD to exact 3200×1800; never crop\nTW, TH = 3200, 1800\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\nchart.save(f\"plot-{THEME}.html\")\n\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}×{_h}, exceeds target {TW}×{TH}. \"\n        f\"Shrink chart .properties(width=, height=) 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"}