|
1 | | -""" pyplots.ai |
| 1 | +""" anyplot.ai |
2 | 2 | donut-basic: Basic Donut Chart |
3 | | -Library: pygal 3.1.0 | Python 3.13.11 |
4 | | -Quality: 91/100 | Created: 2025-12-23 |
| 3 | +Library: pygal 3.1.0 | Python 3.14.4 |
| 4 | +Quality: 88/100 | Updated: 2026-04-24 |
5 | 5 | """ |
6 | 6 |
|
7 | | -import pygal |
8 | | -from pygal.style import Style |
| 7 | +import os |
| 8 | +import re |
| 9 | +import sys |
9 | 10 |
|
10 | 11 |
|
11 | | -# Data - Budget allocation by department |
12 | | -categories = ["Engineering", "Marketing", "Sales", "Operations", "HR"] |
13 | | -values = [35, 25, 20, 12, 8] |
| 12 | +# Script filename shadows the installed `pygal` package when run as `python pygal.py`; |
| 13 | +# dropping the script directory from sys.path lets the real package resolve. |
| 14 | +sys.path.pop(0) |
| 15 | + |
| 16 | +import cairosvg # noqa: E402 |
| 17 | +import pygal # noqa: E402 |
| 18 | +from pygal.style import Style # noqa: E402 |
| 19 | + |
| 20 | + |
| 21 | +# Theme tokens |
| 22 | +THEME = os.getenv("ANYPLOT_THEME", "light") |
| 23 | +PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17" |
| 24 | +INK = "#1A1A17" if THEME == "light" else "#F0EFE8" |
| 25 | +INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0" |
| 26 | +INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F" |
| 27 | + |
| 28 | +# Okabe-Ito categorical palette — first series is always brand green |
| 29 | +OKABE_ITO = ("#009E73", "#D55E00", "#0072B2", "#CC79A7", "#E69F00", "#56B4E9", "#F0E442") |
| 30 | + |
| 31 | +# Data — Annual budget allocation by department (USD thousands) |
| 32 | +categories = ["Engineering", "Operations", "Marketing", "Sales", "Support"] |
| 33 | +values = [480, 210, 155, 125, 55] |
| 34 | +total = sum(values) |
| 35 | + |
| 36 | +font = "DejaVu Sans, Helvetica, Arial, sans-serif" |
14 | 37 |
|
15 | | -# Custom style for 4800x2700 px |
16 | 38 | custom_style = Style( |
17 | | - background="white", |
18 | | - plot_background="white", |
19 | | - foreground="#333333", |
20 | | - foreground_strong="#333333", |
21 | | - foreground_subtle="#666666", |
22 | | - colors=("#306998", "#FFD43B", "#4B8BBE", "#FFE873", "#646464"), |
| 39 | + background=PAGE_BG, |
| 40 | + plot_background=PAGE_BG, |
| 41 | + foreground=INK_SOFT, |
| 42 | + foreground_strong=INK, |
| 43 | + foreground_subtle=INK_MUTED, |
| 44 | + colors=OKABE_ITO, |
| 45 | + font_family=font, |
| 46 | + title_font_family=font, |
| 47 | + label_font_family=font, |
| 48 | + major_label_font_family=font, |
| 49 | + legend_font_family=font, |
| 50 | + tooltip_font_family=font, |
| 51 | + value_font_family=font, |
23 | 52 | title_font_size=72, |
24 | 53 | label_font_size=48, |
25 | 54 | major_label_font_size=42, |
26 | | - legend_font_size=42, |
27 | | - value_font_size=36, |
| 55 | + legend_font_size=48, |
28 | 56 | tooltip_font_size=36, |
| 57 | + value_font_size=52, |
| 58 | + value_colors=["#F0EFE8"] * len(categories), |
| 59 | + opacity=1, |
| 60 | + opacity_hover=0.85, |
| 61 | + transition="200ms ease-in", |
29 | 62 | ) |
30 | 63 |
|
31 | | -# Create donut chart |
| 64 | +# Square canvas — 3600 × 3600 works best for circular charts at ~13 MP |
32 | 65 | chart = pygal.Pie( |
33 | | - width=4800, |
34 | | - height=2700, |
| 66 | + width=3600, |
| 67 | + height=3600, |
35 | 68 | style=custom_style, |
36 | | - inner_radius=0.6, # Creates donut hole |
37 | | - title="donut-basic · pygal · pyplots.ai", |
| 69 | + inner_radius=0.58, |
| 70 | + title="Budget by Department · donut-basic · pygal · anyplot.ai", |
38 | 71 | show_legend=True, |
39 | 72 | legend_at_bottom=True, |
40 | | - legend_box_size=36, |
| 73 | + legend_at_bottom_columns=len(categories), |
| 74 | + legend_box_size=52, |
| 75 | + margin=40, |
41 | 76 | print_values=True, |
42 | | - print_values_position="call", |
43 | | - value_formatter=lambda x: f"{x}%", |
| 77 | + print_values_position="inside", |
| 78 | + print_labels=False, |
| 79 | + value_formatter=lambda v: f"{v / total * 100:.1f}%", |
| 80 | + truncate_legend=-1, |
44 | 81 | ) |
45 | 82 |
|
46 | | -# Add data with percentage labels |
47 | | -for category, value in zip(categories, values, strict=True): |
48 | | - chart.add(category, value) |
| 83 | +for cat, val in zip(categories, values, strict=True): |
| 84 | + chart.add(cat, val) |
| 85 | + |
| 86 | +# Render to SVG, then inject center-label text (pygal has no native donut-hole label). |
| 87 | +svg_text = chart.render(is_unicode=True) |
| 88 | + |
| 89 | +# Locate plot-group translate and the first slice's top-of-arc anchor so we can |
| 90 | +# compute the exact donut center in SVG coordinates. |
| 91 | +plot_match = re.search(r'<g transform="translate\(([\d.\-]+),\s*([\d.\-]+)\)"\s+class="plot"', svg_text) |
| 92 | +plot_x = float(plot_match.group(1)) |
| 93 | +plot_y = float(plot_match.group(2)) |
49 | 94 |
|
50 | | -# Save outputs |
51 | | -chart.render_to_file("plot.svg") |
52 | | -chart.render_to_png("plot.png") |
| 95 | +slice_match = re.search(r'<path d="M([\d.\-]+)\s+([\d.\-]+)\s+A([\d.\-]+)', svg_text) |
| 96 | +top_x = float(slice_match.group(1)) |
| 97 | +top_y = float(slice_match.group(2)) |
| 98 | +outer_r = float(slice_match.group(3)) |
| 99 | + |
| 100 | +cx = plot_x + top_x |
| 101 | +cy = plot_y + top_y + outer_r |
| 102 | + |
| 103 | +# Center metric: "Total budget" (secondary label, above) + formatted total |
| 104 | +# (primary headline, below). Baselines spaced to avoid visual collision at 180px. |
| 105 | +center_text = ( |
| 106 | + f'<g class="center-metric">' |
| 107 | + f'<text x="{cx:.2f}" y="{cy - 80:.2f}" text-anchor="middle" ' |
| 108 | + f'fill="{INK_SOFT}" style="font-size:72px;letter-spacing:3px;' |
| 109 | + f'text-transform:uppercase;font-family:{font};">Total Budget</text>' |
| 110 | + f'<text x="{cx:.2f}" y="{cy + 120:.2f}" text-anchor="middle" ' |
| 111 | + f'fill="{INK}" style="font-size:220px;font-weight:700;font-family:{font};">${total:,}K</text>' |
| 112 | + f"</g>" |
| 113 | +) |
| 114 | + |
| 115 | +output_svg = svg_text.replace("</svg>", f"{center_text}</svg>") |
| 116 | + |
| 117 | +cairosvg.svg2png(bytestring=output_svg.encode("utf-8"), write_to=f"plot-{THEME}.png", output_width=3600) |
| 118 | + |
| 119 | +html_content = f"""<!DOCTYPE html> |
| 120 | +<html> |
| 121 | +<head> |
| 122 | + <meta charset="utf-8"> |
| 123 | + <title>donut-basic · pygal · anyplot.ai</title> |
| 124 | + <style> |
| 125 | + body {{ margin: 0; background: {PAGE_BG}; display: flex; |
| 126 | + justify-content: center; align-items: center; min-height: 100vh; }} |
| 127 | + .chart {{ max-width: 100%; height: auto; }} |
| 128 | + </style> |
| 129 | +</head> |
| 130 | +<body> |
| 131 | + <figure class="chart"> |
| 132 | + {output_svg} |
| 133 | + </figure> |
| 134 | +</body> |
| 135 | +</html> |
| 136 | +""" |
53 | 137 |
|
54 | | -# Also save HTML for interactive version |
55 | | -chart.render_to_file("plot.html") |
| 138 | +with open(f"plot-{THEME}.html", "w", encoding="utf-8") as f: |
| 139 | + f.write(html_content) |
0 commit comments