{"spec_id":"dumbbell-basic","library":"altair","language":"python","code":"\"\"\" anyplot.ai\ndumbbell-basic: Basic Dumbbell Chart\nLibrary: altair 6.2.2 | Python 3.13.14\nQuality: 92/100 | Updated: 2026-06-30\n\"\"\"\n\nimport os\n\nimport altair as alt\nimport pandas as pd\nfrom PIL import Image\n\n\n# Theme-adaptive chrome\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\"\n\n# Imprint palette positions 1 and 2\nCOLOR_BEFORE = \"#009E73\"\nCOLOR_AFTER = \"#C475FD\"\n\n# Data — Employee satisfaction scores before and after policy changes\ndata = pd.DataFrame(\n    {\n        \"category\": [\n            \"Customer Support\",\n            \"Engineering\",\n            \"Sales\",\n            \"Marketing\",\n            \"HR\",\n            \"Finance\",\n            \"Operations\",\n            \"Legal\",\n            \"R&D\",\n            \"IT\",\n        ],\n        \"Before\": [52, 61, 58, 65, 72, 68, 55, 74, 63, 59],\n        \"After\": [78, 82, 76, 81, 85, 79, 64, 81, 69, 64],\n    }\n)\ndata[\"difference\"] = data[\"After\"] - data[\"Before\"]\ndata = data.sort_values(\"difference\", ascending=True).reset_index(drop=True)\n\n# Long-form data for the two dot series\ndots_data = pd.melt(\n    data, id_vars=[\"category\", \"difference\"], value_vars=[\"Before\", \"After\"], var_name=\"period\", value_name=\"score\"\n)\n\ntitle = \"Employee Satisfaction · dumbbell-basic · python · altair · anyplot.ai\"\nn = len(title)\ntitle_fontsize = round(16 * (67 / n)) if n > 67 else 16\n\nx_scale = alt.Scale(domain=[45, 92])\ny_sort = alt.EncodingSortField(field=\"difference\", order=\"ascending\")\n\n# Connecting lines (theme-adaptive subtle ink)\nlines = (\n    alt.Chart(data)\n    .mark_rule(strokeWidth=3, color=INK_SOFT, opacity=0.45)\n    .encode(y=alt.Y(\"category:N\", sort=y_sort, title=None), x=alt.X(\"Before:Q\", scale=x_scale), x2=alt.X2(\"After:Q\"))\n)\n\n# Dots for Before / After values\ndots = (\n    alt.Chart(dots_data)\n    .mark_circle(size=350, opacity=1.0, stroke=PAGE_BG, strokeWidth=2)\n    .encode(\n        y=alt.Y(\"category:N\", sort=y_sort, title=None),\n        x=alt.X(\"score:Q\", scale=x_scale, title=\"Employee Satisfaction Score (%)\"),\n        color=alt.Color(\n            \"period:N\",\n            scale=alt.Scale(domain=[\"Before\", \"After\"], range=[COLOR_BEFORE, COLOR_AFTER]),\n            legend=alt.Legend(title=\"Policy Change\", labelFontSize=10, titleFontSize=12),\n        ),\n        tooltip=[\"category:N\", \"period:N\", \"score:Q\"],\n    )\n)\n\n# Difference labels via transform_calculate — shows the gain at a glance\ndiff_labels = (\n    alt.Chart(data)\n    .transform_calculate(label=\"'+' + toString(datum.difference) + ' pts'\")\n    .mark_text(align=\"left\", dx=8, fontSize=11, fontWeight=\"bold\", clip=False)\n    .encode(\n        y=alt.Y(\"category:N\", sort=y_sort, title=None),\n        x=alt.X(\"After:Q\", scale=x_scale),\n        text=alt.Text(\"label:N\"),\n        color=alt.value(INK_SOFT),\n    )\n)\n\nchart = (\n    (lines + dots + diff_labels)\n    .properties(\n        width=576,\n        height=374,\n        title=alt.Title(title, fontSize=title_fontsize, color=INK, anchor=\"start\", offset=16),\n        background=PAGE_BG,\n    )\n    .configure_view(fill=PAGE_BG, stroke=None)\n    .configure_axis(\n        labelFontSize=10,\n        titleFontSize=12,\n        domainColor=INK_SOFT,\n        domainOpacity=0,\n        tickColor=INK_SOFT,\n        gridColor=INK,\n        gridOpacity=0.10,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n    )\n    .configure_title(color=INK)\n    .configure_legend(fillColor=ELEVATED_BG, strokeColor=INK_SOFT, labelColor=INK_SOFT, titleColor=INK, padding=10)\n)\n\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\n\n# Pad to exact 3200×1800 canvas (vl-convert pads outside width/height)\nTW, TH = 3200, 1800\n_img = Image.open(f\"plot-{THEME}.png\").convert(\"RGB\")\n_w, _h = _img.size\nif _w > TW or _h > TH:\n    raise SystemExit(\n        f\"altair vl-convert produced {_w}×{_h}, exceeds target {TW}×{TH}. \"\n        f\"Shrink chart .properties(width=, height=) values and re-render.\"\n    )\nif _w < TW or _h < TH:\n    _canvas = Image.new(\"RGB\", (TW, TH), PAGE_BG)\n    _canvas.paste(_img, ((TW - _w) // 2, (TH - _h) // 2))\n    _canvas.save(f\"plot-{THEME}.png\")\n\nchart.save(f\"plot-{THEME}.html\")\n"}