{"spec_id":"venn-labeled-items","library":"plotly","language":"python","code":"\"\"\" anyplot.ai\nvenn-labeled-items: Chartgeist-Style Venn Diagram with Labeled Items\nLibrary: plotly 6.8.0 | Python 3.13.14\nQuality: 84/100 | Updated: 2026-06-25\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport plotly.graph_objects as go\n\n\n# Theme tokens\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\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Imprint categorical palette — first series is brand green\nCIRCLE_COLORS = [\"#009E73\", \"#C475FD\", \"#4467A3\"]\nCIRCLE_FILLS = [\"rgba(0, 158, 115, 0.26)\", \"rgba(196, 117, 253, 0.26)\", \"rgba(68, 103, 163, 0.26)\"]\n\n# Data: editorial Chartgeist-style commentary on tech/culture\ncircles = [{\"name\": \"Overhyped\"}, {\"name\": \"Actually Useful\"}, {\"name\": \"Secretly Loved\"}]\n\nitems = [\n    {\"label\": \"NFTs\", \"zone\": \"A\"},\n    {\"label\": \"Crypto Bros\", \"zone\": \"A\"},\n    {\"label\": \"Metaverse\", \"zone\": \"A\"},\n    {\"label\": \"Google Maps\", \"zone\": \"B\"},\n    {\"label\": \"VS Code\", \"zone\": \"B\"},\n    {\"label\": \"Dishwasher\", \"zone\": \"B\"},\n    {\"label\": \"ABBA\", \"zone\": \"C\"},\n    {\"label\": \"Slippers\", \"zone\": \"C\"},\n    {\"label\": \"Crocs\", \"zone\": \"C\"},\n    {\"label\": \"ChatGPT\", \"zone\": \"AB\"},\n    {\"label\": \"Slack\", \"zone\": \"AB\"},\n    {\"label\": \"Vinyl Records\", \"zone\": \"AC\"},\n    {\"label\": \"Dolly Parton\", \"zone\": \"BC\"},\n    {\"label\": \"Sourdough\", \"zone\": \"ABC\"},\n    {\"label\": \"Beige Walls\", \"zone\": \"outside\"},\n]\n\n# Three-circle symmetric Venn layout\nradius = 1.0\nangles = [np.pi / 2, np.pi / 2 + 2 * np.pi / 3, np.pi / 2 + 4 * np.pi / 3]\ncenter_dist = 0.55\ncx = [center_dist * np.cos(a) for a in angles]\ncy = [center_dist * np.sin(a) for a in angles]\n\n# Per-zone anchor (x, y, textposition) with outward-pointing alignment:\n# items in the left sector point left ('middle left'), right sector point right,\n# bottom-center zone floats below its marker, outside cluster reads rightward.\nzone_anchors = {\n    \"A\": (0.0, 1.07, \"middle right\"),\n    \"B\": (-0.95, -0.30, \"middle left\"),\n    \"C\": (0.95, -0.30, \"middle right\"),\n    \"AB\": (-0.50, 0.30, \"middle left\"),\n    \"AC\": (0.50, 0.30, \"middle right\"),\n    \"BC\": (0.0, -0.55, \"bottom center\"),\n    \"ABC\": (0.0, 0.0, \"middle right\"),\n    \"outside\": (-2.20, 1.55, \"middle right\"),\n}\n\n# Spread offsets when multiple items share a zone (in data units, spread vertically)\nzone_offsets = {\n    1: [(0.0, 0.0)],\n    2: [(0.0, 0.18), (0.0, -0.18)],\n    3: [(0.0, 0.28), (0.0, 0.0), (0.0, -0.28)],\n    4: [(0.0, 0.42), (0.0, 0.14), (0.0, -0.14), (0.0, -0.42)],\n}\n\n# Group items by zone, compute final coords\nzones_grouped: dict[str, list[str]] = {}\nfor it in items:\n    zones_grouped.setdefault(it[\"zone\"], []).append(it[\"label\"])\n\nplaced = []\nfor zone, labels in zones_grouped.items():\n    ax, ay, tpos = zone_anchors[zone]\n    n = len(labels)\n    offsets = zone_offsets.get(n, [(i * 0.28 - 0.14 * (n - 1), 0.0) for i in range(n)])\n    for label, (dx, dy) in zip(labels, offsets, strict=False):\n        placed.append((ax + dx, ay + dy, label, zone, tpos))\n\n# Build figure\nfig = go.Figure()\n\n# Filled circles\ntheta = np.linspace(0, 2 * np.pi, 240)\nfor i in range(3):\n    fig.add_trace(\n        go.Scatter(\n            x=cx[i] + radius * np.cos(theta),\n            y=cy[i] + radius * np.sin(theta),\n            fill=\"toself\",\n            fillcolor=CIRCLE_FILLS[i],\n            line={\"color\": CIRCLE_COLORS[i], \"width\": 3},\n            mode=\"lines\",\n            name=circles[i][\"name\"],\n            showlegend=False,\n            hoverinfo=\"skip\",\n        )\n    )\n\n# Item markers + labels — one trace per text position to mix alignments\nfor tpos in sorted({p[4] for p in placed}):\n    pts = [p for p in placed if p[4] == tpos]\n    fig.add_trace(\n        go.Scatter(\n            x=[p[0] for p in pts],\n            y=[p[1] for p in pts],\n            mode=\"markers+text\",\n            marker={\"size\": 11, \"color\": INK, \"line\": {\"width\": 0}},\n            text=[f\" {p[2]} \" for p in pts],\n            textposition=tpos,\n            textfont={\"family\": \"Georgia, 'Times New Roman', serif\", \"size\": 13, \"color\": INK},\n            hovertext=[f\"{p[2]} — {p[3]}\" for p in pts],\n            hoverinfo=\"text\",\n            showlegend=False,\n        )\n    )\n\n# Category labels outside each circle\ncategory_label_positions = [\n    (cx[0], cy[0] + radius + 0.32, \"center\", \"bottom\"),\n    (cx[1] - radius * 0.55, cy[1] - radius - 0.05, \"right\", \"top\"),\n    (cx[2] + radius * 0.55, cy[2] - radius - 0.05, \"left\", \"top\"),\n]\n\nfor i, (lx, ly, xa, ya) in enumerate(category_label_positions):\n    fig.add_annotation(\n        x=lx,\n        y=ly,\n        text=f\"<b>{circles[i]['name'].upper()}</b>\",\n        showarrow=False,\n        font={\"family\": \"Georgia, 'Times New Roman', serif\", \"size\": 15, \"color\": CIRCLE_COLORS[i]},\n        xanchor=xa,\n        yanchor=ya,\n    )\n\n# Hint label for the \"outside\" cluster\nfig.add_annotation(\n    x=-2.20,\n    y=1.87,\n    text=\"<i>Neither here nor there</i>\",\n    showarrow=False,\n    font={\"family\": \"Georgia, 'Times New Roman', serif\", \"size\": 11, \"color\": INK_MUTED},\n    xanchor=\"left\",\n    yanchor=\"bottom\",\n)\n\n# Editorial subtitle / kicker\nfig.add_annotation(\n    xref=\"paper\",\n    yref=\"paper\",\n    x=0.5,\n    y=1.005,\n    text=\"<i>An entirely subjective taxonomy of things, ranked by vibe.</i>\",\n    showarrow=False,\n    font={\"family\": \"Georgia, 'Times New Roman', serif\", \"size\": 11, \"color\": INK_SOFT},\n    xanchor=\"center\",\n    yanchor=\"bottom\",\n)\n\n# Use explicit x/y ranges with equal pixel density (no scaleanchor) so circles render round.\n# Base canvas: 600×600, margins l=80 r=40 t=100 b=60 → plot area 480×440 px.\n# x range 5.2 units → 480/5.2 ≈ 92.3 px/unit; y range 4.76 units → 440/4.76 ≈ 92.4 px/unit.\n# y range shifted up 0.41 units to centre diagram and eliminate bottom dead space.\nfig.update_layout(\n    autosize=False,\n    title={\n        \"text\": \"<b>CHARTGEIST</b>  ·  venn-labeled-items · python · plotly · anyplot.ai\",\n        \"font\": {\"family\": \"Georgia, 'Times New Roman', serif\", \"size\": 20, \"color\": INK},\n        \"x\": 0.5,\n        \"xanchor\": \"center\",\n        \"y\": 0.975,\n    },\n    paper_bgcolor=PAGE_BG,\n    plot_bgcolor=PAGE_BG,\n    showlegend=False,\n    xaxis={\"visible\": False, \"range\": [-3.0, 2.2]},\n    yaxis={\"visible\": False, \"range\": [-1.7, 3.06]},\n    margin={\"l\": 80, \"r\": 40, \"t\": 100, \"b\": 60},\n    font={\"family\": \"Georgia, 'Times New Roman', serif\", \"color\": INK},\n)\n\n# Square canvas — canonical plotly square: width=600, height=600, scale=4 → 2400×2400 px\nfig.write_image(f\"plot-{THEME}.png\", width=600, height=600, scale=4)\nfig.write_html(f\"plot-{THEME}.html\", include_plotlyjs=\"cdn\")\n"}