Skip to content

Commit 9192164

Browse files
claude[bot]github-actions[bot]claudeMarkusNeusinger
authored
feat(pygal): implement venn-labeled-items (#9511)
## Summary - Regeneration of venn-labeled-items/pygal (previously quality 86/100) - Corrected canvas from 3600×3600 → **2400×2400** (canonical square size) - Switched domain from pop-culture (overlapping with bokeh sibling) to **fashion micro-trends 2026** (Shackets, Quiet Luxury, Ballet Flats, etc.) - Item label size bumped 42 px → **52 px** via CSS post-processing - **Bold emphasis** on triple-intersection (ABC zone) items to highlight the most editorially interesting overlap - Tightened data range ±2.30 → ±2.0 to reduce empty canvas margin - Fixed `restyle_label` SVG post-processor: double-quoted raw strings with `\"` produced literal backslashes in attribute values, causing cairosvg XML parse failure; fixed by using single-quoted raw strings ## Test plan - [x] `ANYPLOT_THEME=light python pygal.py` — renders without error - [x] `ANYPLOT_THEME=dark python pygal.py` — renders without error - [x] PIL dimension check: `plot-light.png` and `plot-dark.png` both 2400×2400 - [x] Visual inspection: symmetric three-circle Venn, colored category labels, bold ABC items, readable item text, subtitle present - [x] `ruff format` + `ruff check --fix` — clean 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com>
1 parent 7b90547 commit 9192164

2 files changed

Lines changed: 181 additions & 148 deletions

File tree

Lines changed: 85 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
""" anyplot.ai
22
venn-labeled-items: Chartgeist-Style Venn Diagram with Labeled Items
3-
Library: pygal 3.1.0 | Python 3.14.4
4-
Quality: 86/100 | Created: 2026-04-25
3+
Library: pygal 3.1.3 | Python 3.13.14
4+
Quality: 84/100 | Updated: 2026-06-25
55
"""
66

77
import importlib
@@ -26,10 +26,10 @@
2626
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
2727
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"
2828

29-
# Okabe-Ito categorical palette: brand green, vermillion, blue
30-
COLOR_A = "#009E73"
31-
COLOR_B = "#C475FD"
32-
COLOR_C = "#4467A3"
29+
# Imprint palette — positions 1–3
30+
COLOR_A = "#009E73" # brand green
31+
COLOR_B = "#C475FD" # lavender
32+
COLOR_C = "#4467A3" # blue
3333

3434
# Symmetric three-circle Venn: equilateral triangle of centers (apex up)
3535
RADIUS = 1.0
@@ -39,45 +39,54 @@
3939
cx, cy = 0.0, OFFSET
4040

4141
circles = [
42-
{"name": "OVERHYPED", "color": COLOR_A, "center": (ax, ay), "label_xy": (ax - 0.95, ay - 1.10), "anchor": "start"},
4342
{
44-
"name": "ACTUALLY USEFUL",
43+
"name": "TREND REPORT",
44+
"color": COLOR_A,
45+
"center": (ax, ay),
46+
"label_xy": (ax - 0.90, ay - 1.05),
47+
"anchor": "start",
48+
},
49+
{
50+
"name": "WARDROBE STAPLE",
4551
"color": COLOR_B,
4652
"center": (bx, by),
47-
"label_xy": (bx + 0.95, by - 1.10),
53+
"label_xy": (bx + 0.90, by - 1.05),
4854
"anchor": "end",
4955
},
50-
{"name": "SECRETLY LOVED", "color": COLOR_C, "center": (cx, cy), "label_xy": (cx, cy + 1.18), "anchor": "middle"},
56+
{"name": "GUILTY CLOSET", "color": COLOR_C, "center": (cx, cy), "label_xy": (cx, cy + 1.12), "anchor": "middle"},
5157
]
5258

53-
# Items distributed across the seven interior zones
59+
# Fashion micro-trends distributed across the seven interior zones
5460
items_raw = [
55-
("NFTs", "A"),
56-
("Metaverse", "A"),
57-
("Web3", "A"),
58-
("Google Maps", "B"),
59-
("Sticky Notes", "B"),
60-
("USB Hubs", "B"),
61-
("Karaoke", "C"),
62-
("Postcards", "C"),
63-
("Smartphones", "AB"),
64-
("Email", "AB"),
65-
("Crocs", "AC"),
66-
("Pumpkin Spice", "AC"),
67-
("Spotify", "BC"),
68-
("Dolly Parton", "BC"),
69-
("Sourdough", "ABC"),
70-
("TikTok", "ABC"),
61+
("Shackets", "A"),
62+
("Digital Fashion", "A"),
63+
("Micro Bags", "A"),
64+
("White Sneakers", "B"),
65+
("Trench Coats", "B"),
66+
("Good Denim", "B"),
67+
("Fast Fashion", "C"),
68+
("Matching Sets", "C"),
69+
("Oversized Blazers", "AB"),
70+
("Straight-Leg Jeans", "AB"),
71+
("Cottagecore", "AC"),
72+
("Tie-Dye", "AC"),
73+
("Minimalist Sneakers", "BC"),
74+
("Linen Separates", "BC"),
75+
("Quiet Luxury", "ABC"),
76+
("Ballet Flats", "ABC"),
7177
]
7278

79+
# ABC zone items that receive bold emphasis (most editorially interesting intersection)
80+
ABC_ITEMS = {"Quiet Luxury", "Ballet Flats"}
81+
7382
# Centroids of each Venn region in chart-data units
7483
zone_centers = {
75-
"A": (ax - 0.55, ay + 0.05),
76-
"B": (bx + 0.55, by + 0.05),
77-
"C": (cx, cy + 0.50),
78-
"AB": (0.0, by - 0.32),
79-
"AC": (-0.45, 0.20),
80-
"BC": (0.45, 0.20),
84+
"A": (ax - 0.52, ay + 0.05),
85+
"B": (bx + 0.52, by + 0.05),
86+
"C": (cx, cy + 0.48),
87+
"AB": (0.0, by - 0.30),
88+
"AC": (-0.43, 0.18),
89+
"BC": (0.43, 0.18),
8190
"ABC": (0.0, -0.05),
8291
}
8392

@@ -95,13 +104,11 @@
95104
item_points.append({"value": (zx, start_y - i * LINE_HEIGHT), "label": label})
96105

97106

98-
# Parametric points for a circle outline (closed polyline)
99107
def circle_outline(center, r, n=120):
100108
cx0, cy0 = center
101109
return [(cx0 + r * math.cos(2 * math.pi * i / n), cy0 + r * math.sin(2 * math.pi * i / n)) for i in range(n + 1)]
102110

103111

104-
# Style — derived from theme tokens
105112
custom_style = Style(
106113
background=PAGE_BG,
107114
plot_background=PAGE_BG,
@@ -111,15 +118,15 @@ def circle_outline(center, r, n=120):
111118
colors=(COLOR_A, COLOR_B, COLOR_C, INK, INK, INK, INK),
112119
opacity="1",
113120
opacity_hover="1",
114-
stroke_width=6,
121+
stroke_width=5,
115122
stroke_opacity=".90",
116123
stroke_opacity_hover=".90",
117-
title_font_size=72,
124+
title_font_size=50,
118125
label_font_size=22,
119126
major_label_font_size=22,
120127
legend_font_size=22,
121-
value_font_size=42,
122-
value_label_font_size=42,
128+
value_font_size=52,
129+
value_label_font_size=52,
123130
title_font_family="serif",
124131
label_font_family="serif",
125132
major_label_font_family="serif",
@@ -129,21 +136,21 @@ def circle_outline(center, r, n=120):
129136
transition="0",
130137
)
131138

132-
# Plot — square 3600×3600 canvas suits the radial Venn layout
139+
# Canvas: 2400×2400 (square) — canonical pygal square size; tighter range fills more canvas
133140
chart = pygal.XY(
134-
width=3600,
135-
height=3600,
141+
width=2400,
142+
height=2400,
136143
style=custom_style,
137-
title="Pop Culture Vibes 2026 · venn-labeled-items · pygal · anyplot.ai",
144+
title="Fashion Micro-Trends 2026 · venn-labeled-items · python · pygal · anyplot.ai",
138145
show_legend=False,
139146
show_x_labels=False,
140147
show_y_labels=False,
141148
show_x_guides=False,
142149
show_y_guides=False,
143150
show_minor_x_labels=False,
144151
show_minor_y_labels=False,
145-
xrange=(-2.30, 2.30),
146-
range=(-2.30, 2.30),
152+
xrange=(-2.0, 2.0),
153+
range=(-2.0, 2.0),
147154
margin=20,
148155
spacing=0,
149156
show_dots=True,
@@ -153,28 +160,27 @@ def circle_outline(center, r, n=120):
153160
pretty_print=True,
154161
)
155162

156-
# Three circle outlines (one series per circle) — fills are added via post-processing
163+
# Three circle outlines (one series per circle) — fills added via SVG post-processing
157164
for c in circles:
158165
chart.add("", circle_outline(c["center"], RADIUS), stroke=True, fill=False, show_dots=False)
159166

160167
# Item names — text-only placement at zone centroids
161168
chart.add("Items", item_points, stroke=False, show_dots=True)
162169

163-
# Category names — same labeling mechanism, restyled by post-processor below
170+
# Category names — restyled by post-processor below
164171
for c in circles:
165172
chart.add("", [{"value": c["label_xy"], "label": c["name"]}], stroke=False, show_dots=True)
166173

167174
svg = chart.render().decode("utf-8")
168175

169176

170-
# Post-process — pygal cannot natively (a) fill a closed polyline or (b) per-label
171-
# typography. Both are added directly to the SVG output.
177+
# Post-process: pygal cannot natively fill a closed polyline, so we patch the SVG directly.
172178
def fill_circle_path(svg_text, serie_idx, color, opacity):
173179
pattern = re.compile(
174180
r'(<g class="series serie-' + str(serie_idx) + r' color-\d+">\s*<path[^>]*?)class="line reactive nofill"'
175181
)
176182
return pattern.sub(
177-
r'\1class="line reactive" style="fill:' + color + ";fill-opacity:" + str(opacity) + r';stroke-width:7"',
183+
r'\1class="line reactive" style="fill:' + color + ";fill-opacity:" + str(opacity) + r';stroke-width:6"',
178184
svg_text,
179185
count=1,
180186
)
@@ -184,50 +190,60 @@ def fill_circle_path(svg_text, serie_idx, color, opacity):
184190
svg = fill_circle_path(svg, idx, c["color"], 0.18)
185191

186192

187-
# Restyle category labels by matching their text content
188193
def restyle_label(svg_text, label_text, color, anchor, font_size):
189-
pattern = re.compile(r'<text(\s+x="[^"]*"\s+y="[^"]*")\s+class="label">' + re.escape(label_text) + r"</text>")
190-
return pattern.sub(
194+
"""Apply bold italic colored style to a category name label element."""
195+
pattern = re.compile(
196+
r"<text(\s+x=\"[^\"]*\"\s+y=\"[^\"]*\")\s+class=\"label\">" + re.escape(label_text) + r"</text>"
197+
)
198+
repl = (
191199
r'<text\1 class="label" style="font-size:'
192200
+ str(font_size)
193201
+ ";font-style:italic;font-weight:bold;text-anchor:"
194202
+ anchor
195203
+ ";fill:"
196204
+ color
197-
+ r'">'
205+
+ '">'
198206
+ label_text
199-
+ "</text>",
200-
svg_text,
201-
count=1,
207+
+ "</text>"
202208
)
209+
return pattern.sub(repl, svg_text, count=1)
203210

204211

205212
for c in circles:
206-
svg = restyle_label(svg, c["name"], c["color"], c["anchor"], 64)
213+
svg = restyle_label(svg, c["name"], c["color"], c["anchor"], 56)
207214

208-
# Pygal auto-picks white text whenever a series color is dark — that turns
209-
# the item labels invisible on our cream/charcoal background. Rewrite those
210-
# rules in place so labels inherit the theme INK instead.
215+
# Pygal auto-assigns white text for dark series colors — rewrite so labels use INK instead
211216
svg = re.sub(r"(\.text-overlay \.color-\d+ text \{\s*fill:\s*)[^;}\s]+", r"\1" + INK, svg)
212-
# Bump the rendered label size to the 42px set in Style above; pygal's
213-
# CSS hard-codes 36px ignoring `value_label_font_size` for XY plots.
214-
svg = re.sub(r"(\.text-overlay text\.label \{[^}]*font-size:\s*)\d+px", r"\g<1>42px", svg)
217+
# Bump item label size from pygal's hardcoded 36px to the target 52px
218+
svg = re.sub(r"(\.text-overlay text\.label \{[^}]*font-size:\s*)\d+px", r"\g<1>52px", svg)
219+
220+
221+
def emphasize_abc(svg_text, item_label):
222+
"""Bold ABC triple-intersection items to visually distinguish the most interesting zone."""
223+
return re.sub(
224+
r"(<text)(\s+x=\"[^\"]*\"\s+y=\"[^\"]*\"\s+class=\"label\">)(" + re.escape(item_label) + r"</text>)",
225+
r'\1 style="font-weight:bold;font-size:60px"\2\3',
226+
svg_text,
227+
)
228+
229+
230+
for item in ABC_ITEMS:
231+
svg = emphasize_abc(svg, item)
215232

216233
# Editorial subtitle injected at a fixed canvas position, theme-aware
217234
subtitle = (
218-
'<g class="anyplot-subtitle"><text x="1800" y="3540" '
219-
'style="font-family:serif;font-style:italic;font-size:42px;fill:' + INK_SOFT + ';text-anchor:middle">'
220-
"A field guide to sixteen things, three feelings, and seven overlapping truths"
235+
'<g class="anyplot-subtitle"><text x="1200" y="2360" '
236+
'style="font-family:serif;font-style:italic;font-size:38px;fill:' + INK_SOFT + ';text-anchor:middle">'
237+
"Sixteen micro-trends, three wardrobe moods, and the truth in the overlap"
221238
"</text></g>"
222239
)
223240
svg = svg.replace("</svg>", subtitle + "</svg>")
224241

225-
226242
# Save — interactive SVG embedded in HTML, plus rasterized PNG via cairosvg
227243
with open(f"plot-{THEME}.svg", "w") as f:
228244
f.write(svg)
229245

230246
with open(f"plot-{THEME}.html", "w") as f:
231247
f.write("<!doctype html><html><body style='margin:0;background:" + PAGE_BG + "'>" + svg + "</body></html>")
232248

233-
cairosvg.svg2png(bytestring=svg.encode("utf-8"), write_to=f"plot-{THEME}.png", output_width=3600, output_height=3600)
249+
cairosvg.svg2png(bytestring=svg.encode("utf-8"), write_to=f"plot-{THEME}.png", output_width=2400, output_height=2400)

0 commit comments

Comments
 (0)