{"spec_id":"bar-diverging-likert","library":"plotnine","language":"python","code":"\"\"\" anyplot.ai\nbar-diverging-likert: Likert Scale Diverging Bar Chart\nLibrary: plotnine 0.15.4 | Python 3.13.13\nQuality: 89/100 | Updated: 2026-06-01\n\"\"\"\n\nimport sys\n\n\n# Remove the script directory from sys.path so the plotnine package isn't shadowed by this file\nif sys.path and sys.path[0] in (\"\", \".\"):\n    sys.path.pop(0)\nsys.path = [p for p in sys.path if not p.endswith(\"/implementations/python\")]\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom plotnine import (\n    aes,\n    annotate,\n    element_blank,\n    element_line,\n    element_rect,\n    element_text,\n    geom_rect,\n    geom_text,\n    geom_vline,\n    ggplot,\n    guide_legend,\n    guides,\n    labs,\n    scale_color_manual,\n    scale_fill_manual,\n    scale_x_continuous,\n    scale_y_continuous,\n    theme,\n    theme_minimal,\n)\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\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Imprint diverging palette for 5 Likert levels: matte-red → neutral → blue\n# Piecewise linear interpolation via numpy (avoids matplotlib import conflict)\nc_neg = np.array([0xAE, 0x30, 0x30], dtype=float)\nc_mid = np.array([0x6B, 0x6A, 0x63] if THEME == \"light\" else [0xA8, 0xA7, 0x9F], dtype=float)\nc_pos = np.array([0x44, 0x67, 0xA3], dtype=float)\nlevels = [\n    \"#{:02x}{:02x}{:02x}\".format(\n        *(\n            (c_neg + t * 2 * (c_mid - c_neg)).clip(0, 255).astype(int)\n            if t <= 0.5\n            else (c_mid + (t * 2 - 1) * (c_pos - c_mid)).clip(0, 255).astype(int)\n        )\n    )\n    for t in [0.0, 0.25, 0.5, 0.75, 1.0]\n]\nresponse_order = [\"Strongly Disagree\", \"Disagree\", \"Neutral\", \"Agree\", \"Strongly Agree\"]\nfill_colors = dict(zip(response_order, levels, strict=True))\n\n# Contrast-aware label text: dark mid fills use white; lighter dark-theme mid fills use near-black\nmid_label = \"#1A1A17\" if THEME == \"dark\" else \"white\"\nlabel_colors = {\n    \"Strongly Disagree\": \"white\",\n    \"Disagree\": mid_label,\n    \"Neutral\": mid_label,\n    \"Agree\": mid_label,\n    \"Strongly Agree\": \"white\",\n}\n\n# Data: employee engagement survey — Compensation has negative net agreement to showcase divergence\nsurvey_data = pd.DataFrame(\n    {\n        \"question\": [\n            \"Team collaboration\",\n            \"Workplace environment\",\n            \"Job security\",\n            \"Company culture\",\n            \"Career growth\",\n            \"Training & development\",\n            \"Work-life balance\",\n            \"Management communication\",\n            \"Recognition & rewards\",\n            \"Compensation & benefits\",\n        ],\n        \"strongly_disagree\": [3, 4, 6, 7, 5, 10, 8, 12, 14, 20],\n        \"disagree\": [7, 8, 10, 12, 10, 15, 12, 18, 16, 25],\n        \"neutral\": [12, 14, 16, 15, 15, 18, 18, 20, 18, 20],\n        \"agree\": [42, 38, 38, 36, 40, 32, 35, 30, 30, 22],\n        \"strongly_agree\": [36, 36, 30, 30, 30, 25, 27, 20, 22, 13],\n    }\n)\n\n# Sort ascending by net agreement (lowest at bottom showcases the negative-net question)\nsurvey_data[\"net_agreement\"] = (\n    survey_data[\"agree\"] + survey_data[\"strongly_agree\"] - survey_data[\"disagree\"] - survey_data[\"strongly_disagree\"]\n)\nsurvey_data = survey_data.sort_values(\"net_agreement\", ascending=True).reset_index(drop=True)\n\n# Wide-to-long transformation for grammar of graphics\nresponse_cols = [\"strongly_disagree\", \"disagree\", \"neutral\", \"agree\", \"strongly_agree\"]\nlong_df = survey_data.melt(id_vars=[\"question\"], value_vars=response_cols, var_name=\"response_key\", value_name=\"pct\")\n\nname_map = {\n    \"strongly_disagree\": \"Strongly Disagree\",\n    \"disagree\": \"Disagree\",\n    \"neutral\": \"Neutral\",\n    \"agree\": \"Agree\",\n    \"strongly_agree\": \"Strongly Agree\",\n}\nlong_df[\"response\"] = pd.Categorical(long_df[\"response_key\"].map(name_map), categories=response_order, ordered=True)\n\n# Diverging position: offset = SD + D + N/2 centers the neutral segment on zero\noffset_map = survey_data.set_index(\"question\")[[\"strongly_disagree\", \"disagree\", \"neutral\"]].assign(\n    offset=lambda d: d[\"strongly_disagree\"] + d[\"disagree\"] + d[\"neutral\"] / 2\n)[\"offset\"]\nlong_df[\"offset\"] = long_df[\"question\"].map(offset_map)\n\nstack_pos = {col: i for i, col in enumerate(response_cols)}\nlong_df[\"stack_pos\"] = long_df[\"response_key\"].map(stack_pos)\nlong_df = long_df.sort_values([\"question\", \"stack_pos\"]).reset_index(drop=True)\n\nlong_df[\"xmax\"] = long_df.groupby(\"question\")[\"pct\"].cumsum() - long_df[\"offset\"]\nlong_df[\"xmin\"] = long_df[\"xmax\"] - long_df[\"pct\"]\n\n# Horizontal bar geometry\nquestion_order = survey_data[\"question\"].tolist()\ny_map = {q: i for i, q in enumerate(question_order)}\nlong_df[\"y_pos\"] = long_df[\"question\"].map(y_map)\nbar_height = 0.7\nlong_df[\"ymin\"] = long_df[\"y_pos\"] - bar_height / 2\nlong_df[\"ymax\"] = long_df[\"y_pos\"] + bar_height / 2\nlong_df[\"label_x\"] = (long_df[\"xmin\"] + long_df[\"xmax\"]) / 2\n# Shift neutral labels slightly right of center to avoid overlap with the x=0 reference vline\nneutral_mask = long_df[\"response_key\"] == \"neutral\"\nlong_df.loc[neutral_mask, \"label_x\"] = long_df.loc[neutral_mask, \"xmin\"] + long_df.loc[neutral_mask, \"pct\"] * 0.60\nlong_df[\"label\"] = long_df[\"pct\"].apply(lambda v: f\"{v}%\" if v >= 10 else \"\")\n\ntitle = \"bar-diverging-likert · python · plotnine · anyplot.ai\"\n\n# Plot\nplot = (\n    ggplot(long_df)\n    + geom_rect(aes(xmin=\"xmin\", xmax=\"xmax\", ymin=\"ymin\", ymax=\"ymax\", fill=\"response\"))\n    + geom_text(\n        aes(x=\"label_x\", y=\"y_pos\", label=\"label\", color=\"response\"), size=2.8, fontweight=\"bold\", show_legend=False\n    )\n    + geom_vline(xintercept=0, color=INK_SOFT, size=0.8)\n    + scale_fill_manual(values=fill_colors, breaks=response_order)\n    + scale_color_manual(values=label_colors, breaks=response_order)\n    + scale_y_continuous(breaks=list(range(len(question_order))), labels=question_order, limits=(-0.5, 10.2))\n    + scale_x_continuous(labels=lambda ticks: [f\"{abs(int(v))}%\" for v in ticks], expand=(0.02, 2))\n    + annotate(\n        \"text\", x=0, y=9.9, label=\"← Disagree    Agree →\", size=3.5, color=INK_MUTED, fontstyle=\"italic\", ha=\"center\"\n    )\n    + labs(x=\"Percentage of Responses\", y=\"\", title=title, fill=\"Response\")\n    + guides(fill=guide_legend(nrow=1))\n    + theme_minimal()\n    + theme(\n        figure_size=(8, 4.5),\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG),\n        panel_grid_major_y=element_blank(),\n        panel_grid_minor=element_blank(),\n        panel_grid_major_x=element_line(color=INK, size=0.3, alpha=0.15),\n        text=element_text(size=7),\n        axis_title=element_text(size=10, color=INK),\n        axis_text_y=element_text(size=8, color=INK_SOFT),\n        axis_text_x=element_text(size=8, color=INK_SOFT),\n        plot_title=element_text(size=12, color=INK, ha=\"center\"),\n        legend_text=element_text(size=8, color=INK_SOFT),\n        legend_title=element_text(size=8, color=INK),\n        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n        legend_position=\"bottom\",\n    )\n)\n\n# Save\nplot.save(f\"plot-{THEME}.png\", dpi=400, width=8, height=4.5, units=\"in\", verbose=False)\n"}