{"spec_id":"bar-diverging-likert","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\nbar-diverging-likert: Likert Scale Diverging Bar Chart\nLibrary: letsplot 4.10.1 | Python 3.13.13\nQuality: 90/100 | Updated: 2026-06-01\n\"\"\"\n\nimport os\n\nimport pandas as pd\nfrom lets_plot import *\n\n\nLetsPlot.setup_html()\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\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Imprint diverging palette for 5 Likert categories\n# Interpolated from imprint_div anchors: #AE3030 ↔ INK_MUTED (neutral) ↔ #4467A3\n_rn = (int(INK_MUTED[1:3], 16), int(INK_MUTED[3:5], 16), int(INK_MUTED[5:7], 16))\n_c_d = \"#{:02X}{:02X}{:02X}\".format(\n    round(0xAE + (_rn[0] - 0xAE) * 0.5), round(0x30 + (_rn[1] - 0x30) * 0.5), round(0x30 + (_rn[2] - 0x30) * 0.5)\n)\n_c_a = \"#{:02X}{:02X}{:02X}\".format(\n    round(_rn[0] + (0x44 - _rn[0]) * 0.5), round(_rn[1] + (0x67 - _rn[1]) * 0.5), round(_rn[2] + (0xA3 - _rn[2]) * 0.5)\n)\ncategories = [\"Strongly Disagree\", \"Disagree\", \"Neutral\", \"Agree\", \"Strongly Agree\"]\nlikert_palette = [\"#AE3030\", _c_d, INK_MUTED, _c_a, \"#4467A3\"]\n\n# Text colors inside bars — near-white works on all bars except neutral in dark theme\ntext_color_map = {\n    \"Strongly Disagree\": \"#F0EFE8\",\n    \"Disagree\": \"#F0EFE8\",\n    \"Neutral\": \"#1A1A17\" if THEME == \"dark\" else \"#F0EFE8\",\n    \"Agree\": \"#F0EFE8\",\n    \"Strongly Agree\": \"#F0EFE8\",\n}\n\n# Data: Employee engagement survey (percentages, each row sums to 100)\nsurvey = pd.DataFrame(\n    {\n        \"question\": [\n            \"My manager supports me\",\n            \"I'd recommend this company\",\n            \"I feel valued at work\",\n            \"Goals are well-defined\",\n            \"Work-life balance is good\",\n            \"Resources are adequate\",\n            \"I have growth opportunities\",\n            \"Communication is clear\",\n        ],\n        \"Strongly Disagree\": [3, 4, 5, 7, 8, 10, 12, 15],\n        \"Disagree\": [8, 9, 10, 15, 18, 20, 22, 25],\n        \"Neutral\": [12, 14, 15, 18, 15, 22, 20, 18],\n        \"Agree\": [42, 40, 45, 38, 38, 32, 30, 28],\n        \"Strongly Agree\": [35, 33, 25, 22, 21, 16, 16, 14],\n    }\n)\n\n# Sort by net agreement (positive minus negative)\nsurvey[\"net\"] = survey[\"Agree\"] + survey[\"Strongly Agree\"] - survey[\"Disagree\"] - survey[\"Strongly Disagree\"]\nsurvey = survey.sort_values(\"net\").reset_index(drop=True)\n\n# Build diverging bar segments centered on neutral midpoint\nrows = []\nbar_extents = {}\nfor idx, row in survey.iterrows():\n    half_n = row[\"Neutral\"] / 2\n    segments = [\n        (\n            \"Strongly Disagree\",\n            -(half_n + row[\"Disagree\"] + row[\"Strongly Disagree\"]),\n            -(half_n + row[\"Disagree\"]),\n            row[\"Strongly Disagree\"],\n        ),\n        (\"Disagree\", -(half_n + row[\"Disagree\"]), -half_n, row[\"Disagree\"]),\n        (\"Neutral\", -half_n, half_n, row[\"Neutral\"]),\n        (\"Agree\", half_n, half_n + row[\"Agree\"], row[\"Agree\"]),\n        (\"Strongly Agree\", half_n + row[\"Agree\"], half_n + row[\"Agree\"] + row[\"Strongly Agree\"], row[\"Strongly Agree\"]),\n    ]\n    bar_extents[idx] = half_n + row[\"Agree\"] + row[\"Strongly Agree\"]\n\n    for cat, xmin, xmax, pct in segments:\n        rows.append(\n            {\n                \"y\": idx,\n                \"ymin\": idx - 0.38,\n                \"ymax\": idx + 0.38,\n                \"xmin\": xmin,\n                \"xmax\": xmax,\n                \"category\": cat,\n                \"pct\": int(pct),\n                \"x_mid\": (xmin + xmax) / 2,\n                \"label\": f\"{int(pct)}%\" if pct >= 8 else \"\",\n                \"text_color\": text_color_map[cat],\n                \"question\": row[\"question\"],\n            }\n        )\n\nrect_df = pd.DataFrame(rows)\nfor col in [\"category\", \"label\", \"text_color\", \"question\"]:\n    rect_df[col] = rect_df[col].astype(object)\n\nlabel_df = rect_df[rect_df[\"label\"] != \"\"].copy()\n\nq_labels = survey[\"question\"].tolist()\nq_breaks = list(range(len(q_labels)))\n\n# Net agreement annotations (storytelling focal points)\ntop_idx = len(survey) - 1\nbot_idx = 0\ntop_net = int(survey.iloc[top_idx][\"net\"])\nbot_net = int(survey.iloc[bot_idx][\"net\"])\n\nannot_top = pd.DataFrame({\"x\": [bar_extents[top_idx] + 2], \"y\": [top_idx], \"label\": [f\"Net +{top_net}%\"]})\nannot_bot = pd.DataFrame({\"x\": [bar_extents[bot_idx] + 2], \"y\": [bot_idx], \"label\": [f\"Net +{bot_net}%\"]})\n\n# Subtle theme-adaptive highlight bands for best/worst items\nhl_fill_top = \"#DFF0F8\" if THEME == \"light\" else \"#1B2229\"\nhl_fill_bot = \"#F8E6E6\" if THEME == \"light\" else \"#291C1B\"\nhl_top = pd.DataFrame({\"xmin\": [-60], \"xmax\": [100], \"ymin\": [top_idx - 0.48], \"ymax\": [top_idx + 0.48]})\nhl_bot = pd.DataFrame({\"xmin\": [-60], \"xmax\": [100], \"ymin\": [bot_idx - 0.48], \"ymax\": [bot_idx + 0.48]})\n\n# Plot\ntitle_str = \"bar-diverging-likert · python · letsplot · anyplot.ai\"\n\nplot = (\n    ggplot()\n    # Highlight bands — visual emphasis on best/worst items\n    + geom_rect(\n        data=hl_top,\n        mapping=aes(xmin=\"xmin\", xmax=\"xmax\", ymin=\"ymin\", ymax=\"ymax\"),\n        fill=hl_fill_top,\n        color=hl_fill_top,\n        alpha=0.7,\n    )\n    + geom_rect(\n        data=hl_bot,\n        mapping=aes(xmin=\"xmin\", xmax=\"xmax\", ymin=\"ymin\", ymax=\"ymax\"),\n        fill=hl_fill_bot,\n        color=hl_fill_bot,\n        alpha=0.7,\n    )\n    # Diverging bar segments with interactive tooltips\n    + geom_rect(\n        data=rect_df,\n        mapping=aes(xmin=\"xmin\", xmax=\"xmax\", ymin=\"ymin\", ymax=\"ymax\", fill=\"category\"),\n        tooltips=layer_tooltips().line(\"@question\").line(\"@category: @{pct}%\").format(\"pct\", \"d\"),\n    )\n    # Percentage labels inside bars\n    + geom_text(\n        data=label_df, mapping=aes(x=\"x_mid\", y=\"y\", label=\"label\", color=\"text_color\"), size=5, fontface=\"bold\"\n    )\n    # Center reference line\n    + geom_vline(xintercept=0, color=INK_SOFT, size=0.8)\n    # Net agreement annotations\n    + geom_text(\n        data=annot_top, mapping=aes(x=\"x\", y=\"y\", label=\"label\"), color=\"#4467A3\", size=4, fontface=\"bold\", hjust=0\n    )\n    + geom_text(\n        data=annot_bot, mapping=aes(x=\"x\", y=\"y\", label=\"label\"), color=\"#AE3030\", size=4, fontface=\"bold\", hjust=0\n    )\n    # Scales\n    + scale_fill_manual(values=likert_palette, breaks=categories, name=\"Response\")\n    + scale_color_identity()\n    + scale_y_continuous(breaks=q_breaks, labels=q_labels)\n    + scale_x_continuous(breaks=[-40, -20, 0, 20, 40, 60, 80], labels=[\"40%\", \"20%\", \"0%\", \"20%\", \"40%\", \"60%\", \"80%\"])\n    + labs(\n        title=title_str,\n        subtitle=\"Employee engagement survey — sorted by net agreement\",\n        x=\"Percentage of Responses\",\n        y=\"\",\n    )\n    + theme_minimal()\n    + theme(\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG),\n        plot_title=element_text(size=16, face=\"bold\", color=INK),\n        plot_subtitle=element_text(size=13, color=INK_MUTED),\n        axis_title_x=element_text(size=12, color=INK),\n        axis_title_y=element_blank(),\n        axis_text_x=element_text(size=10, color=INK_SOFT),\n        axis_text_y=element_text(size=10, color=INK_SOFT),\n        legend_title=element_text(size=12, color=INK),\n        legend_text=element_text(size=10, color=INK_SOFT),\n        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n        legend_position=\"bottom\",\n        panel_grid_major_x=element_line(color=INK_SOFT, size=0.2),\n        panel_grid_major_y=element_blank(),\n        panel_grid_minor=element_blank(),\n    )\n    + ggsize(800, 450)\n)\n\n# Save\nggsave(plot, f\"plot-{THEME}.png\", path=\".\", scale=4)\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n"}