{"spec_id":"scatter-marginal","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// scatter-marginal: Scatter Plot with Marginal Distributions\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 89/100 | Created: 2026-09-09\n//# anyplot-orientation: square\n// anyplot.ai\n// scatter-marginal: Scatter Plot with Marginal Distributions\n// Library: Highcharts 12.6.0 | Node 22\n// License: Highcharts — commercial license, free for non-commercial use (highcharts.com/license)\n// Quality: pending | Created: 2026-09-09\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic LCG) ------------------------------------\n// Annual rainfall vs. crop yield across 400 farm plots — positively correlated\n// measurement data, a classic marginal-distribution use case (skew + outliers\n// visible on each axis alongside the joint relationship).\nfunction makeRng(seed) {\n  let state = seed >>> 0;\n  return function () {\n    state = (1103515245 * state + 12345) >>> 0;\n    return state / 4294967296;\n  };\n}\nconst rng = makeRng(20260909);\nfunction randNormal() {\n  const u1 = Math.max(rng(), 1e-12);\n  const u2 = rng();\n  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\n\nconst N = 400;\nconst rainfallMm = [];\nconst yieldTonsPerHa = [];\nfor (let i = 0; i < N; i++) {\n  const rainfall = Math.min(1500, Math.max(300, 900 + randNormal() * 190));\n  const yieldTons = Math.min(8.5, Math.max(0.3, 0.0028 * rainfall + randNormal() * 0.55 + 1.0));\n  rainfallMm.push(Math.round(rainfall));\n  yieldTonsPerHa.push(Math.round(yieldTons * 100) / 100);\n}\n\nconst xDataMin = Math.min(...rainfallMm);\nconst xDataMax = Math.max(...rainfallMm);\nconst yDataMin = Math.min(...yieldTonsPerHa);\nconst yDataMax = Math.max(...yieldTonsPerHa);\nconst xPad = (xDataMax - xDataMin) * 0.06;\nconst yPad = (yDataMax - yDataMin) * 0.08;\nconst xAxisMin = Math.floor(xDataMin - xPad);\nconst xAxisMax = Math.ceil(xDataMax + xPad);\nconst yAxisMin = Math.max(0, Math.floor((yDataMin - yPad) * 10) / 10);\nconst yAxisMax = Math.ceil((yDataMax + yPad) * 10) / 10;\n\n// --- Marginal histograms -----------------------------------------------------\nfunction histogram(values, min, max, bins) {\n  const width = (max - min) / bins;\n  const counts = new Array(bins).fill(0);\n  values.forEach((v) => {\n    let idx = Math.floor((v - min) / width);\n    if (idx < 0) idx = 0;\n    if (idx >= bins) idx = bins - 1;\n    counts[idx]++;\n  });\n  return { counts, width };\n}\n\nconst xHist = histogram(rainfallMm, xAxisMin, xAxisMax, 26);\nconst yHist = histogram(yieldTonsPerHa, yAxisMin, yAxisMax, 20);\n\nconst scatterData = rainfallMm.map((r, i) => [r, yieldTonsPerHa[i]]);\nconst topHistData = xHist.counts.map((c, i) => [xAxisMin + xHist.width * (i + 0.5), c]);\nconst rightHistData = yHist.counts.map((c, i) => [yAxisMin + yHist.width * (i + 0.5), c]);\n\n// Gaussian KDE overlay (Silverman bandwidth) on each marginal, scaled to the\n// histogram's count axis (density * N * binWidth) so the curve reads directly\n// against the bars behind it — adds distributional shape beyond raw bin counts.\nfunction stdDev(values) {\n  const mean = values.reduce((a, b) => a + b, 0) / values.length;\n  const variance = values.reduce((a, b) => a + (b - mean) * (b - mean), 0) / values.length;\n  return Math.sqrt(variance);\n}\nfunction kdeCurve(values, min, max, binWidth, gridPoints) {\n  const n = values.length;\n  const bandwidth = Math.max(1e-6, 1.06 * stdDev(values) * Math.pow(n, -0.2));\n  const step = (max - min) / (gridPoints - 1);\n  const points = [];\n  for (let i = 0; i < gridPoints; i++) {\n    const x = min + step * i;\n    let sum = 0;\n    for (const v of values) {\n      const u = (x - v) / bandwidth;\n      sum += Math.exp(-0.5 * u * u);\n    }\n    const density = sum / (n * bandwidth * Math.sqrt(2 * Math.PI));\n    points.push([x, density * n * binWidth]);\n  }\n  return points;\n}\nconst xKde = kdeCurve(rainfallMm, xAxisMin, xAxisMax, xHist.width, 80);\nconst yKde = kdeCurve(yieldTonsPerHa, yAxisMin, yAxisMax, yHist.width, 80);\n\nconst scatterColor = Highcharts.color(t.palette[0]).setOpacity(0.6).get();\nconst marginalColor = Highcharts.color(t.palette[0]).setOpacity(0.4).get();\nconst kdeLineColor = t.palette[0];\n\n// --- Mount layout: title strip + top marginal + main scatter + right marginal\nconst container = document.getElementById(\"container\");\ncontainer.style.display = \"grid\";\ncontainer.style.gridTemplateColumns = \"900px 300px\";\ncontainer.style.gridTemplateRows = \"70px 280px 850px\";\n\nconst titleEl = document.createElement(\"div\");\ntitleEl.style.gridColumn = \"1 / 3\";\ntitleEl.style.gridRow = \"1\";\ntitleEl.style.display = \"flex\";\ntitleEl.style.alignItems = \"center\";\ntitleEl.style.justifyContent = \"center\";\ntitleEl.style.color = t.ink;\ntitleEl.style.fontSize = \"22px\";\ntitleEl.style.fontWeight = \"600\";\ntitleEl.textContent = \"scatter-marginal · javascript · highcharts · anyplot.ai\";\ncontainer.appendChild(titleEl);\n\nconst topEl = document.createElement(\"div\");\ntopEl.style.gridColumn = \"1\";\ntopEl.style.gridRow = \"2\";\ncontainer.appendChild(topEl);\n\nconst mainEl = document.createElement(\"div\");\nmainEl.style.gridColumn = \"1\";\nmainEl.style.gridRow = \"3\";\ncontainer.appendChild(mainEl);\n\nconst rightEl = document.createElement(\"div\");\nrightEl.style.gridColumn = \"2\";\nrightEl.style.gridRow = \"3\";\ncontainer.appendChild(rightEl);\n\n// Shared margins so the value axes line up pixel-for-pixel across panels:\n// topEl/mainEl share marginLeft+marginRight (x alignment); mainEl/rightEl\n// share marginTop+marginBottom (y alignment).\nconst MARGIN_TOP = 10;\nconst MARGIN_BOTTOM = 90;\nconst MARGIN_LEFT = 90;\nconst MARGIN_RIGHT = 20;\n\nconst baseChart = {\n  backgroundColor: \"transparent\",\n  animation: false,\n  style: { fontFamily: \"inherit\" },\n};\n\nHighcharts.chart(topEl, {\n  chart: { ...baseChart, type: \"column\", marginTop: 10, marginBottom: 10, marginLeft: MARGIN_LEFT, marginRight: MARGIN_RIGHT },\n  credits: { enabled: false },\n  title: { text: null },\n  xAxis: {\n    min: xAxisMin,\n    max: xAxisMax,\n    lineColor: t.inkSoft,\n    tickLength: 0,\n    gridLineColor: t.grid,\n    labels: { enabled: false },\n    title: { text: null },\n  },\n  yAxis: {\n    min: 0,\n    gridLineColor: t.grid,\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    labels: { style: { color: t.inkSoft, fontSize: \"13px\" } },\n    title: { text: \"Count\", style: { color: t.inkSoft, fontSize: \"14px\" } },\n  },\n  legend: { enabled: false },\n  plotOptions: {\n    series: { animation: false },\n    column: { pointPadding: 0.03, groupPadding: 0, borderWidth: 0, pointRange: xHist.width, color: marginalColor },\n  },\n  series: [\n    { name: \"Rainfall distribution\", data: topHistData },\n    {\n      name: \"Density estimate\",\n      type: \"spline\",\n      data: xKde,\n      color: kdeLineColor,\n      lineWidth: 2,\n      marker: { enabled: false },\n      enableMouseTracking: false,\n    },\n  ],\n});\n\nHighcharts.chart(mainEl, {\n  chart: { ...baseChart, type: \"scatter\", marginTop: MARGIN_TOP, marginBottom: MARGIN_BOTTOM, marginLeft: MARGIN_LEFT, marginRight: MARGIN_RIGHT },\n  credits: { enabled: false },\n  title: { text: null },\n  xAxis: {\n    min: xAxisMin,\n    max: xAxisMax,\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    gridLineColor: t.grid,\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n    title: { text: \"Annual Rainfall (mm)\", style: { color: t.inkSoft, fontSize: \"16px\" } },\n  },\n  yAxis: {\n    min: yAxisMin,\n    max: yAxisMax,\n    gridLineColor: t.grid,\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n    title: { text: \"Crop Yield (tons/hectare)\", style: { color: t.inkSoft, fontSize: \"16px\" } },\n  },\n  legend: { enabled: false },\n  plotOptions: { series: { animation: false } },\n  series: [\n    {\n      name: \"Farm plots\",\n      data: scatterData,\n      color: scatterColor,\n      marker: { radius: 4, symbol: \"circle\", lineColor: t.pageBg, lineWidth: 0.5 },\n    },\n  ],\n});\n\nHighcharts.chart(rightEl, {\n  chart: { ...baseChart, type: \"bar\", marginTop: MARGIN_TOP, marginBottom: MARGIN_BOTTOM, marginLeft: 10, marginRight: 20 },\n  credits: { enabled: false },\n  title: { text: null },\n  xAxis: {\n    min: yAxisMin,\n    max: yAxisMax,\n    lineColor: t.inkSoft,\n    tickLength: 0,\n    gridLineColor: t.grid,\n    labels: { enabled: false },\n    title: { text: null },\n  },\n  yAxis: {\n    min: 0,\n    gridLineColor: t.grid,\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    labels: { style: { color: t.inkSoft, fontSize: \"13px\" } },\n    title: { text: \"Count\", style: { color: t.inkSoft, fontSize: \"14px\" } },\n  },\n  legend: { enabled: false },\n  plotOptions: {\n    series: { animation: false },\n    bar: { pointPadding: 0.03, groupPadding: 0, borderWidth: 0, pointRange: yHist.width, color: marginalColor },\n  },\n  series: [\n    { name: \"Yield distribution\", data: rightHistData },\n    {\n      name: \"Density estimate\",\n      type: \"spline\",\n      data: yKde,\n      color: kdeLineColor,\n      lineWidth: 2,\n      marker: { enabled: false },\n      enableMouseTracking: false,\n    },\n  ],\n});\n"}