{"spec_id":"venn-labeled-items","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nvenn-labeled-items: Chartgeist-Style Venn Diagram with Labeled Items\nLibrary: altair 6.2.2 | Python 3.13.14\nQuality: 85/100 | Updated: 2026-06-25\n\"\"\"\n\nimport importlib\nimport math\nimport os\nimport sys\nfrom collections import defaultdict\n\n\n# Drop script dir from sys.path so `altair` package resolves, not this file\nsys.path[:] = [p for p in sys.path if os.path.abspath(p or \".\") != os.path.dirname(os.path.abspath(__file__))]\nalt = importlib.import_module(\"altair\")\npd = importlib.import_module(\"pandas\")\n\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\n\n# Imprint palette — first series is always #009E73\nCOLOR_A = \"#009E73\"\nCOLOR_B = \"#C475FD\"\nCOLOR_C = \"#4467A3\"\n\n# Canvas: square 2400×2400 target (inner view 500×460, scale_factor=4.0)\nCANVAS_W = 500\nCANVAS_H = 460\nTARGET_W, TARGET_H = 2400, 2400\n\n# Symmetric three-circle Venn layout in a 500×460 coordinate space.\n# center_y=240 (below midpoint) shifts diagram toward canvas bottom, reducing empty lower space.\ncenter_x, center_y = 250.0, 240.0\nRADIUS = 90.0\nOFFSET = RADIUS / math.sqrt(3)  # ≈ 51.96\n\ncx_a = center_x - OFFSET * math.sin(math.radians(60))  # ≈ 205\ncy_a = center_y + OFFSET * math.cos(math.radians(60))  # ≈ 266\ncx_b = center_x + OFFSET * math.sin(math.radians(60))  # ≈ 295\ncy_b = cy_a\ncx_c = center_x  # 250\ncy_c = center_y - OFFSET  # ≈ 188\n\ndf_circles = pd.DataFrame(\n    [\n        {\"name\": \"Overhyped\", \"x\": cx_a, \"y\": cy_a, \"color\": COLOR_A},\n        {\"name\": \"Actually Useful\", \"x\": cx_b, \"y\": cy_b, \"color\": COLOR_B},\n        {\"name\": \"Secretly Loved\", \"x\": cx_c, \"y\": cy_c, \"color\": COLOR_C},\n    ]\n)\n\n# Category labels — placed outside each circle on the side away from the diagram centre\nlabel_a_x = cx_a + math.cos(math.radians(150)) * (RADIUS + 12)\nlabel_a_y = cy_a + math.sin(math.radians(150)) * (RADIUS + 12)\nlabel_b_x = cx_b + math.cos(math.radians(30)) * (RADIUS + 12)\nlabel_b_y = cy_b + math.sin(math.radians(30)) * (RADIUS + 12)\nlabel_c_x = cx_c\nlabel_c_y = cy_c - (RADIUS + 12)\n\nitems_raw = [\n    (\"NFTs\", \"A\"),\n    (\"Metaverse\", \"A\"),\n    (\"Spreadsheets\", \"B\"),\n    (\"USB Hubs\", \"B\"),\n    (\"Bubble Wrap\", \"C\"),\n    (\"Karaoke\", \"C\"),\n    (\"ChatGPT\", \"AB\"),\n    (\"Smartphones\", \"AB\"),\n    (\"Vinyl Records\", \"AC\"),\n    (\"Avocado Toast\", \"AC\"),\n    (\"Google Maps\", \"BC\"),\n    (\"Dolly Parton\", \"BC\"),\n    (\"Sourdough\", \"ABC\"),\n    (\"Coffee\", \"ABC\"),\n]\n\n# Geometric centroids of each Venn region, verified to lie in the correct zone.\n# AC/BC pushed outward (x±50) and downward (y-41) from original to separate from\n# the ABC centroid, eliminating the collision in the densely-packed centre cluster.\nzone_centers = {\n    \"A\": (155.0, 252.0),\n    \"B\": (345.0, 252.0),\n    \"C\": (250.0, 135.0),\n    \"AB\": (250.0, 290.0),\n    \"AC\": (200.0, 213.0),\n    \"BC\": (300.0, 213.0),\n    \"ABC\": (250.0, 254.0),\n}\n\nLINE_HEIGHT = 14.0\nzone_to_items = defaultdict(list)\nfor lbl, zone in items_raw:\n    zone_to_items[zone].append(lbl)\n\nrecords = []\nfor zone, labels in zone_to_items.items():\n    cx_zone, cy_zone = zone_centers[zone]\n    n = len(labels)\n    start_y = cy_zone + (n - 1) * LINE_HEIGHT / 2\n    for idx, label in enumerate(labels):\n        records.append({\"label\": label, \"zone\": zone, \"x\": cx_zone, \"y\": start_y - idx * LINE_HEIGHT})\ndf_items = pd.DataFrame(records)\n\ndomain_x = [0, CANVAS_W]\ndomain_y = [0, CANVAS_H]\n# circle_size: mark_point size is area in px² at view scale; radius = RADIUS px (1 data unit = 1 px here)\ncircle_size = math.pi * RADIUS * RADIUS\n\nfilled_circles = (\n    alt.Chart(df_circles)\n    .mark_point(shape=\"circle\", filled=True, opacity=0.30, strokeWidth=0)\n    .encode(\n        x=alt.X(\"x:Q\", scale=alt.Scale(domain=domain_x), axis=None),\n        y=alt.Y(\"y:Q\", scale=alt.Scale(domain=domain_y), axis=None),\n        color=alt.Color(\"color:N\", scale=None, legend=None),\n        size=alt.value(circle_size),\n    )\n)\n\noutline_circles = (\n    alt.Chart(df_circles)\n    .mark_point(shape=\"circle\", filled=False, strokeWidth=2.5, opacity=0.85)\n    .encode(\n        x=alt.X(\"x:Q\", scale=alt.Scale(domain=domain_x), axis=None),\n        y=alt.Y(\"y:Q\", scale=alt.Scale(domain=domain_y), axis=None),\n        stroke=alt.Color(\"color:N\", scale=None, legend=None),\n        size=alt.value(circle_size),\n    )\n)\n\nlabel_a = (\n    alt.Chart(pd.DataFrame([{\"x\": label_a_x, \"y\": label_a_y}]))\n    .mark_text(\n        text=\"Overhyped\",\n        fontSize=14,\n        fontWeight=\"bold\",\n        fontStyle=\"italic\",\n        font=\"serif\",\n        color=COLOR_A,\n        align=\"right\",\n        baseline=\"bottom\",\n    )\n    .encode(\n        x=alt.X(\"x:Q\", scale=alt.Scale(domain=domain_x), axis=None),\n        y=alt.Y(\"y:Q\", scale=alt.Scale(domain=domain_y), axis=None),\n    )\n)\n\nlabel_b = (\n    alt.Chart(pd.DataFrame([{\"x\": label_b_x, \"y\": label_b_y}]))\n    .mark_text(\n        text=\"Actually Useful\",\n        fontSize=14,\n        fontWeight=\"bold\",\n        fontStyle=\"italic\",\n        font=\"serif\",\n        color=COLOR_B,\n        align=\"left\",\n        baseline=\"bottom\",\n    )\n    .encode(\n        x=alt.X(\"x:Q\", scale=alt.Scale(domain=domain_x), axis=None),\n        y=alt.Y(\"y:Q\", scale=alt.Scale(domain=domain_y), axis=None),\n    )\n)\n\nlabel_c = (\n    alt.Chart(pd.DataFrame([{\"x\": label_c_x, \"y\": label_c_y}]))\n    .mark_text(\n        text=\"Secretly Loved\",\n        fontSize=14,\n        fontWeight=\"bold\",\n        fontStyle=\"italic\",\n        font=\"serif\",\n        color=COLOR_C,\n        align=\"center\",\n        baseline=\"top\",\n    )\n    .encode(\n        x=alt.X(\"x:Q\", scale=alt.Scale(domain=domain_x), axis=None),\n        y=alt.Y(\"y:Q\", scale=alt.Scale(domain=domain_y), axis=None),\n    )\n)\n\nitem_labels = (\n    alt.Chart(df_items)\n    .mark_text(fontSize=10, color=INK, fontWeight=\"normal\")\n    .encode(\n        x=alt.X(\"x:Q\", scale=alt.Scale(domain=domain_x), axis=None),\n        y=alt.Y(\"y:Q\", scale=alt.Scale(domain=domain_y), axis=None),\n        text=\"label:N\",\n    )\n)\n\nchart = (\n    alt.layer(filled_circles, outline_circles, label_a, label_b, label_c, item_labels)\n    .properties(\n        width=CANVAS_W,\n        height=CANVAS_H,\n        background=PAGE_BG,\n        title=alt.Title(\n            text=\"Pop Culture Vibes · venn-labeled-items · python · altair · anyplot.ai\",\n            subtitle=\"An opinionated three-circle taxonomy\",\n            fontSize=16,\n            subtitleFontSize=11,\n            color=INK,\n            subtitleColor=INK_SOFT,\n            anchor=\"middle\",\n            font=\"serif\",\n            subtitleFont=\"serif\",\n            subtitleFontStyle=\"italic\",\n            offset=16,\n        ),\n        padding={\"left\": 20, \"right\": 20, \"top\": 10, \"bottom\": 10},\n    )\n    .configure_view(fill=PAGE_BG, stroke=None)\n)\n\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\n\n# Pad to exact 2400×2400 — vl-convert may land slightly short; never crop\nfrom PIL import Image as PILImage\n\n\n_img = PILImage.open(f\"plot-{THEME}.png\").convert(\"RGB\")\n_w, _h = _img.size\nif _w > TARGET_W or _h > TARGET_H:\n    raise SystemExit(\n        f\"vl-convert produced {_w}×{_h}, exceeds target {TARGET_W}×{TARGET_H}. Shrink chart width/height and re-render.\"\n    )\nif _w < TARGET_W or _h < TARGET_H:\n    _canvas = PILImage.new(\"RGB\", (TARGET_W, TARGET_H), PAGE_BG)\n    _canvas.paste(_img, ((TARGET_W - _w) // 2, (TARGET_H - _h) // 2))\n    _canvas.save(f\"plot-{THEME}.png\")\n\nchart.save(f\"plot-{THEME}.html\")\n"}