{"spec_id":"venn-basic","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// venn-basic: Venn Diagram\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 93/100 | Created: 2026-09-09\n\n//# anyplot-orientation: square\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Reading-habit survey of 500 adults: which genres they read regularly.\n// Respondents may read more than one genre, so the three sets overlap.\nconst totalReaders = 500;\nconst fictionTotal = 280;\nconst nonfictionTotal = 210;\nconst memoirTotal = 150;\n\n// Pairwise totals (each includes the triple-overlap readers, standard\n// inclusion-exclusion convention) plus the triple overlap itself.\nconst fictionNonfiction = 95;\nconst fictionMemoir = 60;\nconst nonfictionMemoir = 70;\nconst allThree = 35;\n\nconst sets = [\n  { name: \"Fiction readers\", total: fictionTotal, color: t.palette[0] },\n  { name: \"Nonfiction readers\", total: nonfictionTotal, color: t.palette[1] },\n  { name: \"Memoir readers\", total: memoirTotal, color: t.palette[2] },\n];\n\n// Exclusive region counts, derived from the pairwise/triple totals above.\nconst onlyFiction = fictionTotal - fictionNonfiction - fictionMemoir + allThree;\nconst onlyNonfiction = nonfictionTotal - fictionNonfiction - nonfictionMemoir + allThree;\nconst onlyMemoir = memoirTotal - fictionMemoir - nonfictionMemoir + allThree;\nconst onlyFictionNonfiction = fictionNonfiction - allThree;\nconst onlyFictionMemoir = fictionMemoir - allThree;\nconst onlyNonfictionMemoir = nonfictionMemoir - allThree;\n\nfunction withAlpha(hex, alpha) {\n  const n = parseInt(hex.slice(1), 16);\n  const r = (n >> 16) & 255;\n  const g = (n >> 8) & 255;\n  const b = n & 255;\n  return `rgba(${r}, ${g}, ${b}, ${alpha})`;\n}\n\n// --- Proportional-Venn geometry ----------------------------------------------\n// Circle-circle lens (intersection) area for radii r1, r2 with center\n// distance d — the standard two-circle-lens formula.\nfunction lensArea(r1, r2, d) {\n  if (d >= r1 + r2) return 0;\n  if (d <= Math.abs(r1 - r2)) return Math.PI * Math.min(r1, r2) ** 2;\n  const r1sq = r1 * r1;\n  const r2sq = r2 * r2;\n  const dsq = d * d;\n  const alpha = Math.acos((dsq + r1sq - r2sq) / (2 * d * r1));\n  const beta = Math.acos((dsq + r2sq - r1sq) / (2 * d * r2));\n  const triangleTerm =\n    0.5 * Math.sqrt((-d + r1 + r2) * (d + r1 - r2) * (d - r1 + r2) * (d + r1 + r2));\n  return r1sq * alpha + r2sq * beta - triangleTerm;\n}\n\n// Binary-search the center distance that makes lensArea(r1, r2, d) equal\n// targetArea — lensArea is monotonically decreasing in d, so bisection works.\nfunction solveDistanceForArea(r1, r2, targetArea) {\n  const maxArea = Math.PI * Math.min(r1, r2) ** 2;\n  if (targetArea >= maxArea) return Math.abs(r1 - r2) + 1e-3;\n  if (targetArea <= 0) return r1 + r2;\n  let lo = Math.abs(r1 - r2) + 1e-6;\n  let hi = r1 + r2 - 1e-6;\n  for (let i = 0; i < 50; i++) {\n    const mid = (lo + hi) / 2;\n    if (lensArea(r1, r2, mid) > targetArea) lo = mid;\n    else hi = mid;\n  }\n  return (lo + hi) / 2;\n}\n\nfunction normalize(vx, vy) {\n  const len = Math.hypot(vx, vy) || 1e-6;\n  return [vx / len, vy / len];\n}\n\n// --- Venn diagram plugin -------------------------------------------------------\n// Chart.js has no native Venn geometry. A \"bubble\" dataset sized to the real\n// circle radius would work in principle, but Chart.js auto-reserves layout\n// padding equal to the largest point radius on every side (so large bubbles\n// never clip) — at this scale that padding eats almost the whole chart area.\n// Instead the dataset stays a near-invisible placeholder (keeps `new Chart`\n// idiomatic) and this plugin draws the three circles and their region labels\n// directly onto the canvas, sized from `chart.chartArea` (native Chart.js\n// plugin API — not an external library).\n//\n// Circle radii scale with sqrt(set total) (area proportional to size). Each\n// pairwise center distance is then solved so the two-circle lens area\n// matches that pair's real overlap count at the same area-per-reader scale\n// — a true proportional derivation, not a fixed geometric factor. The third\n// circle is triangulated from the two solved pairwise distances to its\n// neighbors, so all three pairwise overlaps are simultaneously exact; the\n// resulting triple-overlap lens is the geometric consequence of that\n// triangle, and its label reports the real data-derived count.\nconst vennDiagram = {\n  id: \"vennDiagram\",\n  afterDatasetsDraw(chart) {\n    const { ctx, chartArea: area } = chart;\n    const width = area.right - area.left;\n    const height = area.bottom - area.top;\n    const cxMid = area.left + width / 2;\n    const cyMid = area.top + height / 2;\n\n    // Radii proportional to sqrt(total) so circle area scales with set size.\n    const rFiction0 = height * 0.34;\n    const k = rFiction0 / Math.sqrt(fictionTotal);\n    const radii = sets.map((s) => k * Math.sqrt(s.total));\n    const areaPerReader = Math.PI * k * k;\n\n    const dAB = solveDistanceForArea(radii[0], radii[1], areaPerReader * fictionNonfiction);\n    const dAC = solveDistanceForArea(radii[0], radii[2], areaPerReader * fictionMemoir);\n    const dBC = solveDistanceForArea(radii[1], radii[2], areaPerReader * nonfictionMemoir);\n\n    // Triangulate: A at origin, B on the x-axis at distance dAB, C placed so\n    // its distances to A and B match dAC and dBC (standard trilateration).\n    const localA = { x: 0, y: 0 };\n    const localB = { x: dAB, y: 0 };\n    const cx = (dAC * dAC - dBC * dBC + dAB * dAB) / (2 * dAB);\n    const cy = Math.sqrt(Math.max(dAC * dAC - cx * cx, 0));\n    const localC = { x: cx, y: cy };\n\n    const centroid = {\n      x: (localA.x + localB.x + localC.x) / 3,\n      y: (localA.y + localB.y + localC.y) / 3,\n    };\n    const local = [localA, localB, localC].map((p) => ({ x: p.x - centroid.x, y: p.y - centroid.y }));\n\n    // Fit the triangle + circles inside the chart area, leaving room for\n    // set-name labels above/around the cluster.\n    const labelMargin = 170;\n    let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;\n    local.forEach((p, i) => {\n      minX = Math.min(minX, p.x - radii[i]);\n      maxX = Math.max(maxX, p.x + radii[i]);\n      minY = Math.min(minY, p.y - radii[i]);\n      maxY = Math.max(maxY, p.y + radii[i]);\n    });\n    const bboxW = maxX - minX;\n    const bboxH = maxY - minY;\n    const fitScale = Math.min(\n      (width - 2 * labelMargin) / bboxW,\n      (height - 2 * labelMargin) / bboxH,\n      1\n    );\n\n    // Canvas y grows downward; local y was built \"up positive\", so flip it.\n    const centers = local.map((p, i) => ({\n      x: cxMid + p.x * fitScale,\n      y: cyMid - p.y * fitScale,\n      r: radii[i] * fitScale,\n    }));\n\n    ctx.save();\n\n    // Circles — drawn with translucent fill so overlaps blend visibly.\n    centers.forEach((c, i) => {\n      ctx.beginPath();\n      ctx.arc(c.x, c.y, c.r, 0, Math.PI * 2);\n      ctx.fillStyle = withAlpha(sets[i].color, 0.5);\n      ctx.fill();\n      ctx.lineWidth = 1.5;\n      ctx.strokeStyle = withAlpha(sets[i].color, 0.9);\n      ctx.stroke();\n    });\n\n    // Set names, placed radially outward from the cluster center so they\n    // clear the neighboring circles regardless of triangle shape.\n    ctx.textAlign = \"center\";\n    ctx.textBaseline = \"middle\";\n    sets.forEach((s, i) => {\n      const c = centers[i];\n      const [dx, dy] = normalize(c.x - cxMid, c.y - cyMid);\n      const lx = c.x + dx * (c.r + 26);\n      const ly = c.y + dy * (c.r + 26);\n      ctx.fillStyle = t.ink;\n      ctx.font = \"bold 24px -apple-system, sans-serif\";\n      ctx.fillText(s.name, lx, ly);\n    });\n\n    // Region counts: one-per-set exclusive regions, the three pairwise-only\n    // regions, and the triple overlap at the triangle centroid.\n    const mid = (p, q) => ({ x: (p.x + q.x) / 2, y: (p.y + q.y) / 2 });\n    const [A, B, C] = centers;\n    const midAB = mid(A, B);\n    const midAC = mid(A, C);\n    const midBC = mid(B, C);\n    const [dirAB_x, dirAB_y] = normalize(midAB.x - C.x, midAB.y - C.y);\n    const [dirAC_x, dirAC_y] = normalize(midAC.x - B.x, midAC.y - B.y);\n    const [dirBC_x, dirBC_y] = normalize(midBC.x - A.x, midBC.y - A.y);\n    const [dirA_x, dirA_y] = normalize(A.x - cxMid, A.y - cyMid);\n    const [dirB_x, dirB_y] = normalize(B.x - cxMid, B.y - cyMid);\n    const [dirC_x, dirC_y] = normalize(C.x - cxMid, C.y - cyMid);\n\n    const regions = [\n      { count: onlyFiction, label: \"Fiction only\", x: A.x + dirA_x * A.r * 0.45, y: A.y + dirA_y * A.r * 0.45 },\n      { count: onlyNonfiction, label: \"Nonfiction only\", x: B.x + dirB_x * B.r * 0.45, y: B.y + dirB_y * B.r * 0.45 },\n      { count: onlyMemoir, label: \"Memoir only\", x: C.x + dirC_x * C.r * 0.45, y: C.y + dirC_y * C.r * 0.45 },\n      { count: onlyFictionNonfiction, label: \"Fiction & Nonfiction\", x: midAB.x + dirAB_x * 22, y: midAB.y + dirAB_y * 22 },\n      { count: onlyFictionMemoir, label: \"Fiction & Memoir\", x: midAC.x + dirAC_x * 22, y: midAC.y + dirAC_y * 22 },\n      { count: onlyNonfictionMemoir, label: \"Nonfiction & Memoir\", x: midBC.x + dirBC_x * 22, y: midBC.y + dirBC_y * 22 },\n      { count: allThree, label: \"All three\", x: cxMid, y: cyMid },\n    ];\n    regions.forEach((r) => {\n      ctx.fillStyle = t.ink;\n      ctx.font = \"bold 26px -apple-system, sans-serif\";\n      ctx.fillText(String(r.count), r.x, r.y - 12);\n      ctx.fillStyle = t.inkSoft;\n      ctx.font = \"13px -apple-system, sans-serif\";\n      ctx.fillText(r.label, r.x, r.y + 12);\n    });\n\n    ctx.restore();\n  },\n};\n\n// --- Mount -------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Chart ---------------------------------------------------------------------\nnew Chart(canvas, {\n  type: \"bubble\",\n  data: {\n    // Invisible placeholder points — the visible circles are hand-drawn by\n    // vennDiagram above, sized to the actual chart area instead of Chart.js's\n    // radius-based auto-padding.\n    datasets: sets.map((s) => ({\n      label: s.name,\n      data: [{ x: 0, y: 0, r: 1 }],\n      backgroundColor: \"transparent\",\n      borderWidth: 0,\n    })),\n  },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    layout: { padding: { top: 20, bottom: 40, left: 40, right: 40 } },\n    plugins: {\n      title: {\n        display: true,\n        text: \"venn-basic · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22 },\n        padding: { bottom: 4 },\n      },\n      subtitle: {\n        display: true,\n        text: `Reading habits of ${totalReaders} surveyed adults`,\n        color: t.inkSoft,\n        font: { size: 16 },\n        padding: { bottom: 20 },\n      },\n      legend: { display: false },\n      tooltip: { enabled: false },\n    },\n    scales: {\n      x: { display: false, min: -1, max: 1 },\n      y: { display: false, min: -1, max: 1 },\n    },\n  },\n  plugins: [vennDiagram],\n});\n"}