{"spec_id":"donut-basic","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\ndonut-basic: Basic Donut Chart\nLibrary: pygal 3.1.3 | Python 3.13.14\nQuality: 86/100 | Updated: 2026-06-25\n\"\"\"\n\nimport os\nimport re\nimport sys\n\n\n# Script filename shadows the installed `pygal` package when run as `python pygal.py`;\n# dropping the script directory from sys.path lets the real package resolve.\nsys.path.pop(0)\n\nimport cairosvg\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 — first series is always brand green\nIMPRINT = (\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\")\n\n# Data — model portfolio allocation (USD thousands)\ncategories = [\"US Stocks\", \"Intl Stocks\", \"Bonds\", \"Real Estate\", \"Cash\"]\nvalues = [450, 200, 200, 80, 70]\ntotal = sum(values)\n\nfont = \"DejaVu Sans, Helvetica, Arial, sans-serif\"\n\ntitle = \"Portfolio Allocation · donut-basic · python · pygal · anyplot.ai\"\nn = len(title)\nratio = 67 / n if n > 67 else 1.0\ntitle_font_size = max(44, round(66 * ratio))\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    font_family=font,\n    title_font_family=font,\n    label_font_family=font,\n    major_label_font_family=font,\n    legend_font_family=font,\n    tooltip_font_family=font,\n    value_font_family=font,\n    title_font_size=title_font_size,\n    label_font_size=56,\n    major_label_font_size=44,\n    legend_font_size=44,\n    tooltip_font_size=36,\n    value_font_size=44,\n    value_colors=[INK] * len(categories),\n    opacity=1,\n    opacity_hover=0.85,\n    transition=\"200ms ease-in\",\n)\n\n# Square canvas — 2400 × 2400 px (canonical pygal square)\nchart = pygal.Pie(\n    width=2400,\n    height=2400,\n    style=custom_style,\n    inner_radius=0.58,\n    title=title,\n    show_legend=True,\n    legend_at_bottom=True,\n    legend_at_bottom_columns=len(categories),\n    legend_box_size=44,\n    margin=40,\n    print_values=True,\n    print_values_position=\"inside\",\n    print_labels=False,\n    value_formatter=lambda v: f\"{v / total * 100:.1f}%\",\n    truncate_legend=-1,\n)\n\nfor cat, val in zip(categories, values, strict=True):\n    chart.add(cat, val)\n\n# Render to SVG, then inject center-label text (pygal has no native donut-hole label).\nsvg_text = chart.render(is_unicode=True)\n\n# Locate donut center from SVG; fall back to fixed geometric calculation.\nplot_match = re.search(r'<g transform=\"translate\\(([\\d.\\-]+),\\s*([\\d.\\-]+)\\)\"\\s+class=\"plot\"', svg_text)\nslice_match = re.search(r'<path d=\"M([\\d.\\-]+)\\s+([\\d.\\-]+)\\s+A([\\d.\\-]+)', svg_text)\n\nif plot_match and slice_match:\n    plot_x = float(plot_match.group(1))\n    plot_y = float(plot_match.group(2))\n    top_x = float(slice_match.group(1))\n    top_y = float(slice_match.group(2))\n    outer_r = float(slice_match.group(3))\n    cx = plot_x + top_x\n    cy = plot_y + top_y + outer_r\nelse:\n    # Fixed geometric center for 2400×2400 canvas: title ~280px top, legend ~160px bottom\n    cx = 2400 / 2\n    cy = 280 + (2400 - 280 - 160) / 2\n\n# Center metric: muted label + divider + bold headline value.\nlabel_y = cy - 110\ndivider_y = cy - 30\nvalue_y = cy + 110\ndivider_half = 90\n\ncenter_text = (\n    f'<g class=\"center-metric\">'\n    f'<text x=\"{cx:.2f}\" y=\"{label_y:.2f}\" text-anchor=\"middle\" '\n    f'fill=\"{INK_SOFT}\" style=\"font-size:50px;letter-spacing:4px;'\n    f'text-transform:uppercase;font-family:{font};\">Portfolio</text>'\n    f'<line x1=\"{cx - divider_half:.2f}\" y1=\"{divider_y:.2f}\" '\n    f'x2=\"{cx + divider_half:.2f}\" y2=\"{divider_y:.2f}\" '\n    f'stroke=\"{INK_MUTED}\" stroke-width=\"1.5\" opacity=\"0.5\"/>'\n    f'<text x=\"{cx:.2f}\" y=\"{value_y:.2f}\" text-anchor=\"middle\" '\n    f'fill=\"{INK}\" style=\"font-size:148px;font-weight:700;font-family:{font};\">${total:,}K</text>'\n    f\"</g>\"\n)\n\noutput_svg = svg_text.replace(\"</svg>\", f\"{center_text}</svg>\")\n\ncairosvg.svg2png(bytestring=output_svg.encode(\"utf-8\"), write_to=f\"plot-{THEME}.png\", output_width=2400)\n\nhtml_content = f\"\"\"<!DOCTYPE html>\n<html>\n<head>\n    <meta charset=\"utf-8\">\n    <title>donut-basic · python · pygal · anyplot.ai</title>\n    <style>\n        body {{ margin: 0; background: {PAGE_BG}; display: flex;\n                justify-content: center; align-items: center; min-height: 100vh; }}\n        .chart {{ max-width: 100%; height: auto; }}\n    </style>\n</head>\n<body>\n    <figure class=\"chart\">\n        {output_svg}\n    </figure>\n</body>\n</html>\n\"\"\"\n\nwith open(f\"plot-{THEME}.html\", \"w\", encoding=\"utf-8\") as f:\n    f.write(html_content)\n"}