{"spec_id":"donut-basic","library":"altair","language":"python","code":"\"\"\" anyplot.ai\ndonut-basic: Basic Donut Chart\nLibrary: altair 6.2.2 | Python 3.13.14\nQuality: 91/100 | Updated: 2026-06-25\n\"\"\"\n\nimport importlib\nimport os\nimport sys\n\n\n# Drop script directory from sys.path so the `altair` package resolves, not this file\nsys.path[:] = [p for p in sys.path if os.path.abspath(p or \".\") != os.path.dirname(os.path.abspath(__file__))]\nalt = importlib.import_module(\"altair\")\npd = importlib.import_module(\"pandas\")\nfrom PIL import Image\n\n\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-5 for 5 departments\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\"]\n\ndata = pd.DataFrame(\n    {\"category\": [\"Engineering\", \"Marketing\", \"Operations\", \"Sales\", \"Support\"], \"value\": [480, 210, 155, 125, 55]}\n)\n\ntotal = int(data[\"value\"].sum())\ndata[\"percentage\"] = (data[\"value\"] / total * 100).round(1)\ndata[\"label\"] = data[\"percentage\"].astype(str) + \"%\"\n\n# Hover selection: dims non-hovered segments to draw the eye (Vega-Lite interactive feature)\nhighlight = alt.selection_point(on=\"mouseover\", empty=False, fields=[\"category\"])\n\n# Main ring — all segments at standard radius\narc = (\n    alt.Chart(data)\n    .mark_arc(innerRadius=100, outerRadius=200, stroke=PAGE_BG, strokeWidth=3)\n    .encode(\n        theta=alt.Theta(field=\"value\", type=\"quantitative\", stack=True),\n        color=alt.Color(\n            field=\"category\",\n            type=\"nominal\",\n            scale=alt.Scale(domain=list(data[\"category\"]), range=IMPRINT),\n            legend=alt.Legend(\n                title=\"Department\", titleFontSize=10, labelFontSize=10, orient=\"right\", symbolSize=100, padding=8\n            ),\n        ),\n        opacity=alt.condition(highlight, alt.value(1.0), alt.value(0.75)),\n        order=alt.Order(field=\"value\", sort=\"descending\"),\n        tooltip=[\n            alt.Tooltip(\"category:N\", title=\"Department\"),\n            alt.Tooltip(\"value:Q\", title=\"Budget ($K)\"),\n            alt.Tooltip(\"percentage:Q\", title=\"Share (%)\", format=\".1f\"),\n        ],\n    )\n    .add_params(highlight)\n)\n\n# Focal emphasis: Engineering (dominant at 46.8%) gets a wider outer ring\nengineering_data = data[data[\"category\"] == \"Engineering\"]\narc_emphasis = (\n    alt.Chart(engineering_data)\n    .mark_arc(innerRadius=100, outerRadius=218, stroke=PAGE_BG, strokeWidth=3)\n    .encode(\n        theta=alt.Theta(field=\"value\", type=\"quantitative\", stack=True),\n        color=alt.Color(\n            field=\"category\", type=\"nominal\", scale=alt.Scale(domain=list(data[\"category\"]), range=IMPRINT), legend=None\n        ),\n        order=alt.Order(field=\"value\", sort=\"descending\"),\n    )\n)\n\nlabels = (\n    alt.Chart(data)\n    .mark_text(radius=150, fontSize=14, fontWeight=\"bold\", color=\"#FFFFFF\")\n    .encode(\n        theta=alt.Theta(field=\"value\", type=\"quantitative\", stack=True),\n        order=alt.Order(field=\"value\", sort=\"descending\"),\n        text=alt.Text(\"label:N\"),\n    )\n)\n\ncenter = (\n    alt.Chart(pd.DataFrame({\"line\": [\"Total budget\", f\"${total:,}K\"], \"y\": [0.08, -0.08]}))\n    .mark_text(fontSize=20, fontWeight=\"bold\", color=INK, align=\"center\")\n    .encode(y=alt.Y(\"y:Q\", axis=None, scale=alt.Scale(domain=[-1, 1])), text=\"line:N\")\n)\n\ntitle_str = \"donut-basic · python · altair · anyplot.ai\"\n\nfinal_chart = (\n    alt.layer(arc, arc_emphasis, labels, center)\n    .properties(\n        width=480,\n        height=500,\n        background=PAGE_BG,\n        title=alt.Title(text=title_str, fontSize=16, anchor=\"middle\", color=INK, offset=10),\n        padding={\"left\": 0, \"right\": 0, \"top\": 0, \"bottom\": 0},\n    )\n    .configure_view(fill=PAGE_BG, stroke=None)\n    .configure_legend(fillColor=ELEVATED_BG, strokeColor=INK_SOFT, labelColor=INK_SOFT, titleColor=INK, cornerRadius=6)\n)\n\nfinal_chart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\n\n# Pad to exact 2400×2400 target (square — symmetric circular chart)\nTW, TH = 2400, 2400\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\nfinal_chart.save(f\"plot-{THEME}.html\")\n"}