{"spec_id":"dumbbell-basic","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\ndumbbell-basic: Basic Dumbbell Chart\nLibrary: pygal 3.1.3 | Python 3.13.14\nQuality: 88/100 | Updated: 2026-06-30\n\"\"\"\n\nimport os\nimport sys\nfrom pathlib import Path\n\n\n# Remove script dir from sys.path to avoid name collision with the pygal package\n_script_dir = str(Path(__file__).parent)\nsys.path = [p for p in sys.path if p != _script_dir]\n\nimport pygal\nfrom pygal.style import Style\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_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Imprint categorical palette — positions 1 and 2 for two-series dumbbell\nBEFORE = \"#009E73\"  # Imprint position 1 — brand green, always first series\nAFTER = \"#C475FD\"  # Imprint position 2 — lavender\nCONNECTOR = INK_SOFT  # theme-adaptive neutral connector line\nLOSS_COLOR = \"#AE3030\"  # Imprint matte red — semantic anchor for regression/bad outcomes\n\n# Data — employee satisfaction scores before and after new workplace policy\ncategories = [\n    \"Engineering\",\n    \"Sales\",\n    \"Marketing\",\n    \"Customer Support\",\n    \"Finance\",\n    \"Human Resources\",\n    \"Operations\",\n    \"Product\",\n    \"Legal\",\n]\nbefore = [62, 71, 58, 45, 68, 52, 64, 73, 70]\nafter = [78, 82, 75, 69, 74, 71, 79, 85, 67]\n\n# Sort by improvement (largest gain at top; Legal regression ends up at bottom)\ndata = sorted(zip(categories, before, after, strict=True), key=lambda x: x[2] - x[1], reverse=True)\ncategories = [d[0] for d in data]\nbefore = [d[1] for d in data]\nafter = [d[2] for d in data]\nn = len(categories)\n\n# Top row = biggest improvement; y=n at top, y=1 at bottom\ny_positions = list(range(n, 0, -1))\n\n# Category labels include delta for at-a-glance storytelling (e.g. \"Customer Support (+24)\")\ny_labels = [\n    {\"label\": f\"{cat}  ({a - b:+d})\", \"value\": pos}\n    for cat, b, a, pos in zip(categories, before, after, y_positions, strict=True)\n]\n\n# Title — include language token; scale font size for title length\ntitle = \"Employee Satisfaction · dumbbell-basic · python · pygal · anyplot.ai\"\nn_chars = len(title)  # 70 chars\nratio = 67 / n_chars if n_chars > 67 else 1.0\ntitle_font_size = max(44, round(66 * ratio))  # ≈ 63\n\n# Color sequence: neutral connectors (red for negative deltas), then dot colors\nconnector_colors = tuple(LOSS_COLOR if a < b else CONNECTOR for b, a in zip(before, after, strict=True))\ncolors_tuple = connector_colors + (BEFORE, AFTER)\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=colors_tuple,\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=36,\n    stroke_width=2.5,\n    opacity=1.0,\n    opacity_hover=0.85,\n)\n\nchart = pygal.XY(\n    width=3200,\n    height=1800,\n    style=custom_style,\n    title=title,\n    x_title=\"Satisfaction Score (out of 100)\",\n    y_title=\"Department\",\n    show_legend=True,\n    legend_at_bottom=True,\n    legend_at_bottom_columns=2,\n    legend_box_size=44,\n    margin=80,\n    margin_bottom=20,\n    show_x_guides=False,\n    show_y_guides=False,\n    xrange=(35, 95),\n    range=(0.5, n + 0.5),\n    y_labels=y_labels,\n    truncate_legend=-1,\n    truncate_label=-1,\n    stroke=False,\n)\n\n# Connector lines drawn first so they sit underneath the dots\nfor b, a, pos in zip(before, after, y_positions, strict=True):\n    chart.add(None, [(b, pos), (a, pos)], stroke=True, show_dots=False, stroke_style={\"width\": 5, \"linecap\": \"round\"})\n\n# Before dots — Imprint brand green (position 1)\nbefore_points = [\n    {\"value\": (b, pos), \"label\": f\"{cat}: {b}\"} for cat, b, pos in zip(categories, before, y_positions, strict=True)\n]\nchart.add(\"Before policy change\", before_points, stroke=False, dots_size=24)\n\n# After dots — Imprint lavender (position 2)\nafter_points = [\n    {\"value\": (a, pos), \"label\": f\"{cat}: {a}\"} for cat, a, pos in zip(categories, after, y_positions, strict=True)\n]\nchart.add(\"After policy change\", after_points, stroke=False, dots_size=24)\n\n# Save PNG and interactive HTML\nchart.render_to_png(f\"plot-{THEME}.png\")\nwith open(f\"plot-{THEME}.html\", \"wb\") as f:\n    f.write(chart.render())\n"}