{"spec_id":"bar-diverging-likert","library":"plotly","language":"python","code":"\"\"\" anyplot.ai\nbar-diverging-likert: Likert Scale Diverging Bar Chart\nLibrary: plotly 6.7.0 | Python 3.13.13\nQuality: 91/100 | Updated: 2026-06-01\n\"\"\"\n\nimport os\nimport sys\n\n\n# Prevent self-import: remove this script's directory from sys.path so that\n# \"import plotly\" resolves to the installed package, not this file.\n_here = os.path.dirname(os.path.abspath(__file__))\nsys.path = [p for p in sys.path if os.path.abspath(p) != _here]\n\nimport pandas as pd\nimport plotly.graph_objects as go\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\"\nGRID = \"rgba(26,26,23,0.15)\" if THEME == \"light\" else \"rgba(240,239,232,0.15)\"\n\n# Imprint diverging colors for Likert scale (sampled from imprint_div: #AE3030 → midpoint → #4467A3)\n# Intermediate shades interpolated toward the theme midpoint; neutral uses the muted anchor\nLIKERT_COLORS = {\n    \"strongly_disagree\": \"#AE3030\",\n    \"disagree\": \"#CC807D\" if THEME == \"light\" else \"#B06060\",\n    \"neutral\": INK_MUTED,\n    \"agree\": \"#8DA1C2\" if THEME == \"light\" else \"#4A6898\",\n    \"strongly_agree\": \"#4467A3\",\n}\n\n# Text contrast inside bar segments\nTEXT_COLORS = {\n    \"strongly_disagree\": \"#F0EFE8\",\n    \"disagree\": INK if THEME == \"light\" else \"#F0EFE8\",\n    \"neutral\": \"#F0EFE8\" if THEME == \"light\" else \"#1A1A17\",\n    \"agree\": INK if THEME == \"light\" else \"#F0EFE8\",\n    \"strongly_agree\": \"#F0EFE8\",\n}\n\nFONT_FAMILY = \"Inter, Helvetica Neue, Arial, sans-serif\"\n\n# Data — employee engagement survey (10 questions, 5-point Likert scale)\ndf = 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            \"Communication is transparent\",\n            \"Work-life balance is respected\",\n            \"I see a clear career path\",\n            \"Team collaboration is effective\",\n            \"Company vision is inspiring\",\n            \"Training opportunities are sufficient\",\n            \"I would recommend this workplace\",\n        ],\n        \"strongly_disagree\": [5, 8, 3, 15, 10, 20, 5, 12, 18, 6],\n        \"disagree\": [10, 12, 7, 20, 15, 25, 10, 18, 22, 9],\n        \"neutral\": [15, 20, 10, 25, 20, 20, 15, 20, 25, 12],\n        \"agree\": [35, 30, 40, 25, 30, 20, 35, 28, 20, 33],\n        \"strongly_agree\": [35, 30, 40, 15, 25, 15, 35, 22, 15, 40],\n    }\n)\n\n# Sort by net agreement (positive minus negative)\ndf[\"net_agreement\"] = df[\"agree\"] + df[\"strongly_agree\"] - df[\"disagree\"] - df[\"strongly_disagree\"]\ndf = df.sort_values(\"net_agreement\").reset_index(drop=True)\n\n# Diverging positions — neutral split evenly across zero midpoint\nhalf_neutral = df[\"neutral\"] / 2\nneg_sd = -(df[\"strongly_disagree\"] + df[\"disagree\"] + half_neutral)\nneg_d = -(df[\"disagree\"] + half_neutral)\nneg_n = -half_neutral\npos_n = half_neutral\npos_a = half_neutral + df[\"agree\"]\npos_sa = half_neutral + df[\"agree\"] + df[\"strongly_agree\"]\n\nlabels = {\n    \"strongly_disagree\": \"Strongly Disagree\",\n    \"disagree\": \"Disagree\",\n    \"neutral\": \"Neutral\",\n    \"agree\": \"Agree\",\n    \"strongly_agree\": \"Strongly Agree\",\n}\n\n# Plot\nfig = go.Figure()\n\nsegments = [\n    (\"strongly_disagree\", neg_sd, neg_d),\n    (\"disagree\", neg_d, neg_n),\n    (\"neutral\", neg_n, pos_n),\n    (\"agree\", pos_n, pos_a),\n    (\"strongly_agree\", pos_a, pos_sa),\n]\n\nfor key, starts, ends in segments:\n    widths = ends - starts\n    text_vals = df[key].astype(int).astype(str) + \"%\"\n    text_display = [t if abs(w) > 7 else \"\" for t, w in zip(text_vals, widths, strict=False)]\n\n    fig.add_trace(\n        go.Bar(\n            y=df[\"question\"],\n            x=widths,\n            base=starts,\n            orientation=\"h\",\n            name=labels[key],\n            marker={\"color\": LIKERT_COLORS[key], \"line\": {\"color\": PAGE_BG, \"width\": 0.8}},\n            text=text_display,\n            textposition=\"inside\",\n            textfont={\"size\": 11, \"color\": TEXT_COLORS[key], \"family\": FONT_FAMILY},\n            customdata=df[key],\n            hovertemplate=\"%{y}<br>\" + labels[key] + \": %{customdata}%<extra></extra>\",\n        )\n    )\n\n# Net score annotations for top and bottom items\nbest_idx = df[\"net_agreement\"].idxmax()\nworst_idx = df[\"net_agreement\"].idxmin()\nbest_net = df.loc[best_idx, \"net_agreement\"]\nworst_net = df.loc[worst_idx, \"net_agreement\"]\nbest_end = pos_sa.iloc[best_idx]\nworst_start = neg_sd.iloc[worst_idx]\n\nfig.add_annotation(\n    x=best_end + 2,\n    y=df.loc[best_idx, \"question\"],\n    text=f\"<b>+{best_net}</b> net\",\n    showarrow=False,\n    font={\"size\": 11, \"color\": \"#4467A3\", \"family\": FONT_FAMILY},\n    xanchor=\"left\",\n)\nfig.add_annotation(\n    x=worst_start - 2,\n    y=df.loc[worst_idx, \"question\"],\n    text=f\"<b>{worst_net}</b> net\",\n    showarrow=False,\n    font={\"size\": 11, \"color\": \"#AE3030\", \"family\": FONT_FAMILY},\n    xanchor=\"right\",\n)\n\n# Title — 79 chars, scale down from default 16px\ntitle_text = \"Employee Engagement Survey · bar-diverging-likert · python · plotly · anyplot.ai\"\ntitle_n = len(title_text)\ntitle_size = round(16 * 67 / title_n) if title_n > 67 else 16\n\n# Style\nfig.update_layout(\n    autosize=False,\n    paper_bgcolor=PAGE_BG,\n    plot_bgcolor=PAGE_BG,\n    font={\"family\": FONT_FAMILY, \"color\": INK},\n    title={\n        \"text\": title_text,\n        \"subtitle\": {\n            \"text\": \"Tools and teamwork rank highest; career development lags across the organization\",\n            \"font\": {\"size\": 10, \"color\": INK_MUTED, \"family\": FONT_FAMILY},\n        },\n        \"font\": {\"size\": title_size, \"family\": FONT_FAMILY, \"color\": INK},\n        \"x\": 0.5,\n        \"xanchor\": \"center\",\n    },\n    xaxis={\n        \"title\": {\"text\": \"Response (%)\", \"font\": {\"size\": 12, \"color\": INK}},\n        \"tickfont\": {\"size\": 10, \"color\": INK_SOFT, \"family\": FONT_FAMILY},\n        \"ticksuffix\": \"%\",\n        \"zeroline\": True,\n        \"zerolinecolor\": INK_SOFT,\n        \"zerolinewidth\": 2,\n        \"showgrid\": True,\n        \"gridcolor\": GRID,\n        \"linecolor\": INK_SOFT,\n        \"showline\": False,\n        \"range\": [neg_sd.min() - 14, pos_sa.max() + 14],\n    },\n    yaxis={\n        \"tickfont\": {\"size\": 10, \"color\": INK_SOFT, \"family\": FONT_FAMILY},\n        \"automargin\": True,\n        \"linecolor\": INK_SOFT,\n        \"showline\": False,\n    },\n    barmode=\"overlay\",\n    legend={\n        \"orientation\": \"h\",\n        \"yanchor\": \"bottom\",\n        \"y\": -0.28,\n        \"xanchor\": \"center\",\n        \"x\": 0.5,\n        \"font\": {\"size\": 10, \"family\": FONT_FAMILY, \"color\": INK_SOFT},\n        \"bgcolor\": ELEVATED_BG,\n        \"bordercolor\": INK_SOFT,\n        \"borderwidth\": 1,\n        \"traceorder\": \"normal\",\n    },\n    margin={\"l\": 20, \"r\": 40, \"t\": 90, \"b\": 130},\n    bargap=0.25,\n)\n\n# Save\nfig.write_image(f\"plot-{THEME}.png\", width=800, height=450, scale=4)\nfig.write_html(f\"plot-{THEME}.html\", include_plotlyjs=\"cdn\")\n"}