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