Skip to content

Commit e2ce04a

Browse files
feat(pygal): implement donut-basic (#5342)
## Implementation: `donut-basic` - python/pygal Implements the **python/pygal** version of `donut-basic`. **File:** `plots/donut-basic/implementations/python/pygal.py` **Parent Issue:** #733 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/24874228294)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 3793f9b commit e2ce04a

2 files changed

Lines changed: 273 additions & 152 deletions

File tree

Lines changed: 117 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,139 @@
1-
""" pyplots.ai
1+
""" anyplot.ai
22
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
55
"""
66

7-
import pygal
8-
from pygal.style import Style
7+
import os
8+
import re
9+
import sys
910

1011

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"
1437

15-
# Custom style for 4800x2700 px
1638
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,
2352
title_font_size=72,
2453
label_font_size=48,
2554
major_label_font_size=42,
26-
legend_font_size=42,
27-
value_font_size=36,
55+
legend_font_size=48,
2856
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",
2962
)
3063

31-
# Create donut chart
64+
# Square canvas — 3600 × 3600 works best for circular charts at ~13 MP
3265
chart = pygal.Pie(
33-
width=4800,
34-
height=2700,
66+
width=3600,
67+
height=3600,
3568
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",
3871
show_legend=True,
3972
legend_at_bottom=True,
40-
legend_box_size=36,
73+
legend_at_bottom_columns=len(categories),
74+
legend_box_size=52,
75+
margin=40,
4176
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,
4481
)
4582

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))
4994

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+
"""
53137

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

Comments
 (0)