Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 85 additions & 69 deletions plots/venn-labeled-items/implementations/python/pygal.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
""" anyplot.ai
venn-labeled-items: Chartgeist-Style Venn Diagram with Labeled Items
Library: pygal 3.1.0 | Python 3.14.4
Quality: 86/100 | Created: 2026-04-25
Library: pygal 3.1.3 | Python 3.13.14
Quality: 84/100 | Updated: 2026-06-25
"""

import importlib
Expand All @@ -26,10 +26,10 @@
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"

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

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

circles = [
{"name": "OVERHYPED", "color": COLOR_A, "center": (ax, ay), "label_xy": (ax - 0.95, ay - 1.10), "anchor": "start"},
{
"name": "ACTUALLY USEFUL",
"name": "TREND REPORT",
"color": COLOR_A,
"center": (ax, ay),
"label_xy": (ax - 0.90, ay - 1.05),
"anchor": "start",
},
{
"name": "WARDROBE STAPLE",
"color": COLOR_B,
"center": (bx, by),
"label_xy": (bx + 0.95, by - 1.10),
"label_xy": (bx + 0.90, by - 1.05),
"anchor": "end",
},
{"name": "SECRETLY LOVED", "color": COLOR_C, "center": (cx, cy), "label_xy": (cx, cy + 1.18), "anchor": "middle"},
{"name": "GUILTY CLOSET", "color": COLOR_C, "center": (cx, cy), "label_xy": (cx, cy + 1.12), "anchor": "middle"},
]

# Items distributed across the seven interior zones
# Fashion micro-trends distributed across the seven interior zones
items_raw = [
("NFTs", "A"),
("Metaverse", "A"),
("Web3", "A"),
("Google Maps", "B"),
("Sticky Notes", "B"),
("USB Hubs", "B"),
("Karaoke", "C"),
("Postcards", "C"),
("Smartphones", "AB"),
("Email", "AB"),
("Crocs", "AC"),
("Pumpkin Spice", "AC"),
("Spotify", "BC"),
("Dolly Parton", "BC"),
("Sourdough", "ABC"),
("TikTok", "ABC"),
("Shackets", "A"),
("Digital Fashion", "A"),
("Micro Bags", "A"),
("White Sneakers", "B"),
("Trench Coats", "B"),
("Good Denim", "B"),
("Fast Fashion", "C"),
("Matching Sets", "C"),
("Oversized Blazers", "AB"),
("Straight-Leg Jeans", "AB"),
("Cottagecore", "AC"),
("Tie-Dye", "AC"),
("Minimalist Sneakers", "BC"),
("Linen Separates", "BC"),
("Quiet Luxury", "ABC"),
("Ballet Flats", "ABC"),
]

# ABC zone items that receive bold emphasis (most editorially interesting intersection)
ABC_ITEMS = {"Quiet Luxury", "Ballet Flats"}

# Centroids of each Venn region in chart-data units
zone_centers = {
"A": (ax - 0.55, ay + 0.05),
"B": (bx + 0.55, by + 0.05),
"C": (cx, cy + 0.50),
"AB": (0.0, by - 0.32),
"AC": (-0.45, 0.20),
"BC": (0.45, 0.20),
"A": (ax - 0.52, ay + 0.05),
"B": (bx + 0.52, by + 0.05),
"C": (cx, cy + 0.48),
"AB": (0.0, by - 0.30),
"AC": (-0.43, 0.18),
"BC": (0.43, 0.18),
"ABC": (0.0, -0.05),
}

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


# Parametric points for a circle outline (closed polyline)
def circle_outline(center, r, n=120):
cx0, cy0 = center
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)]


