{"spec_id":"venn-labeled-items","library":"d3","language":"javascript","code":"// anyplot.ai\n// venn-labeled-items: Chartgeist-Style Venn Diagram with Labeled Items\n// Library: d3 7.9.0 | JavaScript 22.23.0\n// Quality: 87/100 | Created: 2026-06-25\n//# anyplot-orientation: square\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst THEME = window.ANYPLOT_THEME || \"light\";\nconst inkMuted = THEME === \"dark\" ? \"#A8A79F\" : \"#6B6A63\";\n\n// --- Data ---\nconst circleData = [\n  { name: \"Overhyped\",       cx: 600, cy: 480, r: 270 },\n  { name: \"Actually Useful\", cx: 738, cy: 718, r: 270 },\n  { name: \"Secretly Loved\",  cx: 462, cy: 718, r: 270 },\n];\n\n// D3 ordinal scale maps category names to Imprint palette in canonical order\nconst colorScale = d3.scaleOrdinal()\n  .domain(circleData.map(d => d.name))\n  .range(t.palette.slice(0, 3));\n\n// Centroid of each Venn zone for label placement\nconst ZONE_CENTROIDS = {\n  A:       { x: 600, y: 314 },\n  B:       { x: 878, y: 800 },\n  C:       { x: 322, y: 800 },\n  AB:      { x: 730, y: 566 },\n  AC:      { x: 470, y: 566 },\n  BC:      { x: 600, y: 840 },\n  ABC:     { x: 600, y: 638 },\n  outside: { x: 975, y: 360 },\n};\n\nconst items = [\n  { label: \"NFTs\",              zone: \"A\"       },\n  { label: \"Metaverse\",         zone: \"A\"       },\n  { label: \"Segway\",            zone: \"A\"       },\n  { label: \"Google Glass\",      zone: \"A\"       },\n  { label: \"Wikipedia\",         zone: \"B\"       },\n  { label: \"GPS Navigation\",    zone: \"B\"       },\n  { label: \"Cloud Backup\",      zone: \"B\"       },\n  { label: \"Password Managers\", zone: \"B\"       },\n  { label: \"Spreadsheets\",      zone: \"C\"       },\n  { label: \"Fax Machines\",      zone: \"C\"       },\n  { label: \"Cable TV\",          zone: \"C\"       },\n  { label: \"ChatGPT\",           zone: \"AB\"      },\n  { label: \"Electric Scooters\", zone: \"AB\"      },\n  { label: \"TikTok\",            zone: \"AC\"      },\n  { label: \"Gamification\",      zone: \"AC\"      },\n  { label: \"Dark Mode\",         zone: \"BC\"      },\n  { label: \"RSS Feeds\",         zone: \"BC\"      },\n  { label: \"Sourdough\",         zone: \"ABC\"     },\n  { label: \"Zoom\",              zone: \"ABC\"     },\n  { label: \"Landlines\",         zone: \"outside\" },\n];\n\n// Use d3.group to partition items by zone, then compute vertical stacking positions\nconst SPACING = 24;\nconst itemsByZone = d3.group(items, d => d.zone);\n\nitemsByZone.forEach((zoneItems, zone) => {\n  const { x: zx, y: zy } = ZONE_CENTROIDS[zone];\n  const totalH = (zoneItems.length - 1) * SPACING;\n  zoneItems.forEach((item, i) => {\n    item.x = zx;\n    item.y = zy - totalH / 2 + i * SPACING;\n  });\n});\n\n// --- SVG ---\nconst svg = d3.select(\"#container\").append(\"svg\")\n  .attr(\"width\", width)\n  .attr(\"height\", height);\n\n// --- Circles (data join, semi-transparent fills, colored strokes) ---\nsvg.selectAll(\"circle.venn-circle\").data(circleData).join(\"circle\")\n  .attr(\"class\", \"venn-circle\")\n  .attr(\"cx\", d => d.cx).attr(\"cy\", d => d.cy).attr(\"r\", d => d.r)\n  .attr(\"fill\", d => colorScale(d.name)).attr(\"fill-opacity\", 0.12)\n  .attr(\"stroke\", d => colorScale(d.name))\n  .attr(\"stroke-width\", 2.5).attr(\"stroke-opacity\", 0.65);\n\n// --- Category labels (data join per circle, editorial serif) ---\nconst catFont = \"Georgia, 'Times New Roman', serif\";\n\nconst catLabelDefs = [\n  { name: \"Overhyped\",       lines: [\"Overhyped\"],          x: circleData[0].cx,                        y: circleData[0].cy - circleData[0].r - 20, anchor: \"middle\" },\n  { name: \"Actually Useful\", lines: [\"Actually\", \"Useful\"], x: circleData[1].cx + circleData[1].r + 26, y: circleData[1].cy - 13,                  anchor: \"start\"  },\n  { name: \"Secretly Loved\",  lines: [\"Secretly\", \"Loved\"],  x: circleData[2].cx - circleData[2].r - 26, y: circleData[2].cy - 13,                  anchor: \"end\"    },\n];\n\ncatLabelDefs.forEach(def => {\n  const catG = svg.append(\"g\");\n  catG.selectAll(\"text\").data(def.lines).join(\"text\")\n    .attr(\"x\", def.x)\n    .attr(\"y\", (_, i) => def.y + i * 28)\n    .attr(\"text-anchor\", def.anchor)\n    .attr(\"fill\", colorScale(def.name))\n    .style(\"font-size\", \"20px\").style(\"font-weight\", \"700\")\n    .style(\"font-family\", catFont)\n    .text(d => d);\n});\n\n// --- \"outside all circles\" annotation ---\nconst outsideItems = items.filter(d => d.zone === \"outside\");\nif (outsideItems.length) {\n  svg.append(\"text\")\n    .attr(\"x\", ZONE_CENTROIDS.outside.x)\n    .attr(\"y\", d3.min(outsideItems, d => d.y) - 22)\n    .attr(\"text-anchor\", \"middle\").attr(\"fill\", inkMuted)\n    .style(\"font-size\", \"14px\").style(\"font-style\", \"italic\")\n    .text(\"outside all circles\");\n}\n\n// --- Items (data join over all items, positions from d3.group stacking) ---\nsvg.selectAll(\"text.venn-item\").data(items).join(\"text\")\n  .attr(\"class\", \"venn-item\")\n  .attr(\"x\", d => d.x)\n  .attr(\"y\", d => d.y)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"dominant-baseline\", \"middle\")\n  .attr(\"fill\", d => d.zone === \"outside\" ? inkMuted : t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .style(\"font-style\", d => d.zone === \"outside\" ? \"italic\" : \"normal\")\n  .style(\"font-family\", \"system-ui, -apple-system, sans-serif\")\n  .text(d => d.label);\n\n// --- Title ---\nsvg.append(\"text\")\n  .attr(\"x\", width / 2).attr(\"y\", 46)\n  .attr(\"text-anchor\", \"middle\").attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\").style(\"font-weight\", \"600\")\n  .text(\"venn-labeled-items · javascript · d3 · anyplot.ai\");\n"}