{"spec_id":"scatter-marginal","library":"d3","language":"javascript","code":"// anyplot.ai\n// scatter-marginal: Scatter Plot with Marginal Distributions\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 89/100 | Created: 2026-09-09\n\n//# anyplot-orientation: square\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// mulberry32 PRNG + Box-Muller — the browser has no seeded RNG.\nfunction mulberry32(seed) {\n  return function () {\n    seed |= 0;\n    seed = (seed + 0x6d2b79f5) | 0;\n    let v = Math.imul(seed ^ (seed >>> 15), 1 | seed);\n    v = (v + Math.imul(v ^ (v >>> 7), 61 | v)) ^ v;\n    return ((v ^ (v >>> 14)) >>> 0) / 4294967296;\n  };\n}\nfunction randNormal(rng, mean, std) {\n  const u1 = Math.max(rng(), 1e-9);\n  const u2 = rng();\n  const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n  return mean + std * z;\n}\n\nconst rng = mulberry32(42);\nconst n = 320;\nconst points = [];\nfor (let i = 0; i < n; i++) {\n  const sunlight = Math.min(Math.max(randNormal(rng, 6, 1.5), 1), 11);\n  const height_cm = Math.max(14 + 9 * sunlight + randNormal(rng, 0, 7.5), 4);\n  points.push({ sunlight, height_cm });\n}\n\n// --- Layout -------------------------------------------------------------\nconst outerLeft = 100;\nconst outerRight = 40;\nconst outerTop = 90;\nconst outerBottom = 90;\nconst gap = 20;\nconst topPanelH = 190;\nconst rightPanelW = 190;\n\nconst availW = width - outerLeft - outerRight;\nconst availH = height - outerTop - outerBottom;\nconst mainW = availW - rightPanelW - gap;\nconst mainH = availH - topPanelH - gap;\nconst mainX = outerLeft;\nconst mainY = outerTop + topPanelH + gap;\n\n// --- Scales -------------------------------------------------------------\nconst xExtent = d3.extent(points, (d) => d.sunlight);\nconst yExtent = d3.extent(points, (d) => d.height_cm);\nconst xPad = (xExtent[1] - xExtent[0]) * 0.08;\nconst yPad = (yExtent[1] - yExtent[0]) * 0.08;\n\nconst x = d3\n  .scaleLinear()\n  .domain([xExtent[0] - xPad, xExtent[1] + xPad])\n  .range([0, mainW])\n  .nice();\nconst y = d3\n  .scaleLinear()\n  .domain([yExtent[0] - yPad, yExtent[1] + yPad])\n  .range([mainH, 0])\n  .nice();\n\n// --- Marginal histograms + KDE curves ------------------------------------\nconst xBins = d3.bin().domain(x.domain()).thresholds(24)(points.map((d) => d.sunlight));\nconst yBins = d3.bin().domain(y.domain()).thresholds(24)(points.map((d) => d.height_cm));\nconst xCountMax = d3.max(xBins, (d) => d.length) || 1;\nconst yCountMax = d3.max(yBins, (d) => d.length) || 1;\n\nconst topCountScale = d3.scaleLinear().domain([0, xCountMax * 1.15]).range([topPanelH, 0]);\nconst rightCountScale = d3.scaleLinear().domain([0, yCountMax * 1.15]).range([0, rightPanelW]);\n\nfunction kernelEpanechnikov(bandwidth) {\n  return (v) => {\n    v /= bandwidth;\n    return Math.abs(v) <= 1 ? (0.75 * (1 - v * v)) / bandwidth : 0;\n  };\n}\nfunction kernelDensityEstimator(kernel, thresholds) {\n  return (values) => thresholds.map((tv) => [tv, d3.mean(values, (v) => kernel(tv - v))]);\n}\n\n// Silverman's rule of thumb: bw = 1.06 * std * n^(-1/5) — a principled\n// bandwidth selector rather than a fixed fraction of the domain.\nfunction silvermanBandwidth(values) {\n  const std = d3.deviation(values);\n  return 1.06 * std * Math.pow(values.length, -1 / 5);\n}\nconst sunlightValues = points.map((d) => d.sunlight);\nconst heightValues = points.map((d) => d.height_cm);\nconst bwX = silvermanBandwidth(sunlightValues);\nconst bwY = silvermanBandwidth(heightValues);\nconst densityX = kernelDensityEstimator(kernelEpanechnikov(bwX), x.ticks(80))(sunlightValues);\nconst densityY = kernelDensityEstimator(kernelEpanechnikov(bwY), y.ticks(80))(heightValues);\n\nconst topDensityScale = d3\n  .scaleLinear()\n  .domain([0, d3.max(densityX, (d) => d[1]) * 1.15])\n  .range([topPanelH, 0]);\nconst rightDensityScale = d3\n  .scaleLinear()\n  .domain([0, d3.max(densityY, (d) => d[1]) * 1.15])\n  .range([0, rightPanelW]);\n\nconst topLine = d3\n  .line()\n  .curve(d3.curveBasis)\n  .x((d) => x(d[0]))\n  .y((d) => topDensityScale(d[1]));\nconst rightLine = d3\n  .line()\n  .curve(d3.curveBasis)\n  .x((d) => rightDensityScale(d[1]))\n  .y((d) => y(d[0]));\n\n// --- SVG mount ------------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\n\n// --- Main scatter -----------------------------------------------------------\nconst mainG = svg.append(\"g\").attr(\"transform\", `translate(${mainX},${mainY})`);\n\nmainG\n  .append(\"g\")\n  .selectAll(\"line\")\n  .data(x.ticks(7))\n  .join(\"line\")\n  .attr(\"x1\", (d) => x(d))\n  .attr(\"x2\", (d) => x(d))\n  .attr(\"y1\", 0)\n  .attr(\"y2\", mainH)\n  .attr(\"stroke\", t.grid);\nmainG\n  .append(\"g\")\n  .selectAll(\"line\")\n  .data(y.ticks(7))\n  .join(\"line\")\n  .attr(\"y1\", (d) => y(d))\n  .attr(\"y2\", (d) => y(d))\n  .attr(\"x1\", 0)\n  .attr(\"x2\", mainW)\n  .attr(\"stroke\", t.grid);\n\nconst xAxisG = mainG.append(\"g\").attr(\"transform\", `translate(0,${mainH})`).call(d3.axisBottom(x).ticks(7));\nconst yAxisG = mainG.append(\"g\").call(d3.axisLeft(y).ticks(7));\nfor (const ax of [xAxisG, yAxisG]) {\n  ax.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"16px\");\n  ax.selectAll(\"line\").attr(\"stroke\", t.grid);\n  ax.select(\".domain\").attr(\"stroke\", t.inkSoft);\n}\n\nmainG\n  .append(\"g\")\n  .selectAll(\"circle\")\n  .data(points)\n  .join(\"circle\")\n  .attr(\"cx\", (d) => x(d.sunlight))\n  .attr(\"cy\", (d) => y(d.height_cm))\n  .attr(\"r\", 4)\n  .attr(\"fill\", t.palette[0])\n  .attr(\"fill-opacity\", 0.65)\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 0.6);\n\nmainG\n  .append(\"text\")\n  .attr(\"x\", mainW / 2)\n  .attr(\"y\", mainH + 64)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"18px\")\n  .text(\"Daily Sunlight Exposure (hours)\");\n\nmainG\n  .append(\"text\")\n  .attr(\"transform\", `translate(${-72},${mainH / 2}) rotate(-90)`)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"18px\")\n  .text(\"Seedling Height after 30 Days (cm)\");\n\n// --- Top marginal (x distribution) -------------------------------------\nconst topG = svg.append(\"g\").attr(\"transform\", `translate(${mainX},${outerTop})`);\n\ntopG\n  .append(\"rect\")\n  .attr(\"class\", \"panel-frame\")\n  .attr(\"x\", 0)\n  .attr(\"y\", 0)\n  .attr(\"width\", mainW)\n  .attr(\"height\", topPanelH)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.grid);\n\ntopG\n  .selectAll(\"rect.bar\")\n  .data(xBins)\n  .join(\"rect\")\n  .attr(\"class\", \"bar\")\n  .attr(\"x\", (d) => x(d.x0) + 1)\n  .attr(\"width\", (d) => Math.max(0, x(d.x1) - x(d.x0) - 2))\n  .attr(\"y\", (d) => topCountScale(d.length))\n  .attr(\"height\", (d) => topPanelH - topCountScale(d.length))\n  .attr(\"fill\", t.inkSoft)\n  .attr(\"fill-opacity\", 0.35);\n\ntopG.append(\"path\").datum(densityX).attr(\"fill\", \"none\").attr(\"stroke\", t.inkSoft).attr(\"stroke-width\", 2.5).attr(\"d\", topLine);\n\n// --- Right marginal (y distribution) -------------------------------------\nconst rightG = svg.append(\"g\").attr(\"transform\", `translate(${mainX + mainW + gap},${mainY})`);\n\nrightG\n  .append(\"rect\")\n  .attr(\"class\", \"panel-frame\")\n  .attr(\"x\", 0)\n  .attr(\"y\", 0)\n  .attr(\"width\", rightPanelW)\n  .attr(\"height\", mainH)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.grid);\n\nrightG\n  .selectAll(\"rect.bar\")\n  .data(yBins)\n  .join(\"rect\")\n  .attr(\"class\", \"bar\")\n  .attr(\"x\", 0)\n  .attr(\"width\", (d) => rightCountScale(d.length))\n  .attr(\"y\", (d) => y(d.x1) + 1)\n  .attr(\"height\", (d) => Math.max(0, y(d.x0) - y(d.x1) - 2))\n  .attr(\"fill\", t.inkSoft)\n  .attr(\"fill-opacity\", 0.35);\n\nrightG\n  .append(\"path\")\n  .datum(densityY)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", 2.5)\n  .attr(\"d\", rightLine);\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\", \"26px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"scatter-marginal · javascript · d3 · anyplot.ai\");\n"}