{"spec_id":"upset-basic","library":"d3","language":"javascript","code":"// anyplot.ai\n// upset-basic: UpSet Plot for Multi-Set Intersection Analysis\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-09-09\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// --- Data (in-memory, deterministic LCG) ------------------------------------\nfunction makeLcg(seed) {\n  let state = seed >>> 0;\n  return function () {\n    state = (Math.imul(state, 1664525) + 1013904223) >>> 0;\n    return state / 4294967296;\n  };\n}\nconst rand = makeLcg(42);\n\n// Bug reports (elements) tagged by the module categories they touch (sets).\nconst SET_DEFS = [\n  { name: \"Crash\", prob: 0.42 },\n  { name: \"UI/UX\", prob: 0.5 },\n  { name: \"Performance\", prob: 0.33 },\n  { name: \"Security\", prob: 0.18 },\n  { name: \"Data Loss\", prob: 0.12 },\n  { name: \"Compatibility\", prob: 0.28 },\n];\n\nconst nElements = 450;\nconst elements = [];\nfor (let i = 0; i < nElements; i++) {\n  let membership;\n  do {\n    membership = SET_DEFS.filter((s) => rand() < s.prob).map((s) => s.name);\n  } while (membership.length === 0);\n  elements.push(membership);\n}\n\n// Rows ordered by set size, largest first (UpSet convention).\nconst setSizes = SET_DEFS.map((s) => ({\n  name: s.name,\n  size: elements.filter((m) => m.includes(s.name)).length,\n})).sort((a, b) => b.size - a.size);\nconst rowNames = setSizes.map((s) => s.name);\n\n// Aggregate elements into unique intersections, keep the largest MAX_COLUMNS.\nconst MAX_COLUMNS = 18;\nconst comboCounts = new Map();\nelements.forEach((m) => {\n  const key = rowNames.filter((name) => m.includes(name)).join(\"|\");\n  comboCounts.set(key, (comboCounts.get(key) || 0) + 1);\n});\nconst intersections = Array.from(comboCounts, ([key, count]) => ({\n  members: key.split(\"|\"),\n  count,\n}))\n  .sort((a, b) => b.count - a.count)\n  .slice(0, MAX_COLUMNS);\n\n// --- Layout -------------------------------------------------------------\nconst margin = { top: 80, right: 50, bottom: 55, left: 40 };\nconst labelWidth = 130;\nconst setBarWidth = 150;\nconst rowHeight = 50;\nconst gapTopMatrix = 10;\n\nconst matrixLeft = margin.left + labelWidth + setBarWidth;\nconst matrixWidth = width - margin.right - matrixLeft;\nconst matrixHeight = rowNames.length * rowHeight;\nconst topChartTop = margin.top;\nconst topChartHeight = height - margin.top - margin.bottom - matrixHeight - gapTopMatrix;\nconst topChartBottom = topChartTop + topChartHeight;\nconst matrixTop = topChartBottom + gapTopMatrix;\nconst matrixBottom = matrixTop + matrixHeight;\n\nconst rowIndex = new Map(rowNames.map((name, i) => [name, i]));\nconst rowCenter = (name) => matrixTop + rowIndex.get(name) * rowHeight + rowHeight / 2;\n\nconst xCol = d3\n  .scaleBand()\n  .domain(d3.range(intersections.length))\n  .range([matrixLeft, matrixLeft + matrixWidth])\n  .padding(0.35);\n\nconst maxCount = d3.max(intersections, (d) => d.count);\nconst yCount = d3.scaleLinear().domain([0, maxCount]).nice().range([topChartBottom, topChartTop]);\n\nconst maxSetSize = d3.max(setSizes, (d) => d.size);\nconst xSetBar = d3.scaleLinear().domain([0, maxSetSize]).nice().range([0, setBarWidth]);\n\nconst maxDegree = d3.max(intersections, (d) => d.members.length);\nconst degreeColor = d3.scaleSequential(d3.interpolateRgbBasis(t.seq)).domain([1, maxDegree]);\n\n// --- SVG mount ----------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\n\n// Title\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 40)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"24px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"upset-basic · javascript · d3 · anyplot.ai\");\n\n// Degree color legend (top right)\nconst legendWidth = 140;\nconst legendHeight = 12;\nconst legendX = width - margin.right - legendWidth;\nconst legendY = 34;\nconst gradient = svg\n  .append(\"defs\")\n  .append(\"linearGradient\")\n  .attr(\"id\", \"upset-degree-gradient\")\n  .attr(\"x1\", \"0%\")\n  .attr(\"x2\", \"100%\")\n  .attr(\"y1\", \"0%\")\n  .attr(\"y2\", \"0%\");\nd3.range(7).forEach((k) => {\n  const deg = 1 + (k / 6) * (maxDegree - 1);\n  gradient\n    .append(\"stop\")\n    .attr(\"offset\", `${(k / 6) * 100}%`)\n    .attr(\"stop-color\", degreeColor(deg));\n});\nsvg\n  .append(\"rect\")\n  .attr(\"x\", legendX)\n  .attr(\"y\", legendY)\n  .attr(\"width\", legendWidth)\n  .attr(\"height\", legendHeight)\n  .attr(\"fill\", \"url(#upset-degree-gradient)\");\nsvg\n  .append(\"text\")\n  .attr(\"x\", legendX)\n  .attr(\"y\", legendY - 8)\n  .attr(\"text-anchor\", \"start\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"12px\")\n  .text(\"1 set\");\nsvg\n  .append(\"text\")\n  .attr(\"x\", legendX + legendWidth)\n  .attr(\"y\", legendY - 8)\n  .attr(\"text-anchor\", \"end\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"12px\")\n  .text(`${maxDegree} sets`);\nsvg\n  .append(\"text\")\n  .attr(\"x\", legendX + legendWidth / 2)\n  .attr(\"y\", legendY + legendHeight + 16)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"11px\")\n  .text(\"Intersection degree\");\n\n// Alternating row stripes for matrix readability\nrowNames.forEach((name, i) => {\n  if (i % 2 === 1) {\n    svg\n      .append(\"rect\")\n      .attr(\"x\", matrixLeft)\n      .attr(\"y\", matrixTop + i * rowHeight)\n      .attr(\"width\", matrixWidth)\n      .attr(\"height\", rowHeight)\n      .attr(\"fill\", t.elevatedBg);\n  }\n});\n\n// Row labels (set names)\nrowNames.forEach((name) => {\n  svg\n    .append(\"text\")\n    .attr(\"x\", margin.left + labelWidth - 12)\n    .attr(\"y\", rowCenter(name))\n    .attr(\"text-anchor\", \"end\")\n    .attr(\"dominant-baseline\", \"middle\")\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"15px\")\n    .text(name);\n});\n\n// Horizontal bars — total members per set, flush against the matrix\nsetSizes.forEach((s) => {\n  const w = xSetBar(s.size);\n  svg\n    .append(\"rect\")\n    .attr(\"x\", matrixLeft - w)\n    .attr(\"y\", rowCenter(s.name) - rowHeight * 0.32)\n    .attr(\"width\", w)\n    .attr(\"height\", rowHeight * 0.64)\n    .attr(\"fill\", t.palette[0]);\n});\n\nconst setBarScale = d3.scaleLinear().domain([0, maxSetSize]).nice().range([matrixLeft, matrixLeft - setBarWidth]);\nconst setBarAxis = svg.append(\"g\").attr(\"transform\", `translate(0,${matrixBottom})`).call(d3.axisBottom(setBarScale).ticks(3));\nsetBarAxis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"12px\");\nsetBarAxis.selectAll(\"line\").attr(\"stroke\", t.grid);\nsetBarAxis.select(\".domain\").attr(\"stroke\", t.inkSoft);\nsvg\n  .append(\"text\")\n  .attr(\"x\", matrixLeft - setBarWidth / 2)\n  .attr(\"y\", matrixBottom + 40)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .text(\"Set Size\");\n\n// Vertical bars — intersection cardinality, colored by degree\nintersections.forEach((d, i) => {\n  const x = xCol(i);\n  const y = yCount(d.count);\n  svg\n    .append(\"rect\")\n    .attr(\"x\", x)\n    .attr(\"y\", y)\n    .attr(\"width\", xCol.bandwidth())\n    .attr(\"height\", topChartBottom - y)\n    .attr(\"fill\", degreeColor(d.members.length));\n});\n\nconst countAxis = svg.append(\"g\").attr(\"transform\", `translate(${matrixLeft},0)`).call(d3.axisLeft(yCount).ticks(5));\ncountAxis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"13px\");\ncountAxis.selectAll(\"line\").attr(\"stroke\", t.grid);\ncountAxis.select(\".domain\").attr(\"stroke\", t.inkSoft);\nsvg\n  .append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -((topChartTop + topChartBottom) / 2))\n  .attr(\"y\", matrixLeft - 46)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .text(\"Intersection Size\");\n\n// Dot matrix — filled dot + connecting line marks which sets form each intersection\nconst dotG = svg.append(\"g\");\nintersections.forEach((d, i) => {\n  const cx = xCol(i) + xCol.bandwidth() / 2;\n  const includedYs = rowNames.filter((name) => d.members.includes(name)).map(rowCenter);\n\n  if (includedYs.length > 1) {\n    dotG\n      .append(\"line\")\n      .attr(\"x1\", cx)\n      .attr(\"x2\", cx)\n      .attr(\"y1\", d3.min(includedYs))\n      .attr(\"y2\", d3.max(includedYs))\n      .attr(\"stroke\", t.ink)\n      .attr(\"stroke-width\", 3);\n  }\n\n  rowNames.forEach((name) => {\n    const included = d.members.includes(name);\n    dotG\n      .append(\"circle\")\n      .attr(\"cx\", cx)\n      .attr(\"cy\", rowCenter(name))\n      .attr(\"r\", included ? 9 : 5)\n      .attr(\"fill\", included ? t.ink : \"none\")\n      .attr(\"stroke\", included ? \"none\" : t.grid)\n      .attr(\"stroke-width\", included ? 0 : 1.5);\n  });\n});\n"}