{"spec_id":"scatter-matrix","library":"d3","language":"javascript","code":"// anyplot.ai\n// scatter-matrix: Scatter Plot Matrix\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 93/100 | Created: 2026-09-09\n//# anyplot-orientation: square\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// --- Data (in-memory, deterministic Iris-like flower measurements) ---------\nfunction lcg(seed) {\n  let s = seed >>> 0;\n  return () => {\n    s = (Math.imul(s, 1103515245) + 12345) >>> 0;\n    return s / 4294967296;\n  };\n}\nconst rand = lcg(42);\nfunction gaussian() {\n  const u1 = Math.max(rand(), 1e-9);\n  const u2 = rand();\n  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\n\nconst species = [\n  { name: \"Setosa\", n: 50, sepalLength: [5.0, 0.35], sepalWidth: [3.4, 0.38], petalLength: [1.5, 0.17], widthRatio: 0.168 },\n  { name: \"Versicolor\", n: 50, sepalLength: [5.9, 0.52], sepalWidth: [2.77, 0.31], petalLength: [4.26, 0.47], widthRatio: 0.312 },\n  { name: \"Virginica\", n: 50, sepalLength: [6.59, 0.64], sepalWidth: [2.97, 0.32], petalLength: [5.55, 0.55], widthRatio: 0.366 },\n];\n\nconst data = [];\nspecies.forEach((sp) => {\n  for (let i = 0; i < sp.n; i++) {\n    const sepalLength = sp.sepalLength[0] + sp.sepalLength[1] * gaussian();\n    const sepalWidth = sp.sepalWidth[0] + sp.sepalWidth[1] * gaussian();\n    const petalLength = Math.max(sp.petalLength[0] + sp.petalLength[1] * gaussian(), 0.1);\n    const petalWidth = Math.max(petalLength * sp.widthRatio + 0.08 * gaussian(), 0.05);\n    data.push({ species: sp.name, sepalLength, sepalWidth, petalLength, petalWidth });\n  }\n});\n\nconst variables = [\n  { key: \"sepalLength\", label: \"Sepal Length (cm)\" },\n  { key: \"sepalWidth\", label: \"Sepal Width (cm)\" },\n  { key: \"petalLength\", label: \"Petal Length (cm)\" },\n  { key: \"petalWidth\", label: \"Petal Width (cm)\" },\n];\nconst n = variables.length;\n\n// --- Layout (square grid, centered, room for title/legend/axis labels) -----\nconst margin = { top: 160, right: 60, bottom: 110, left: 140 };\nconst gridBoxW = width - margin.left - margin.right;\nconst gridBoxH = height - margin.top - margin.bottom;\nconst cell = Math.min(gridBoxW, gridBoxH) / n;\nconst offsetX = margin.left + (gridBoxW - cell * n) / 2;\nconst offsetY = margin.top + (gridBoxH - cell * n) / 2;\nconst inset = cell * 0.12;\n\n// --- Scales: shared per-column (x) and per-row (y) so points align --------\nfunction paddedExtent(values) {\n  const [lo, hi] = d3.extent(values);\n  const pad = (hi - lo) * 0.1 || 1;\n  return [lo - pad, hi + pad];\n}\nconst colScales = variables.map((v) =>\n  d3.scaleLinear().domain(paddedExtent(data.map((d) => d[v.key]))).range([inset, cell - inset])\n);\nconst rowScales = variables.map((v) =>\n  d3.scaleLinear().domain(paddedExtent(data.map((d) => d[v.key]))).range([cell - inset, inset])\n);\nconst color = d3.scaleOrdinal().domain(species.map((s) => s.name)).range(t.palette);\n\n// --- SVG mount ---------------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\n\n// --- Matrix cells: scatter off-diagonal, pooled histogram on the diagonal ---\nfor (let r = 0; r < n; r++) {\n  for (let c = 0; c < n; c++) {\n    const g = svg\n      .append(\"g\")\n      .attr(\"transform\", `translate(${offsetX + c * cell},${offsetY + r * cell})`);\n    g.append(\"rect\")\n      .attr(\"width\", cell)\n      .attr(\"height\", cell)\n      .attr(\"fill\", \"none\")\n      .attr(\"stroke\", t.grid)\n      .attr(\"stroke-width\", 1)\n      .attr(\"stroke-opacity\", 0.35);\n\n    if (r === c) {\n      // Stacked per-species histogram: shared bin boundaries across species so\n      // the diagonal carries the same categorical story as the off-diagonal scatters.\n      const domain = colScales[c].domain();\n      const overallBins = d3.bin().domain(domain).thresholds(10)(data.map((d) => d[variables[c].key]));\n      const thresholds = overallBins.slice(1).map((b) => b.x0);\n      const perSpeciesBins = species.map((sp) => {\n        const values = data.filter((d) => d.species === sp.name).map((d) => d[variables[c].key]);\n        return d3.bin().domain(domain).thresholds(thresholds)(values);\n      });\n      const segments = [];\n      overallBins.forEach((b, i) => {\n        let cum = 0;\n        species.forEach((sp, si) => {\n          const count = perSpeciesBins[si][i].length;\n          if (count > 0) {\n            segments.push({ x0: b.x0, x1: b.x1, y0: cum, y1: cum + count, name: sp.name });\n          }\n          cum += count;\n        });\n      });\n      const yCount = d3\n        .scaleLinear()\n        .domain([0, d3.max(segments, (s) => s.y1) || 1])\n        .nice()\n        .range([cell - inset, inset]);\n      g.selectAll(\"rect.bar\")\n        .data(segments)\n        .join(\"rect\")\n        .attr(\"class\", \"bar\")\n        .attr(\"x\", (s) => colScales[c](s.x0) + 1)\n        .attr(\"y\", (s) => yCount(s.y1))\n        .attr(\"width\", (s) => Math.max(colScales[c](s.x1) - colScales[c](s.x0) - 2, 0))\n        .attr(\"height\", (s) => yCount(s.y0) - yCount(s.y1))\n        .attr(\"fill\", (s) => color(s.name))\n        .attr(\"fill-opacity\", 0.75);\n    } else {\n      g.selectAll(\"circle\")\n        .data(data)\n        .join(\"circle\")\n        .attr(\"cx\", (d) => colScales[c](d[variables[c].key]))\n        .attr(\"cy\", (d) => rowScales[r](d[variables[r].key]))\n        .attr(\"r\", 2.3)\n        .attr(\"fill\", (d) => color(d.species))\n        .attr(\"fill-opacity\", 0.55);\n    }\n\n    if (r === n - 1) {\n      const axis = g\n        .append(\"g\")\n        .attr(\"transform\", `translate(0,${cell - inset})`)\n        .call(d3.axisBottom(colScales[c]).ticks(3).tickSize(4));\n      axis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"12px\");\n      axis.selectAll(\"line\").attr(\"stroke\", t.grid);\n      axis.select(\".domain\").attr(\"stroke\", t.grid);\n    }\n    if (c === 0 && r !== 0) {\n      const axis = g\n        .append(\"g\")\n        .attr(\"transform\", `translate(${inset},0)`)\n        .call(d3.axisLeft(rowScales[r]).ticks(3).tickSize(4));\n      axis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"12px\");\n      axis.selectAll(\"line\").attr(\"stroke\", t.grid);\n      axis.select(\".domain\").attr(\"stroke\", t.grid);\n    }\n  }\n}\n\n// --- Outer boundary: single crisp frame around the whole matrix ------------\nsvg\n  .append(\"rect\")\n  .attr(\"x\", offsetX)\n  .attr(\"y\", offsetY)\n  .attr(\"width\", cell * n)\n  .attr(\"height\", cell * n)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.grid)\n  .attr(\"stroke-width\", 1.5);\n\n// --- Edge labels: variable names along the bottom row and left column ------\nvariables.forEach((v, c) => {\n  svg\n    .append(\"text\")\n    .attr(\"x\", offsetX + c * cell + cell / 2)\n    .attr(\"y\", offsetY + n * cell + 46)\n    .attr(\"text-anchor\", \"middle\")\n    .attr(\"fill\", t.ink)\n    .style(\"font-size\", \"15px\")\n    .text(v.label);\n});\nvariables.forEach((v, r) => {\n  svg\n    .append(\"text\")\n    .attr(\"transform\", `translate(${offsetX - 90},${offsetY + r * cell + cell / 2}) rotate(-90)`)\n    .attr(\"text-anchor\", \"middle\")\n    .attr(\"fill\", t.ink)\n    .style(\"font-size\", \"15px\")\n    .text(v.label);\n});\n\n// --- Legend (species groupings) ---------------------------------------------\nconst legendY = 96;\nconst itemWidth = 190;\nconst legendStartX = width / 2 - (species.length * itemWidth) / 2 + itemWidth / 2;\nspecies.forEach((sp, i) => {\n  const lx = legendStartX + i * itemWidth;\n  svg.append(\"circle\").attr(\"cx\", lx).attr(\"cy\", legendY).attr(\"r\", 7).attr(\"fill\", color(sp.name));\n  svg\n    .append(\"text\")\n    .attr(\"x\", lx + 16)\n    .attr(\"y\", legendY + 5)\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"15px\")\n    .text(sp.name);\n});\n\n// --- Title -------------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 50)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"24px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"scatter-matrix · javascript · d3 · anyplot.ai\");\n"}