{"spec_id":"scatter-marginal","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// scatter-marginal: Scatter Plot with Marginal Distributions\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 85/100 | Created: 2026-09-09\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic LCG + Box-Muller) -----------------------\nlet seed = 42;\nfunction lcgRandom() {\n  seed = (seed * 1664525 + 1013904223) % 4294967296;\n  return seed / 4294967296;\n}\nfunction randn() {\n  const u1 = Math.max(lcgRandom(), 1e-9);\n  const u2 = lcgRandom();\n  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\n\nconst N = 400;\nconst RHO = -0.72;\nconst displacement = [];\nconst fuelEconomy = [];\nfor (let i = 0; i < N; i++) {\n  const z1 = randn();\n  const z2 = randn();\n  const zy = RHO * z1 + Math.sqrt(1 - RHO * RHO) * z2;\n  displacement.push(Math.max(1.0, 2.8 + z1 * 1.0));\n  fuelEconomy.push(Math.max(10, 32 + zy * 6.5));\n}\nconst points = displacement.map((x, i) => ({ x, y: fuelEconomy[i] }));\n\n// \"Nice number\" axis rounding (Heckbert's algorithm) so the outer scale bounds\n// land on round values (e.g. 1.0, 6.5) instead of the raw, jagged data extent.\nfunction niceNum(range, round) {\n  const exponent = Math.floor(Math.log10(range));\n  const fraction = range / Math.pow(10, exponent);\n  let niceFraction;\n  if (round) {\n    if (fraction < 1.5) niceFraction = 1;\n    else if (fraction < 3) niceFraction = 2;\n    else if (fraction < 7) niceFraction = 5;\n    else niceFraction = 10;\n  } else if (fraction <= 1) {\n    niceFraction = 1;\n  } else if (fraction <= 2) {\n    niceFraction = 2;\n  } else if (fraction <= 5) {\n    niceFraction = 5;\n  } else {\n    niceFraction = 10;\n  }\n  return niceFraction * Math.pow(10, exponent);\n}\n\nfunction niceDomain(min, max, targetTicks = 8) {\n  const step = niceNum(niceNum(max - min, false) / (targetTicks - 1), true);\n  return { min: Math.floor(min / step) * step, max: Math.ceil(max / step) * step };\n}\n\nfunction histogram(data, binCount, domainMin, domainMax) {\n  const binWidth = (domainMax - domainMin) / binCount;\n  const counts = new Array(binCount).fill(0);\n  data.forEach((value) => {\n    let idx = Math.floor((value - domainMin) / binWidth);\n    idx = Math.min(Math.max(idx, 0), binCount - 1);\n    counts[idx]++;\n  });\n  const centers = counts.map((_, i) => domainMin + (i + 0.5) * binWidth);\n  return { counts, centers };\n}\n\nconst BIN_COUNT = 22;\nconst xDomain = niceDomain(Math.min(...displacement), Math.max(...displacement));\nconst yDomain = niceDomain(Math.min(...fuelEconomy), Math.max(...fuelEconomy));\nconst xHist = histogram(displacement, BIN_COUNT, xDomain.min, xDomain.max);\nconst yHist = histogram(fuelEconomy, BIN_COUNT, yDomain.min, yDomain.max);\n\nfunction hexToRgba(hex, alpha) {\n  const r = parseInt(hex.slice(1, 3), 16);\n  const g = parseInt(hex.slice(3, 5), 16);\n  const b = parseInt(hex.slice(5, 7), 16);\n  return `rgba(${r}, ${g}, ${b}, ${alpha})`;\n}\n\nconst BRAND = t.palette[0];\nconst MARGINAL_COLOR = hexToRgba(BRAND, 0.4);\nconst MARGINAL_BORDER = hexToRgba(BRAND, 0.7);\n\n// Chart.js linear scales always force a tick at the exact min/max, which can\n// crowd the neighbouring \"nice\" step tick when min/max don't land on a round\n// number. Drop a boundary tick that sits too close to its neighbour.\nfunction pruneBoundaryTicks(scale) {\n  const ticks = scale.ticks;\n  if (ticks.length < 3) return;\n  const threshold = (scale.max - scale.min) * 0.04;\n  if (ticks[1].value - ticks[0].value < threshold) ticks.shift();\n  if (ticks[ticks.length - 1].value - ticks[ticks.length - 2].value < threshold) ticks.pop();\n}\n\n// --- Layout ------------------------------------------------------------------\n// Fixed reserves (CSS px) shared between the main chart and the hidden axes of\n// the marginal charts, so the plot areas line up pixel-for-pixel.\nconst Y_AXIS_RESERVE = 100; // main y-axis (ticks + title) width\nconst X_AXIS_RESERVE = 70; // main x-axis (ticks + title) height\nconst TOP_MARGINAL_SIZE = 190;\nconst RIGHT_MARGINAL_SIZE = 230;\nconst TITLE_SIZE = 60;\n\nconst container = document.getElementById(\"container\");\ncontainer.style.display = \"grid\";\ncontainer.style.gridTemplateColumns = `1fr ${RIGHT_MARGINAL_SIZE}px`;\ncontainer.style.gridTemplateRows = `${TITLE_SIZE}px ${TOP_MARGINAL_SIZE}px 1fr`;\ncontainer.style.fontFamily = \"inherit\";\n\nconst titleCell = document.createElement(\"div\");\ntitleCell.style.gridColumn = \"1 / span 2\";\ntitleCell.style.display = \"flex\";\ntitleCell.style.alignItems = \"center\";\ntitleCell.style.justifyContent = \"center\";\ntitleCell.style.color = t.ink;\ntitleCell.style.fontSize = \"26px\";\ntitleCell.style.fontWeight = \"600\";\ntitleCell.textContent = \"scatter-marginal · javascript · chartjs · anyplot.ai\";\ncontainer.appendChild(titleCell);\n\nconst topCell = document.createElement(\"div\");\nconst cornerCell = document.createElement(\"div\");\nconst mainCell = document.createElement(\"div\");\nconst rightCell = document.createElement(\"div\");\n[topCell, mainCell, cornerCell, rightCell].forEach((cell) => {\n  cell.style.position = \"relative\";\n  cell.style.width = \"100%\";\n  cell.style.height = \"100%\";\n});\ncontainer.appendChild(topCell);\ncontainer.appendChild(cornerCell);\ncontainer.appendChild(mainCell);\ncontainer.appendChild(rightCell);\n\nfunction makeCanvas(cell) {\n  const canvas = document.createElement(\"canvas\");\n  cell.appendChild(canvas);\n  return canvas;\n}\n\n// --- Main scatter --------------------------------------------------------\nnew Chart(makeCanvas(mainCell), {\n  type: \"scatter\",\n  data: {\n    datasets: [\n      {\n        label: \"Vehicles\",\n        data: points,\n        backgroundColor: hexToRgba(BRAND, 0.65),\n        borderColor: t.pageBg,\n        borderWidth: 0.5,\n        radius: 5,\n        hoverRadius: 6,\n      },\n    ],\n  },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    plugins: { legend: { display: false } },\n    scales: {\n      x: {\n        type: \"linear\",\n        min: xDomain.min,\n        max: xDomain.max,\n        title: { display: true, text: \"Engine Displacement (L)\", color: t.ink, font: { size: 16 } },\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n        grid: { color: t.grid },\n        afterBuildTicks: pruneBoundaryTicks,\n        afterFit: (scale) => {\n          scale.height = X_AXIS_RESERVE;\n        },\n      },\n      y: {\n        type: \"linear\",\n        min: yDomain.min,\n        max: yDomain.max,\n        title: { display: true, text: \"Fuel Economy (mpg)\", color: t.ink, font: { size: 16 } },\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n        grid: { color: t.grid },\n        afterBuildTicks: pruneBoundaryTicks,\n        afterFit: (scale) => {\n          scale.width = Y_AXIS_RESERVE;\n        },\n      },\n    },\n  },\n});\n\n// --- Top marginal: distribution of x (Engine Displacement) -----------------\nnew Chart(makeCanvas(topCell), {\n  type: \"bar\",\n  data: {\n    datasets: [\n      {\n        data: xHist.centers.map((center, i) => ({ x: center, y: xHist.counts[i] })),\n        backgroundColor: MARGINAL_COLOR,\n        borderColor: MARGINAL_BORDER,\n        borderWidth: 1,\n        borderRadius: { topLeft: 2, topRight: 2 },\n        borderSkipped: false,\n        barPercentage: 1.0,\n        categoryPercentage: 0.95,\n      },\n    ],\n  },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    plugins: { legend: { display: false } },\n    scales: {\n      x: {\n        type: \"linear\",\n        min: xDomain.min,\n        max: xDomain.max,\n        display: true,\n        ticks: { display: false },\n        grid: { display: false },\n        border: { display: false },\n      },\n      y: {\n        type: \"linear\",\n        beginAtZero: true,\n        display: true,\n        ticks: { display: false },\n        grid: { display: false },\n        border: { display: false },\n        afterFit: (scale) => {\n          scale.width = Y_AXIS_RESERVE;\n        },\n      },\n    },\n  },\n});\n\n// --- Right marginal: distribution of y (Fuel Economy) -----------------------\nnew Chart(makeCanvas(rightCell), {\n  type: \"bar\",\n  data: {\n    datasets: [\n      {\n        data: yHist.centers.map((center, i) => ({ x: yHist.counts[i], y: center })),\n        backgroundColor: MARGINAL_COLOR,\n        borderColor: MARGINAL_BORDER,\n        borderWidth: 1,\n        borderRadius: { topRight: 2, bottomRight: 2 },\n        borderSkipped: false,\n        barPercentage: 1.0,\n        categoryPercentage: 0.95,\n      },\n    ],\n  },\n  options: {\n    indexAxis: \"y\",\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    plugins: { legend: { display: false } },\n    scales: {\n      x: {\n        type: \"linear\",\n        beginAtZero: true,\n        display: true,\n        ticks: { display: false },\n        grid: { display: false },\n        border: { display: false },\n        afterFit: (scale) => {\n          scale.height = X_AXIS_RESERVE;\n        },\n      },\n      y: {\n        type: \"linear\",\n        min: yDomain.min,\n        max: yDomain.max,\n        display: true,\n        ticks: { display: false },\n        grid: { display: false },\n        border: { display: false },\n      },\n    },\n  },\n});\n"}