{"spec_id":"scatter-marginal","library":"muix","language":"javascript","code":"// anyplot.ai\n// scatter-marginal: Scatter Plot with Marginal Distributions\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 87/100 | Created: 2026-09-09\n//# anyplot-orientation: square\n// anyplot.ai\n// scatter-marginal: Scatter Plot with Marginal Distributions\n// Library: MUI X Charts | React | Node 22\n// License: @mui/x-charts — MIT (community). Pro/Premium are out of scope.\n// Quality: pending | Created: 2026-09-09\nimport { ScatterChart } from \"@mui/x-charts/ScatterChart\";\nimport { BarChart } from \"@mui/x-charts/BarChart\";\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Field-trial data: seasonal rainfall vs. crop yield across 400 plots. Yield\n// tracks rainfall with a moderate positive correlation plus agronomic noise,\n// so both the joint relationship and each variable's own spread are visible.\nlet seed = 42;\nconst nextRandom = () => {\n  seed = (Math.imul(seed, 1103515245) + 12345) & 0x7fffffff;\n  return seed / 0x7fffffff;\n};\nconst nextGaussian = () => {\n  const u1 = Math.max(nextRandom(), 1e-9);\n  const u2 = nextRandom();\n  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n};\n\nconst POINT_COUNT = 400;\nconst points = [];\nfor (let i = 0; i < POINT_COUNT; i += 1) {\n  const rainfall = 800 + 150 * nextGaussian();\n  const yieldPerHectare = Math.max(0.6, 1.6 + 0.0046 * rainfall + 0.7 * nextGaussian());\n  points.push({\n    id: i,\n    x: Number(rainfall.toFixed(1)),\n    y: Number(yieldPerHectare.toFixed(2)),\n  });\n}\n\nconst rainfallValues = points.map((p) => p.x);\nconst yieldValues = points.map((p) => p.y);\n\n// Shared axis domains — the SAME min/max drive both the main scatter's axes\n// and the marginal histograms' binning, which is what keeps the three panels\n// pixel-aligned.\nconst domainPad = (values) => {\n  const lo = Math.min(...values);\n  const hi = Math.max(...values);\n  const pad = (hi - lo) * 0.06;\n  return [lo - pad, hi + pad];\n};\nconst [xMin, xMax] = domainPad(rainfallValues);\nconst [yMin, yMax] = domainPad(yieldValues);\n\nconst BIN_COUNT = 22;\nconst histogram = (values, min, max, bins) => {\n  const binWidth = (max - min) / bins;\n  const counts = new Array(bins).fill(0);\n  values.forEach((value) => {\n    const idx = Math.min(bins - 1, Math.max(0, Math.floor((value - min) / binWidth)));\n    counts[idx] += 1;\n  });\n  const centers = counts.map((_, i) => Number((min + (i + 0.5) * binWidth).toFixed(2)));\n  return { counts, centers };\n};\nconst { counts: countsX, centers: centersX } = histogram(rainfallValues, xMin, xMax, BIN_COUNT);\nconst { counts: countsY, centers: centersY } = histogram(yieldValues, yMin, yMax, BIN_COUNT);\nconst maxCountX = Math.max(...countsX);\nconst maxCountY = Math.max(...countsY);\n\n// Points get moderate transparency to reveal density (spec: alpha ~0.6-0.7);\n// marginal histograms stay subtle so they don't compete with the scatter.\nconst 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};\nconst BRAND = t.palette[0];\nconst MARKER_FILL = withAlpha(BRAND, 0.65);\nconst MARGINAL_FILL = withAlpha(BRAND, 0.32);\n\n// --- Layout — main scatter lower-left, marginal histograms top + right -----\nconst TITLE_H = 72;\nconst GAP = 24;\nconst MARGIN_PANEL = 250;\nconst plotAreaH = height - TITLE_H;\nconst mainWidth = width - GAP - MARGIN_PANEL;\nconst mainHeight = plotAreaH - GAP - MARGIN_PANEL;\n// Shared left/right and top/bottom margins keep the histogram bins aligned\n// with the main plot's axis ticks (same drawable-area geometry on both axes).\nconst MAIN_MARGIN = { left: 96, right: 20, top: 16, bottom: 78 };\n\n// --- Title (fontsize scales with title length, see plot-generator.md) -------\nconst TITLE = \"scatter-marginal · javascript · muix · anyplot.ai\";\nconst TITLE_FONTSIZE = Math.round(22 * (TITLE.length > 67 ? 67 / TITLE.length : 1));\n\nconst HIDE_AXIS_SX = { \"& .MuiChartsAxis-root\": { display: \"none\" } };\n// Thin background-colored strokes separate adjacent marks (bars) and\n// overlapping marks (scatter points) from one another — same edge color as\n// the page background so it reads as a \"cutout\" gap in both themes.\nconst MARGIN_BAR_SX = { ...HIDE_AXIS_SX, \"& .MuiBarElement-root\": { stroke: t.pageBg, strokeWidth: 1 } };\nconst SCATTER_MARKER_SX = { \"& circle\": { stroke: t.pageBg, strokeWidth: 1 } };\n\n// --- Chart (default-exported component — the harness mounts it) -------------\nexport default function Chart() {\n  return (\n    <div style={{ width, height, display: \"flex\", flexDirection: \"column\" }}>\n      <div\n        style={{\n          height: TITLE_H,\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          fontSize: TITLE_FONTSIZE,\n          fontWeight: 600,\n          color: t.ink,\n          fontFamily: \"Roboto, Helvetica, Arial, sans-serif\",\n        }}\n      >\n        {TITLE}\n      </div>\n      <div\n        style={{\n          width,\n          height: plotAreaH,\n          display: \"grid\",\n          gridTemplateColumns: `${mainWidth}px ${GAP}px ${MARGIN_PANEL}px`,\n          gridTemplateRows: `${MARGIN_PANEL}px ${GAP}px ${mainHeight}px`,\n        }}\n      >\n        <div style={{ gridColumn: 1, gridRow: 1 }}>\n          <BarChart\n            width={mainWidth}\n            height={MARGIN_PANEL}\n            skipAnimation\n            legend={{ hidden: true }}\n            margin={{ left: MAIN_MARGIN.left, right: MAIN_MARGIN.right, top: 14, bottom: 6 }}\n            xAxis={[{ scaleType: \"band\", data: centersX, categoryGapRatio: 0 }]}\n            yAxis={[{ min: 0, max: maxCountX * 1.08 }]}\n            series={[{ data: countsX, color: MARGINAL_FILL }]}\n            sx={MARGIN_BAR_SX}\n          />\n        </div>\n        <div style={{ gridColumn: 1, gridRow: 3 }}>\n          <ScatterChart\n            width={mainWidth}\n            height={mainHeight}\n            skipAnimation\n            legend={{ hidden: true }}\n            grid={{ horizontal: true, vertical: true }}\n            margin={MAIN_MARGIN}\n            xAxis={[\n              {\n                min: xMin,\n                max: xMax,\n                label: \"Annual Rainfall (mm)\",\n                labelStyle: { fontSize: 16, fill: t.ink },\n                tickLabelStyle: { fontSize: 13, fill: t.inkSoft },\n              },\n            ]}\n            yAxis={[\n              {\n                min: yMin,\n                max: yMax,\n                label: \"Crop Yield (t/ha)\",\n                labelStyle: { fontSize: 16, fill: t.ink },\n                tickLabelStyle: { fontSize: 13, fill: t.inkSoft },\n              },\n            ]}\n            series={[{ data: points, markerSize: 10, color: MARKER_FILL }]}\n            sx={SCATTER_MARKER_SX}\n          />\n        </div>\n        <div style={{ gridColumn: 3, gridRow: 3 }}>\n          <BarChart\n            layout=\"horizontal\"\n            width={MARGIN_PANEL}\n            height={mainHeight}\n            skipAnimation\n            legend={{ hidden: true }}\n            margin={{ top: MAIN_MARGIN.top, bottom: MAIN_MARGIN.bottom, left: 6, right: 14 }}\n            yAxis={[{ scaleType: \"band\", data: centersY, categoryGapRatio: 0 }]}\n            xAxis={[{ min: 0, max: maxCountY * 1.08 }]}\n            series={[{ data: countsY, color: MARGINAL_FILL }]}\n            sx={MARGIN_BAR_SX}\n          />\n        </div>\n      </div>\n    </div>\n  );\n}\n"}