Skip to content

Commit 989558f

Browse files
feat(altair): implement venn-labeled-items (#5378)
## Implementation: `venn-labeled-items` - python/altair Implements the **python/altair** version of `venn-labeled-items`. **File:** `plots/venn-labeled-items/implementations/python/altair.py` **Parent Issue:** #5364 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/24923425795)* --------- 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 e944da1 commit 989558f

2 files changed

Lines changed: 461 additions & 0 deletions

File tree

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
""" anyplot.ai
2+
venn-labeled-items: Chartgeist-Style Venn Diagram with Labeled Items
3+
Library: altair 6.1.0 | Python 3.14.4
4+
Quality: 86/100 | Created: 2026-04-25
5+
"""
6+
7+
import importlib
8+
import math
9+
import os
10+
import sys
11+
from collections import defaultdict
12+
13+
14+
# Drop script directory from sys.path so the `altair` package resolves, not this file
15+
sys.path[:] = [p for p in sys.path if os.path.abspath(p or ".") != os.path.dirname(os.path.abspath(__file__))]
16+
alt = importlib.import_module("altair")
17+
pd = importlib.import_module("pandas")
18+
19+
20+
# Theme tokens
21+
THEME = os.getenv("ANYPLOT_THEME", "light")
22+
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
23+
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
24+
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
25+
26+
# Okabe-Ito categorical palette: brand green, vermillion, blue
27+
COLOR_A = "#009E73"
28+
COLOR_B = "#D55E00"
29+
COLOR_C = "#0072B2"
30+
31+
# Symmetric three-circle Venn layout on a 1200x1200 square canvas
32+
CANVAS = 1200
33+
center_x, center_y = CANVAS / 2, CANVAS / 2
34+
RADIUS = 240
35+
OFFSET = RADIUS / math.sqrt(3)
36+
37+
cx_a = center_x - OFFSET * math.sin(math.radians(60))
38+
cy_a = center_y + OFFSET * math.cos(math.radians(60))
39+
cx_b = center_x + OFFSET * math.sin(math.radians(60))
40+
cy_b = center_y + OFFSET * math.cos(math.radians(60))
41+
cx_c = center_x
42+
cy_c = center_y - OFFSET
43+
44+
df_circles = pd.DataFrame(
45+
[
46+
{"name": "Overhyped", "x": cx_a, "y": cy_a, "color": COLOR_A},
47+
{"name": "Actually Useful", "x": cx_b, "y": cy_b, "color": COLOR_B},
48+
{"name": "Secretly Loved", "x": cx_c, "y": cy_c, "color": COLOR_C},
49+
]
50+
)
51+
52+
# Category labels: outside each circle, on the side away from the diagram centroid
53+
label_a_x = cx_a + math.cos(math.radians(150)) * (RADIUS + 30)
54+
label_a_y = cy_a + math.sin(math.radians(150)) * (RADIUS + 30)
55+
label_b_x = cx_b + math.cos(math.radians(30)) * (RADIUS + 30)
56+
label_b_y = cy_b + math.sin(math.radians(30)) * (RADIUS + 30)
57+
label_c_x = cx_c
58+
label_c_y = cy_c - (RADIUS + 30)
59+
60+
# Items distributed across the seven Venn zones
61+
items_raw = [
62+
("NFTs", "A"),
63+
("Metaverse", "A"),
64+
("Spreadsheets", "B"),
65+
("USB Hubs", "B"),
66+
("Bubble Wrap", "C"),
67+
("Karaoke", "C"),
68+
("ChatGPT", "AB"),
69+
("Smartphones", "AB"),
70+
("Vinyl Records", "AC"),
71+
("Avocado Toast", "AC"),
72+
("Google Maps", "BC"),
73+
("Dolly Parton", "BC"),
74+
("Sourdough", "ABC"),
75+
("Coffee", "ABC"),
76+
]
77+
78+
# Geometric centroids of each Venn region (chosen for clear in-zone placement)
79+
zone_centers = {
80+
"A": (390, 715),
81+
"B": (810, 715),
82+
"C": (600, 357),
83+
"AB": (600, 745),
84+
"AC": (480, 540),
85+
"BC": (720, 540),
86+
"ABC": (600, 600),
87+
}
88+
89+
zone_to_items = defaultdict(list)
90+
for label, zone in items_raw:
91+
zone_to_items[zone].append(label)
92+
93+
LINE_HEIGHT = 30
94+
records = []
95+
for zone, labels in zone_to_items.items():
96+
cx_zone, cy_zone = zone_centers[zone]
97+
n = len(labels)
98+
start_y = cy_zone + (n - 1) * LINE_HEIGHT / 2
99+
for idx, label in enumerate(labels):
100+
records.append({"label": label, "zone": zone, "x": cx_zone, "y": start_y - idx * LINE_HEIGHT})
101+
df_items = pd.DataFrame(records)
102+
103+
# Plot
104+
domain_x = [0, CANVAS]
105+
domain_y = [0, CANVAS]
106+
circle_size = math.pi * RADIUS * RADIUS
107+
108+
filled_circles = (
109+
alt.Chart(df_circles)
110+
.mark_point(shape="circle", filled=True, opacity=0.22, strokeWidth=0)
111+
.encode(
112+
x=alt.X("x:Q", scale=alt.Scale(domain=domain_x), axis=None),
113+
y=alt.Y("y:Q", scale=alt.Scale(domain=domain_y), axis=None),
114+
color=alt.Color("color:N", scale=None, legend=None),
115+
size=alt.value(circle_size),
116+
)
117+
)
118+
119+
outline_circles = (
120+
alt.Chart(df_circles)
121+
.mark_point(shape="circle", filled=False, strokeWidth=2.5, opacity=0.85)
122+
.encode(
123+
x=alt.X("x:Q", scale=alt.Scale(domain=domain_x), axis=None),
124+
y=alt.Y("y:Q", scale=alt.Scale(domain=domain_y), axis=None),
125+
stroke=alt.Color("color:N", scale=None, legend=None),
126+
size=alt.value(circle_size),
127+
)
128+
)
129+
130+
label_a = (
131+
alt.Chart(pd.DataFrame([{"x": label_a_x, "y": label_a_y}]))
132+
.mark_text(
133+
text="Overhyped",
134+
fontSize=30,
135+
fontWeight="bold",
136+
fontStyle="italic",
137+
font="serif",
138+
color=COLOR_A,
139+
align="right",
140+
baseline="bottom",
141+
)
142+
.encode(
143+
x=alt.X("x:Q", scale=alt.Scale(domain=domain_x), axis=None),
144+
y=alt.Y("y:Q", scale=alt.Scale(domain=domain_y), axis=None),
145+
)
146+
)
147+
148+
label_b = (
149+
alt.Chart(pd.DataFrame([{"x": label_b_x, "y": label_b_y}]))
150+
.mark_text(
151+
text="Actually Useful",
152+
fontSize=30,
153+
fontWeight="bold",
154+
fontStyle="italic",
155+
font="serif",
156+
color=COLOR_B,
157+
align="left",
158+
baseline="bottom",
159+
)
160+
.encode(
161+
x=alt.X("x:Q", scale=alt.Scale(domain=domain_x), axis=None),
162+
y=alt.Y("y:Q", scale=alt.Scale(domain=domain_y), axis=None),
163+
)
164+
)
165+
166+
label_c = (
167+
alt.Chart(pd.DataFrame([{"x": label_c_x, "y": label_c_y}]))
168+
.mark_text(
169+
text="Secretly Loved",
170+
fontSize=30,
171+
fontWeight="bold",
172+
fontStyle="italic",
173+
font="serif",
174+
color=COLOR_C,
175+
align="center",
176+
baseline="top",
177+
)
178+
.encode(
179+
x=alt.X("x:Q", scale=alt.Scale(domain=domain_x), axis=None),
180+
y=alt.Y("y:Q", scale=alt.Scale(domain=domain_y), axis=None),
181+
)
182+
)
183+
184+
item_labels = (
185+
alt.Chart(df_items)
186+
.mark_text(fontSize=20, color=INK, fontWeight="normal")
187+
.encode(
188+
x=alt.X("x:Q", scale=alt.Scale(domain=domain_x), axis=None),
189+
y=alt.Y("y:Q", scale=alt.Scale(domain=domain_y), axis=None),
190+
text="label:N",
191+
)
192+
)
193+
194+
chart = (
195+
alt.layer(filled_circles, outline_circles, label_a, label_b, label_c, item_labels)
196+
.properties(
197+
width=CANVAS,
198+
height=CANVAS,
199+
background=PAGE_BG,
200+
title=alt.Title(
201+
text="Pop Culture Vibes · venn-labeled-items · altair · anyplot.ai",
202+
subtitle="An opinionated three-circle taxonomy",
203+
fontSize=28,
204+
subtitleFontSize=18,
205+
color=INK,
206+
subtitleColor=INK_SOFT,
207+
anchor="middle",
208+
font="serif",
209+
subtitleFont="serif",
210+
subtitleFontStyle="italic",
211+
offset=24,
212+
),
213+
padding={"left": 30, "right": 30, "top": 20, "bottom": 20},
214+
)
215+
.configure_view(fill=PAGE_BG, stroke=None)
216+
)
217+
218+
chart.save(f"plot-{THEME}.png", scale_factor=3.0)
219+
chart.save(f"plot-{THEME}.html")

0 commit comments

Comments
 (0)