{"spec_id":"scatter-matrix","library":"echarts","language":"javascript","code":"// anyplot.ai\n// scatter-matrix: Scatter Plot Matrix\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 88/100 | Created: 2026-09-09\n//# anyplot-orientation: square\n\nconst t = window.ANYPLOT_TOKENS;\nconst size = window.ANYPLOT_SIZE;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Iris-like flower measurements: 3 species x 40 points x 4 variables.\nfunction makeLcg(seed) {\n  let state = seed >>> 0;\n  return function () {\n    state = (state * 1664525 + 1013904223) >>> 0;\n    return state / 4294967296;\n  };\n}\nconst rng = makeLcg(42);\nfunction randNormal(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 + z * std;\n}\n\nconst variables = [\"Sepal Length\", \"Sepal Width\", \"Petal Length\", \"Petal Width\"];\nconst axisNames = variables.map((v) => `${v} (cm)`);\nconst species = [\n  { name: \"Setosa\", means: [5.0, 3.4, 1.5, 0.25], stds: [0.35, 0.38, 0.17, 0.1] },\n  { name: \"Versicolor\", means: [5.9, 2.77, 4.26, 1.33], stds: [0.51, 0.31, 0.47, 0.2] },\n  { name: \"Virginica\", means: [6.59, 2.97, 5.55, 2.03], stds: [0.64, 0.32, 0.55, 0.27] },\n];\nconst pointsPerSpecies = 40;\n\nconst points = [];\nspecies.forEach((sp, speciesIndex) => {\n  for (let i = 0; i < pointsPerSpecies; i++) {\n    const values = sp.means.map((mean, v) => Math.max(0.05, randNormal(mean, sp.stds[v])));\n    points.push({ values, speciesIndex });\n  }\n});\n\n// Shared axis ranges per variable, padded, so columns/rows align across cells.\nconst varMin = variables.map((_, v) => Math.min(...points.map((p) => p.values[v])));\nconst varMax = variables.map((_, v) => Math.max(...points.map((p) => p.values[v])));\nconst varRange = variables.map((_, v) => {\n  const pad = (varMax[v] - varMin[v]) * 0.1;\n  return [varMin[v] - pad, varMax[v] + pad];\n});\n\n// --- Layout: N x N grid of value-axis cartesians -----------------------------\nconst N = variables.length;\nconst outerLeft = 8.5;\nconst outerRight = 2.5;\nconst outerTop = 13;\nconst outerBottom = 9;\nconst gap = 2.8;\nconst cellW = (100 - outerLeft - outerRight - gap * (N - 1)) / N;\nconst cellH = (100 - outerTop - outerBottom - gap * (N - 1)) / N;\nconst cellWpx = (size.width * cellW) / 100;\n\nconst grids = [];\nconst xAxes = [];\nconst yAxes = [];\nconst series = [];\n\nconst histBins = 9;\nconst barWidth = Math.max(2, (cellWpx / histBins) * 0.78);\n\n// Regular split-line ticks near a forced min/max label collide with it\n// (e.g. \"2.5\" running into \"2.7\"); hide any regular tick that lands within\n// 8% of the range from the forced edge, keeping the forced edge label itself.\nfunction edgeSafeFormatter([lo, hi], showMin, showMax) {\n  const thresh = (hi - lo) * 0.08;\n  return (val) => {\n    if (showMax && val !== hi && Math.abs(val - hi) < thresh) return \"\";\n    if (showMin && val !== lo && Math.abs(val - lo) < thresh) return \"\";\n    return val.toFixed(1);\n  };\n}\n\nfor (let row = 0; row < N; row++) {\n  for (let col = 0; col < N; col++) {\n    const idx = row * N + col;\n    const left = outerLeft + col * (cellW + gap);\n    const top = outerTop + row * (cellH + gap);\n    grids.push({ left: `${left}%`, top: `${top}%`, width: `${cellW}%`, height: `${cellH}%` });\n\n    const isBottomRow = row === N - 1;\n    const isLeftCol = col === 0;\n\n    xAxes.push({\n      gridIndex: idx,\n      type: \"value\",\n      min: varRange[col][0],\n      max: varRange[col][1],\n      name: isBottomRow ? axisNames[col] : \"\",\n      nameLocation: \"middle\",\n      nameGap: 30,\n      nameTextStyle: { color: t.ink, fontSize: 14 },\n      axisLabel: {\n        show: isBottomRow,\n        color: t.inkSoft,\n        fontSize: 11,\n        formatter: edgeSafeFormatter(varRange[col], col === 0, col === N - 1),\n        showMinLabel: col === 0,\n        showMaxLabel: col === N - 1,\n      },\n      axisTick: { show: false },\n      axisLine: { show: isBottomRow, lineStyle: { color: t.inkSoft } },\n      splitLine: { show: true, lineStyle: { color: t.grid } },\n    });\n\n    if (row === col) {\n      // Diagonal: histogram of this variable across all species.\n      const [lo, hi] = varRange[col];\n      const binWidth = (hi - lo) / histBins;\n      const counts = new Array(histBins).fill(0);\n      points.forEach((p) => {\n        const bin = Math.min(histBins - 1, Math.floor((p.values[col] - lo) / binWidth));\n        counts[bin] += 1;\n      });\n      const maxCount = Math.max(...counts);\n      const barData = counts.map((c, b) => [lo + (b + 0.5) * binWidth, c]);\n\n      yAxes.push({\n        gridIndex: idx,\n        type: \"value\",\n        min: 0,\n        max: maxCount,\n        name: \"\",\n        axisLabel: { show: false },\n        axisTick: { show: false },\n        axisLine: { show: false },\n        splitLine: { show: false },\n      });\n\n      series.push({\n        type: \"bar\",\n        xAxisIndex: idx,\n        yAxisIndex: idx,\n        data: barData,\n        barWidth,\n        itemStyle: { color: t.palette[0], opacity: 0.85 },\n        silent: true,\n      });\n    } else {\n      yAxes.push({\n        gridIndex: idx,\n        type: \"value\",\n        min: varRange[row][0],\n        max: varRange[row][1],\n        name: isLeftCol ? axisNames[row] : \"\",\n        nameLocation: \"middle\",\n        nameGap: 46,\n        nameTextStyle: { color: t.ink, fontSize: 14 },\n        axisLabel: {\n          show: isLeftCol,\n          color: t.inkSoft,\n          fontSize: 11,\n          formatter: edgeSafeFormatter(varRange[row], row === N - 1, row === 0),\n          showMinLabel: row === N - 1,\n          showMaxLabel: row === 0,\n        },\n        axisTick: { show: false },\n        axisLine: { show: isLeftCol, lineStyle: { color: t.inkSoft } },\n        splitLine: { show: true, lineStyle: { color: t.grid } },\n      });\n\n      species.forEach((sp, speciesIndex) => {\n        const cellPoints = points\n          .filter((p) => p.speciesIndex === speciesIndex)\n          .map((p) => [p.values[col], p.values[row]]);\n        series.push({\n          name: sp.name,\n          type: \"scatter\",\n          xAxisIndex: idx,\n          yAxisIndex: idx,\n          data: cellPoints,\n          symbolSize: 6,\n          itemStyle: { color: t.palette[speciesIndex], opacity: 0.6 },\n        });\n      });\n    }\n  }\n}\n\n// --- Init + Option ------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\nchart.setOption({\n  animation: false,\n  backgroundColor: \"transparent\",\n  title: {\n    text: \"scatter-matrix · javascript · echarts · anyplot.ai\",\n    left: \"center\",\n    top: \"1.5%\",\n    textStyle: { color: t.ink, fontSize: 22, fontWeight: 500 },\n  },\n  legend: {\n    data: species.map((sp) => sp.name),\n    top: \"7.5%\",\n    left: \"center\",\n    itemWidth: 14,\n    itemHeight: 14,\n    textStyle: { color: t.inkSoft, fontSize: 14 },\n  },\n  grid: grids,\n  xAxis: xAxes,\n  yAxis: yAxes,\n  series,\n});\n"}