# Style — derived from theme tokens
custom_style = Style(
background=PAGE_BG,
plot_background=PAGE_BG,
Expand All @@ -111,15 +118,15 @@ def circle_outline(center, r, n=120):
colors=(COLOR_A, COLOR_B, COLOR_C, INK, INK, INK, INK),
opacity="1",
opacity_hover="1",
stroke_width=6,
stroke_width=5,
stroke_opacity=".90",
stroke_opacity_hover=".90",
title_font_size=72,
title_font_size=50,
label_font_size=22,
major_label_font_size=22,
legend_font_size=22,
value_font_size=42,
value_label_font_size=42,
value_font_size=52,
value_label_font_size=52,
title_font_family="serif",
label_font_family="serif",
major_label_font_family="serif",
Expand All @@ -129,21 +136,21 @@ def circle_outline(center, r, n=120):
transition="0",
)

# Plot — square 3600×3600 canvas suits the radial Venn layout
# Canvas: 2400×2400 (square) — canonical pygal square size; tighter range fills more canvas
chart = pygal.XY(
width=3600,
height=3600,
width=2400,
height=2400,
style=custom_style,
title="Pop Culture Vibes 2026 · venn-labeled-items · pygal · anyplot.ai",
title="Fashion Micro-Trends 2026 · venn-labeled-items · python · pygal · anyplot.ai",
show_legend=False,
show_x_labels=False,
show_y_labels=False,
show_x_guides=False,
show_y_guides=False,
show_minor_x_labels=False,
show_minor_y_labels=False,
xrange=(-2.30, 2.30),
range=(-2.30, 2.30),
xrange=(-2.0, 2.0),
range=(-2.0, 2.0),
margin=20,
spacing=0,
show_dots=True,
Expand All @@ -153,28 +160,27 @@ def circle_outline(center, r, n=120):
pretty_print=True,
)

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

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

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

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


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


# Restyle category labels by matching their text content
def restyle_label(svg_text, label_text, color, anchor, font_size):
pattern = re.compile(r'<text(\s+x="[^"]*"\s+y="[^"]*")\s+class="label">' + re.escape(label_text) + r"</text>")
return pattern.sub(
"""Apply bold italic colored style to a category name label element."""
pattern = re.compile(
r"<text(\s+x=\"[^\"]*\"\s+y=\"[^\"]*\")\s+class=\"label\">" + re.escape(label_text) + r"</text>"
)
repl = (
r'<text\1 class="label" style="font-size:'
+ str(font_size)
+ ";font-style:italic;font-weight:bold;text-anchor:"
+ anchor
+ ";fill:"
+ color
+ r'">'
+ '">'
+ label_text
+ "</text>",
svg_text,
count=1,
+ "</text>"
)
return pattern.sub(repl, svg_text, count=1)


for c in circles:
svg = restyle_label(svg, c["name"], c["color"], c["anchor"], 64)
svg = restyle_label(svg, c["name"], c["color"], c["anchor"], 56)

# Pygal auto-picks white text whenever a series color is dark — that turns
# the item labels invisible on our cream/charcoal background. Rewrite those
# rules in place so labels inherit the theme INK instead.
# Pygal auto-assigns white text for dark series colors — rewrite so labels use INK instead
svg = re.sub(r"(\.text-overlay \.color-\d+ text \{\s*fill:\s*)[^;}\s]+", r"\1" + INK, svg)
# Bump the rendered label size to the 42px set in Style above; pygal's
# CSS hard-codes 36px ignoring `value_label_font_size` for XY plots.
svg = re.sub(r"(\.text-overlay text\.label \{[^}]*font-size:\s*)\d+px", r"\g<1>42px", svg)
# Bump item label size from pygal's hardcoded 36px to the target 52px
svg = re.sub(r"(\.text-overlay text\.label \{[^}]*font-size:\s*)\d+px", r"\g<1>52px", svg)


def emphasize_abc(svg_text, item_label):
"""Bold ABC triple-intersection items to visually distinguish the most interesting zone."""
return re.sub(
r"(<text)(\s+x=\"[^\"]*\"\s+y=\"[^\"]*\"\s+class=\"label\">)(" + re.escape(item_label) + r"</text>)",
r'\1 style="font-weight:bold;font-size:60px"\2\3',
svg_text,
)


for item in ABC_ITEMS:
svg = emphasize_abc(svg, item)

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


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

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

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