{"spec_id":"scatter-marginal","library":"echarts","language":"javascript","code":"// anyplot.ai\n// scatter-marginal: Scatter Plot with Marginal Distributions\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-09-09\n\nconst t = window.ANYPLOT_TOKENS;\nconst size = window.ANYPLOT_SIZE;\n\n// --- Deterministic PRNG (LCG) + Box-Muller normal samples -------------------\nlet seed = 42;\nfunction lcg() {\n  seed = (seed * 1103515245 + 12345) & 0x7fffffff;\n  return seed / 0x7fffffff;\n}\nfunction randNormal() {\n  const u1 = Math.max(lcg(), 1e-9);\n  const u2 = lcg();\n  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\n\n// --- Data: daily commute distance vs. commute time ---------------------------\nconst N = 420;\nconst points = [];\nfor (let i = 0; i < N; i++) {\n  const distance = Math.min(34, Math.max(1, 12 + 5 * randNormal()));\n  const time = Math.min(95, Math.max(3, 2.1 * distance + 5 + 6 * randNormal()));\n  points.push([distance, time]);\n}\nconst xs = points.map((p) => p[0]);\nconst ys = points.map((p) => p[1]);\n\n// --- Histogram binning --------------------------------------------------------\nfunction histogram(values, min, max, binCount) {\n  const binWidth = (max - min) / binCount;\n  const counts = new Array(binCount).fill(0);\n  values.forEach((v) => {\n    let idx = Math.floor((v - min) / binWidth);\n    if (idx >= binCount) idx = binCount - 1;\n    if (idx < 0) idx = 0;\n    counts[idx]++;\n  });\n  return counts.map((count, i) => ({\n    start: min + i * binWidth,\n    end: min + (i + 1) * binWidth,\n    count,\n  }));\n}\n\nconst xPad = (Math.max(...xs) - Math.min(...xs)) * 0.08;\nconst yPad = (Math.max(...ys) - Math.min(...ys)) * 0.08;\nconst xMin = Math.min(...xs) - xPad;\nconst xMax = Math.max(...xs) + xPad;\nconst yMin = Math.min(...ys) - yPad;\nconst yMax = Math.max(...ys) + yPad;\n\nconst BIN_COUNT = 18;\nconst xBins = histogram(xs, xMin, xMax, BIN_COUNT);\nconst yBins = histogram(ys, yMin, yMax, BIN_COUNT);\nconst xCountMax = Math.max(...xBins.map((b) => b.count)) * 1.15;\nconst yCountMax = Math.max(...yBins.map((b) => b.count)) * 1.15;\n\n// --- Layout: three grids sharing pixel-aligned axis ranges -------------------\nconst W = (size && size.width) || 1600;\nconst H = (size && size.height) || 900;\nconst OUTER = 20;\nconst GRID_LEFT = 100;\nconst TOP_MARGIN = 90;\nconst TOP_HIST_H = 160;\nconst RIGHT_HIST_W = 170;\nconst GAP = 14;\nconst BOTTOM_MARGIN = 90;\n\nconst mainRight = OUTER + RIGHT_HIST_W + GAP;\nconst mainTop = TOP_MARGIN + TOP_HIST_H + GAP;\n\nconst mainGrid = { left: GRID_LEFT, right: mainRight, top: mainTop, bottom: BOTTOM_MARGIN };\nconst topGrid = { left: GRID_LEFT, right: mainRight, top: TOP_MARGIN, bottom: H - mainTop };\nconst rightGrid = { left: W - OUTER - RIGHT_HIST_W, right: OUTER, top: mainTop, bottom: BOTTOM_MARGIN };\n\n// Subtle, theme-adaptive fill for the marginal histograms so they read as\n// context rather than competing with the brand-green scatter points.\nconst MARGINAL_COLOR = t.inkSoft;\n\n// A custom \"rect\" renderer draws each histogram bar directly in pixel space\n// via api.coord(), which is the only way to get true value-axis-aligned bars\n// (ECharts' built-in bar series always treats the x axis as the base axis\n// when neither axis is categorical, so it cannot draw the horizontal bars\n// the right-hand marginal needs).\nfunction rectRenderItem(p0, p1, params, api) {\n  const shape = echarts.graphic.clipRectByRect(\n    { x: Math.min(p0[0], p1[0]), y: Math.min(p0[1], p1[1]), width: Math.abs(p1[0] - p0[0]), height: Math.abs(p1[1] - p0[1]) },\n    { x: params.coordSys.x, y: params.coordSys.y, width: params.coordSys.width, height: params.coordSys.height }\n  );\n  return shape && { type: \"rect\", shape, style: api.style() };\n}\n\n// --- ECharts option ------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\nchart.setOption({\n  animation: false,\n  backgroundColor: \"transparent\",\n  title: {\n    text: \"scatter-marginal · javascript · echarts · anyplot.ai\",\n    left: \"center\",\n    top: 22,\n    textStyle: { color: t.ink, fontSize: 22, fontWeight: 500 },\n  },\n  grid: [mainGrid, topGrid, rightGrid],\n  xAxis: [\n    {\n      gridIndex: 0,\n      type: \"value\",\n      min: xMin,\n      max: xMax,\n      name: \"Commute Distance (km)\",\n      nameLocation: \"middle\",\n      nameGap: 46,\n      nameTextStyle: { color: t.ink, fontSize: 16 },\n      axisLabel: { color: t.inkSoft, fontSize: 14, showMaxLabel: false },\n      axisLine: { lineStyle: { color: t.inkSoft } },\n      splitLine: { lineStyle: { color: t.grid } },\n    },\n    {\n      gridIndex: 1,\n      type: \"value\",\n      min: xMin,\n      max: xMax,\n      show: false,\n      axisLabel: { show: false },\n      axisLine: { show: false },\n      axisTick: { show: false },\n      splitLine: { show: false },\n    },\n    {\n      gridIndex: 2,\n      type: \"value\",\n      min: 0,\n      max: yCountMax,\n      show: false,\n      axisLabel: { show: false },\n      axisLine: { show: false },\n      axisTick: { show: false },\n      splitLine: { show: false },\n    },\n  ],\n  yAxis: [\n    {\n      gridIndex: 0,\n      type: \"value\",\n      min: yMin,\n      max: yMax,\n      name: \"Commute Time (min)\",\n      nameLocation: \"middle\",\n      nameGap: 60,\n      nameTextStyle: { color: t.ink, fontSize: 16 },\n      axisLabel: { color: t.inkSoft, fontSize: 14, showMaxLabel: false },\n      axisLine: { lineStyle: { color: t.inkSoft } },\n      splitLine: { lineStyle: { color: t.grid } },\n    },\n    {\n      gridIndex: 1,\n      type: \"value\",\n      min: 0,\n      max: xCountMax,\n      show: false,\n      axisLabel: { show: false },\n      axisLine: { show: false },\n      axisTick: { show: false },\n      splitLine: { show: false },\n    },\n    {\n      gridIndex: 2,\n      type: \"value\",\n      min: yMin,\n      max: yMax,\n      show: false,\n      axisLabel: { show: false },\n      axisLine: { show: false },\n      axisTick: { show: false },\n      splitLine: { show: false },\n    },\n  ],\n  series: [\n    {\n      type: \"scatter\",\n      xAxisIndex: 0,\n      yAxisIndex: 0,\n      data: points,\n      symbolSize: 10,\n      itemStyle: { color: t.palette[0], opacity: 0.55, borderColor: t.pageBg, borderWidth: 1 },\n    },\n    {\n      // Top marginal: vertical bars, one per x bin, growing upward from the\n      // grid line adjacent to the main scatter plot.\n      type: \"custom\",\n      xAxisIndex: 1,\n      yAxisIndex: 1,\n      data: xBins.map((b) => [b.start, b.end, b.count]),\n      renderItem: (params, api) => {\n        const start = api.value(0);\n        const end = api.value(1);\n        const count = api.value(2);\n        const p0 = api.coord([start, 0]);\n        const p1 = api.coord([end, count]);\n        return rectRenderItem(p0, p1, params, api);\n      },\n      itemStyle: { color: MARGINAL_COLOR, opacity: 0.55 },\n      silent: true,\n    },\n    {\n      // Right marginal: horizontal bars, one per y bin, growing rightward\n      // from the grid line adjacent to the main scatter plot.\n      type: \"custom\",\n      xAxisIndex: 2,\n      yAxisIndex: 2,\n      data: yBins.map((b) => [b.start, b.end, b.count]),\n      renderItem: (params, api) => {\n        const start = api.value(0);\n        const end = api.value(1);\n        const count = api.value(2);\n        const p0 = api.coord([0, start]);\n        const p1 = api.coord([count, end]);\n        return rectRenderItem(p0, p1, params, api);\n      },\n      itemStyle: { color: MARGINAL_COLOR, opacity: 0.55 },\n      silent: true,\n    },\n  ],\n});\n"}