11""" anyplot.ai
22venn-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
77import importlib
2626INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
2727INK_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)
3535RADIUS = 1.0
3939cx , cy = 0.0 , OFFSET
4040
4141circles = [
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
5460items_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
7483zone_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
95104 item_points .append ({"value" : (zx , start_y - i * LINE_HEIGHT ), "label" : label })
96105
97106
98- # Parametric points for a circle outline (closed polyline)
99107def 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
105112custom_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
133140chart = 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
157164for 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
161168chart .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
164171for c in circles :
165172 chart .add ("" , [{"value" : c ["label_xy" ], "label" : c ["name" ]}], stroke = False , show_dots = True )
166173
167174svg = 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.
172178def 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
188193def 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
205212for 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
211216svg = 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
217234subtitle = (
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)
223240svg = svg .replace ("</svg>" , subtitle + "</svg>" )
224241
225-
226242# Save — interactive SVG embedded in HTML, plus rasterized PNG via cairosvg
227243with open (f"plot-{ THEME } .svg" , "w" ) as f :
228244 f .write (svg )
229245
230246with 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