{"spec_id":"sunburst-basic","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\nsunburst-basic: Basic Sunburst Chart\nLibrary: pygal 3.1.3 | Python 3.13.14\nQuality: 75/100 | Updated: 2026-07-26\n\"\"\"\n\nimport os\nimport re\n\nimport cairosvg\nimport pygal\nfrom pygal.style import Style\n\n\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# All 4 Imprint wedge hues are dark enough that pygal auto-picks BLACK\n# print_labels text (contrast-matched against the wedge fill it normally\n# sits on). But a few team labels land past the disk's outer edge on the\n# plain page background instead of on a wedge; there pygal's per-series\n# fill has no way to know it's off-wedge, so in dark theme black text goes\n# invisible on the near-black canvas. A thin light halo (independent of\n# THEME, since the black fill itself is theme-independent) keeps every\n# label legible everywhere it can land, without touching on-wedge contrast.\nLABEL_HALO = \"#F0EFE8\"\n\nIMPRINT = (\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\")\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=IMPRINT,\n    title_font_size=66,\n    label_font_size=56,\n    major_label_font_size=40,\n    legend_font_size=44,\n    value_font_size=36,\n    value_label_font_size=38,\n    stroke_width=3,\n)\n\n# Organizational budget breakdown (in $K). Each department is added as its\n# own pygal series (its teams are that series' datapoints), NOT as two\n# parallel \"Departments\" + \"Teams\" series. That distinction is what makes\n# pygal's multi-series \"dual\" Pie mode produce a genuine sunburst: dual mode\n# sizes each series' coarse inner wedge by that series' OWN value sum, and\n# subdivides its thin outer-ring arc by its own datapoints. With one series\n# per department, the inner disk becomes 4 wedges sized by true department\n# share, and each wedge's outer arc subdivides proportionally by that\n# department's teams. (A \"Departments\" + \"Teams\" series pair does not work:\n# both series always sum to the same grand total, so their coarse wedges\n# would always split 50/50 no matter what the real department shares are.)\n#\n# Every department lists exactly the same number of teams (3): pygal pads\n# shorter series with a trailing zero-value point to match the longest\n# series, which would silently steal the \"last datapoint\" slot the trick\n# below depends on.\n#\n# Within each department, the smallest team is listed last. pygal's dual\n# mode paints the coarse (department-level) wedge using the metadata of the\n# LAST datapoint drawn, so giving that entry `label=<department name>`\n# is what puts the department name on the big inner wedge. Its own outer\n# sliver stays label-free because pygal only prints a datapoint's label when\n# its angular span is at least ~17 degrees (0.3 radians), and each smallest\n# team is kept under that share by construction; its `tooltip` still\n# surfaces the real team name on hover.\ndepartments = [\n    (\n        \"Technology\",\n        [\n            (\"Backend Engineering\", \"#009E73\", 200),\n            (\"Frontend Engineering\", \"#40B88E\", 175),\n            (\"Data Science\", \"#7DD3AB\", 45),\n        ],\n    ),\n    (\"Business\", [(\"Sales\", \"#C475FD\", 160), (\"Finance\", \"#D195FE\", 105), (\"Legal\", \"#DEB5FE\", 45)]),\n    (\"Operations\", [(\"IT Support\", \"#4467A3\", 85), (\"Human Resources\", \"#7A93BD\", 45), (\"Facilities\", \"#A8BFDA\", 40)]),\n    (\n        \"Marketing\",\n        [(\"Brand Design\", \"#BD8233\", 45), (\"Digital Marketing\", \"#D1A668\", 30), (\"Content Marketing\", \"#E3C89C\", 25)],\n    ),\n]\n\nchart = pygal.Pie(\n    style=custom_style,\n    width=2400,\n    height=2400,\n    title=\"sunburst-basic · python · pygal · anyplot.ai\",\n    show_legend=False,\n    print_labels=True,\n    print_values=False,\n    margin=380,\n)\n\nfor dept_name, teams in departments:\n    points = [{\"value\": value, \"color\": color, \"label\": name, \"tooltip\": name} for name, color, value in teams[:-1]]\n    carrier_name, carrier_color, carrier_value = teams[-1]\n    points.append({\"value\": carrier_value, \"color\": carrier_color, \"label\": dept_name, \"tooltip\": carrier_name})\n    chart.add(dept_name, points)\n\nsvg = chart.render(is_unicode=True)\nchart_id = re.search(r'id=\"(chart-[^\"]+)\"', svg).group(1)\n# pygal's base stylesheet ships `#chart-id text, #chart-id tspan\n# {stroke:none!important}`, which would otherwise strip the halo above -\n# match its id-scoped specificity (plus !important) to win the cascade.\nsvg = svg.replace(\n    '<style type=\"text/css\">',\n    f'<style type=\"text/css\">#{chart_id} .text-overlay text.label '\n    f\"{{stroke:{LABEL_HALO} !important;stroke-width:2px !important;\"\n    f\"stroke-linejoin:round !important;}}\",\n    1,\n)\n\ncairosvg.svg2png(bytestring=svg.encode(\"utf-8\"), write_to=f\"plot-{THEME}.png\", dpi=72)\nwith open(f\"plot-{THEME}.html\", \"w\", encoding=\"utf-8\") as f:\n    f.write(svg)\n"}