{"spec_id":"bar-diverging-likert","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\nbar-diverging-likert: Likert Scale Diverging Bar Chart\nLibrary: pygal 3.1.0 | Python 3.13.13\nQuality: 86/100 | Updated: 2026-06-01\n\"\"\"\n\nimport importlib.util\nimport os\nimport re\nimport sys\nfrom xml.etree import ElementTree as ET\n\nimport cairosvg\n\n\n# Prevent self-import: this file is named pygal.py, so import the installed package explicitly\n_spec = importlib.util.find_spec(\"pygal\")\nif _spec and _spec.origin != __file__:\n    import pygal\n    from pygal.style import Style\nelse:\n    _cwd = os.getcwd()\n    sys.path = [p for p in sys.path if os.path.abspath(p) != _cwd]\n    try:\n        import pygal\n        from pygal.style import Style\n    finally:\n        sys.path.insert(0, _cwd)\n\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n\ndef _lerp_hex(c0, c1, t):\n    r0, g0, b0 = (int(c0[i : i + 2], 16) for i in (1, 3, 5))\n    r1, g1, b1 = (int(c1[i : i + 2], 16) for i in (1, 3, 5))\n    return \"#{:02X}{:02X}{:02X}\".format(\n        int(round(r0 + (r1 - r0) * t)), int(round(g0 + (g1 - g0) * t)), int(round(b0 + (b1 - b0) * t))\n    )\n\n\n# Imprint palette diverging scale for 5 Likert responses\n# Use fixed light-value of INK_MUTED so intermediate colors are theme-invariant\nINK_MUTED_FIXED = \"#6B6A63\"\nSTRONGLY_DISAGREE_C = \"#AE3030\"\n# t=0.35 (less blending) improves visual separation from the neutral gray\nDISAGREE_C = _lerp_hex(\"#AE3030\", INK_MUTED_FIXED, 0.35)\nNEUTRAL_C = INK_MUTED_FIXED\nAGREE_C = _lerp_hex(INK_MUTED_FIXED, \"#4467A3\", 0.5)\nSTRONGLY_AGREE_C = \"#4467A3\"\n\n# Colors in series-addition order: N_left, D, SD, N_right, A, SA\nCHART_COLORS = (NEUTRAL_C, DISAGREE_C, STRONGLY_DISAGREE_C, NEUTRAL_C, AGREE_C, STRONGLY_AGREE_C)\n\n\n# Data — employee engagement survey (8 questions, 5-point Likert scale)\nquestions = [\n    \"I feel valued at work\",\n    \"My manager provides clear direction\",\n    \"I have opportunities for growth\",\n    \"Work-life balance is supported\",\n    \"I receive fair compensation\",\n    \"Team collaboration is effective\",\n    \"Company vision is inspiring\",\n    \"My contributions are recognized\",\n]\n\n# Response percentages: (Strongly Disagree, Disagree, Neutral, Agree, Strongly Agree)\nresponses = [\n    (5, 10, 15, 40, 30),\n    (8, 15, 20, 35, 22),\n    (12, 22, 18, 30, 18),\n    (6, 12, 22, 38, 22),\n    (22, 30, 18, 20, 10),\n    (3, 7, 10, 42, 38),\n    (18, 28, 22, 22, 10),\n    (7, 14, 16, 38, 25),\n]\n\n# Sort ascending by net agreement (most positive renders at top in horizontal chart)\nnet_scores = [(r[3] + r[4]) - (r[0] + r[1]) for r in responses]\norder = sorted(range(len(questions)), key=lambda i: net_scores[i])\nquestions = [questions[i] for i in order]\nresponses = [responses[i] for i in order]\n\n\n# Style — title scaled for length (79 chars vs 67-char baseline → font size 56)\ntitle = \"Employee Engagement Survey · bar-diverging-likert · python · pygal · anyplot.ai\"\nn = len(title)\ntitle_font_size = max(44, round(66 * 67 / n)) if n > 67 else 66\n\ncustom_style = Style(\n    background=PAGE_BG,\n    plot_background=PAGE_BG,\n    foreground=INK,\n    foreground_strong=INK,\n    foreground_subtle=INK_MUTED,\n    colors=CHART_COLORS,\n    title_font_size=title_font_size,\n    label_font_size=56,\n    major_label_font_size=44,\n    legend_font_size=44,\n    value_font_size=30,\n    stroke_width=2.5,\n)\n\n\n# Build diverging series (neutral split evenly at center)\nneutral_left = [-r[2] / 2 for r in responses]\ndisagree_vals = [-r[1] for r in responses]\nstrongly_disagree_vals = [-r[0] for r in responses]\nneutral_right = [r[2] / 2 for r in responses]\nagree_vals = [r[3] for r in responses]\nstrongly_agree_vals = [r[4] for r in responses]\n\n\n# Plot\nchart = pygal.HorizontalStackedBar(\n    width=3200,\n    height=1800,\n    style=custom_style,\n    title=title,\n    show_legend=True,\n    legend_at_bottom=True,\n    legend_at_bottom_columns=5,\n    show_x_guides=False,\n    show_y_guides=False,\n    print_values=True,\n    print_values_position=\"center\",\n    value_formatter=lambda x: f\"{abs(x):.0f}%\" if abs(x) >= 8 else \"\",\n    x_title=\"Response Percentage (%)\",\n    margin=50,\n    spacing=15,\n    truncate_label=-1,\n    truncate_legend=-1,\n)\n\n# Add series in stacking order (center outward); legend shows N, D, SD, A, SA\nchart.add(\"Neutral\", neutral_left)\nchart.add(\"Disagree\", disagree_vals)\nchart.add(\"Strongly Disagree\", strongly_disagree_vals)\nchart.add(None, neutral_right)  # right-half neutral hidden from legend\nchart.add(\"Agree\", agree_vals)\nchart.add(\"Strongly Agree\", strongly_agree_vals)\n\nchart.x_labels = questions\n\n\n# SVG post-processing: remove residual guide lines and reorder legend to SD, D, N, A, SA\nsvg_content = chart.render().decode(\"utf-8\")\n\nsvg_content = re.sub(r'<path [^>]*class=\"(?:major )?(?:axis major )?guide line\"[^/]*/>', \"\", svg_content)\n\nSVG_NS = \"http://www.w3.org/2000/svg\"\nET.register_namespace(\"\", SVG_NS)\nET.register_namespace(\"xlink\", \"http://www.w3.org/1999/xlink\")\nroot = ET.fromstring(svg_content)\n\n# Swap x-positions of serie-0 (Neutral) and serie-2 (Strongly Disagree) legend items\n# so legend reads left-to-right: SD | D | N | A | SA\nserie_0 = root.find(f'.//{{{SVG_NS}}}g[@id=\"activate-serie-0\"]')\nserie_2 = root.find(f'.//{{{SVG_NS}}}g[@id=\"activate-serie-2\"]')\nif serie_0 is not None and serie_2 is not None:\n    for child_0, child_2 in zip(serie_0, serie_2, strict=False):\n        x0, x2 = child_0.get(\"x\"), child_2.get(\"x\")\n        if x0 is not None and x2 is not None:\n            child_0.set(\"x\", x2)\n            child_2.set(\"x\", x0)\n\nsvg_content = ET.tostring(root, encoding=\"unicode\")\n\n\n# Save\ncairosvg.svg2png(bytestring=svg_content.encode(\"utf-8\"), write_to=f\"plot-{THEME}.png\")\n\nwith open(f\"plot-{THEME}.html\", \"w\") as f:\n    f.write(svg_content)\n"}