{"spec_id":"scatter-matrix","library":"muix","language":"javascript","code":"// anyplot.ai\n// scatter-matrix: Scatter Plot Matrix\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-09-09\n//# anyplot-orientation: square\n// anyplot.ai\n// scatter-matrix: Scatter Plot Matrix\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\";\nimport Box from \"@mui/material/Box\";\nimport Typography from \"@mui/material/Typography\";\n\nconst t = window.ANYPLOT_TOKENS;\nconst size = window.ANYPLOT_SIZE;\n\n// Theme-adaptive chrome the harness's ThemeProvider doesn't expose directly —\n// the \"muted\" semantic anchor from default-style-guide.md (other/rest role,\n// used here for the diagonal's univariate distribution).\nconst INK = t.ink;\nconst INK_SOFT = t.inkSoft;\nconst MUTED = t.theme === \"dark\" ? \"#A8A79F\" : \"#6B6A63\";\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\n// --- Deterministic PRNG (Box-Muller over a tiny LCG) ------------------------\nfunction makeLcg(seed) {\n  let state = seed;\n  return () => {\n    state = (state * 1664525 + 1013904223) % 4294967296;\n    return state / 4294967296;\n  };\n}\nconst rand = makeLcg(20260909);\n\nfunction normal(mean, std) {\n  const u1 = Math.max(rand(), 1e-9);\n  const u2 = rand();\n  const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n  return mean + std * z;\n}\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Iris-shaped multivariate data: 4 continuous flower measurements across 3\n// species, generated from each species' approximate real-world summary\n// statistics. Petal length/width are correlated within species so the matrix\n// has a genuine relationship to reveal, not just cluster separation.\nconst VARS = [\n  { key: \"sepalLength\", label: \"Sepal Length (cm)\" },\n  { key: \"sepalWidth\", label: \"Sepal Width (cm)\" },\n  { key: \"petalLength\", label: \"Petal Length (cm)\" },\n  { key: \"petalWidth\", label: \"Petal Width (cm)\" },\n];\nconst N = VARS.length;\n\nconst SPECIES = [\n  {\n    name: \"Setosa\",\n    color: t.palette[0],\n    sepalLength: { mean: 5.0, std: 0.35 },\n    sepalWidth: { mean: 3.42, std: 0.38 },\n    petalLength: { mean: 1.46, std: 0.17 },\n    petalWidthMean: 0.24,\n    petalWidthSlope: 0.15,\n    petalWidthNoise: 0.08,\n  },\n  {\n    name: \"Versicolor\",\n    color: t.palette[1],\n    sepalLength: { mean: 5.94, std: 0.52 },\n    sepalWidth: { mean: 2.77, std: 0.31 },\n    petalLength: { mean: 4.26, std: 0.47 },\n    petalWidthMean: 1.33,\n    petalWidthSlope: 0.36,\n    petalWidthNoise: 0.14,\n  },\n  {\n    name: \"Virginica\",\n    color: t.palette[2],\n    sepalLength: { mean: 6.59, std: 0.64 },\n    sepalWidth: { mean: 2.97, std: 0.32 },\n    petalLength: { mean: 5.55, std: 0.55 },\n    petalWidthMean: 2.03,\n    petalWidthSlope: 0.28,\n    petalWidthNoise: 0.16,\n  },\n];\nconst POINTS_PER_SPECIES = 50;\n\nconst points = SPECIES.flatMap((species, speciesIndex) =>\n  Array.from({ length: POINTS_PER_SPECIES }, (_, i) => {\n    const sepalLength = normal(\n      species.sepalLength.mean,\n      species.sepalLength.std,\n    );\n    const sepalWidth = normal(species.sepalWidth.mean, species.sepalWidth.std);\n    const petalLength = Math.max(\n      0.1,\n      normal(species.petalLength.mean, species.petalLength.std),\n    );\n    const petalWidth = Math.max(\n      0.05,\n      species.petalWidthMean +\n        species.petalWidthSlope * (petalLength - species.petalLength.mean) +\n        normal(0, species.petalWidthNoise),\n    );\n    return {\n      id: `${speciesIndex}-${i}`,\n      speciesIndex,\n      sepalLength,\n      sepalWidth,\n      petalLength,\n      petalWidth,\n    };\n  }),\n);\n\n// Shared per-variable domain (padded) so every row/column lines up across the\n// matrix, and a matching histogram for the diagonal cells.\nconst domains = {};\nconst histograms = {};\nconst HIST_BINS = 12;\nVARS.forEach(({ key }) => {\n  const values = points.map((p) => p[key]);\n  const min = Math.min(...values);\n  const max = Math.max(...values);\n  const pad = (max - min) * 0.08;\n  const domain = [min - pad, max + pad];\n  domains[key] = domain;\n\n  const binWidth = (domain[1] - domain[0]) / HIST_BINS;\n  const counts = new Array(HIST_BINS).fill(0);\n  values.forEach((v) => {\n    const idx = Math.min(\n      HIST_BINS - 1,\n      Math.max(0, Math.floor((v - domain[0]) / binWidth)),\n    );\n    counts[idx] += 1;\n  });\n  const labels = counts.map((_, i) =>\n    (domain[0] + binWidth * (i + 0.5)).toFixed(1),\n  );\n  histograms[key] = { counts, labels };\n});\n\n// --- Layout ------------------------------------------------------------------\nconst HEADER_H = 64;\nconst SIDE_PAD = 16;\nconst CELL_GAP = 6;\nconst cell = Math.floor(\n  Math.min(\n    size.width - 2 * SIDE_PAD - (N - 1) * CELL_GAP,\n    size.height - HEADER_H - SIDE_PAD - (N - 1) * CELL_GAP,\n  ) / N,\n);\nconst EDGE_LABEL = 15;\nconst EDGE_TICK = 12;\n\n// Petal length vs. petal width is the pair with the clearest species\n// separation (per the AI review's data-storytelling feedback) — a subtle\n// marker emphasis on those cells helps the reader find the strongest story\n// without scanning all 16 panels.\nconst HIGHLIGHT_VARS = new Set([\"petalLength\", \"petalWidth\"]);\n\nfunction axisTextStyle(fontSize, fill) {\n  return { fontSize, fill, fontFamily: \"inherit\" };\n}\n\n// --- Chart (default-exported component — the harness mounts it) -------------\nexport default function Chart() {\n  const cells = [];\n  for (let row = 0; row < N; row += 1) {\n    for (let col = 0; col < N; col += 1) {\n      const isDiagonal = row === col;\n      const rowVar = VARS[row];\n      const colVar = VARS[col];\n      const isHighlight =\n        !isDiagonal &&\n        HIGHLIGHT_VARS.has(rowVar.key) &&\n        HIGHLIGHT_VARS.has(colVar.key);\n      const showXEdge = row === N - 1;\n      const showYEdge = !isDiagonal && col === 0;\n      const margin = {\n        top: 8,\n        right: 8,\n        bottom: showXEdge ? 56 : 8,\n        left: showYEdge ? 68 : 8,\n      };\n\n      let content;\n      if (isDiagonal) {\n        const { counts, labels } = histograms[colVar.key];\n        content = (\n          <BarChart\n            width={cell}\n            height={cell}\n            margin={margin}\n            skipAnimation\n            series={[{ data: counts, color: MUTED }]}\n            xAxis={[\n              {\n                scaleType: \"band\",\n                data: labels,\n                categoryGapRatio: 0.08,\n                barGapRatio: 0,\n                disableLine: !showXEdge,\n                disableTicks: true,\n                tickLabelInterval: showXEdge\n                  ? (_v, i) => i % 3 === 1\n                  : () => false,\n                label: showXEdge ? colVar.label : undefined,\n                labelStyle: axisTextStyle(EDGE_LABEL, INK),\n                tickLabelStyle: axisTextStyle(EDGE_TICK, INK_SOFT),\n              },\n            ]}\n            yAxis={[\n              {\n                disableLine: true,\n                disableTicks: true,\n                tickLabelInterval: () => false,\n              },\n            ]}\n            slotProps={{ legend: { hidden: true } }}\n            tooltip={{ trigger: \"none\" }}\n          />\n        );\n      } else {\n        const series = SPECIES.map((species, speciesIndex) => ({\n          id: species.name,\n          label: species.name,\n          color: hexToRgba(species.color, isHighlight ? 0.85 : 0.72),\n          markerSize: isHighlight ? 5 : 4,\n          data: points\n            .filter((p) => p.speciesIndex === speciesIndex)\n            .map((p) => ({ id: p.id, x: p[colVar.key], y: p[rowVar.key] })),\n        }));\n        content = (\n          <ScatterChart\n            width={cell}\n            height={cell}\n            margin={margin}\n            skipAnimation\n            disableVoronoi\n            series={series}\n            xAxis={[\n              {\n                min: domains[colVar.key][0],\n                max: domains[colVar.key][1],\n                disableLine: !showXEdge,\n                disableTicks: true,\n                tickLabelInterval: showXEdge ? \"auto\" : () => false,\n                label: showXEdge ? colVar.label : undefined,\n                labelStyle: axisTextStyle(EDGE_LABEL, INK),\n                tickLabelStyle: axisTextStyle(EDGE_TICK, INK_SOFT),\n              },\n            ]}\n            yAxis={[\n              {\n                min: domains[rowVar.key][0],\n                max: domains[rowVar.key][1],\n                disableLine: !showYEdge,\n                disableTicks: true,\n                tickLabelInterval: showYEdge ? \"auto\" : () => false,\n                label: showYEdge ? rowVar.label : undefined,\n                labelStyle: axisTextStyle(EDGE_LABEL, INK),\n                tickLabelStyle: axisTextStyle(EDGE_TICK, INK_SOFT),\n              },\n            ]}\n            slotProps={{ legend: { hidden: true } }}\n            tooltip={{ trigger: \"none\" }}\n          />\n        );\n      }\n\n      cells.push(\n        <Box\n          key={`${row}-${col}`}\n          sx={{\n            width: cell,\n            height: cell,\n            border: isHighlight\n              ? `1px solid ${hexToRgba(t.palette[0], 0.45)}`\n              : `1px solid ${t.grid}`,\n            boxSizing: \"border-box\",\n            display: \"flex\",\n            alignItems: \"center\",\n            justifyContent: \"center\",\n          }}\n        >\n          {content}\n        </Box>,\n      );\n    }\n  }\n\n  return (\n    <Box\n      sx={{\n        width: size.width,\n        height: size.height,\n        display: \"flex\",\n        flexDirection: \"column\",\n      }}\n    >\n      <Box\n        sx={{\n          height: HEADER_H,\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"space-between\",\n          px: 3,\n        }}\n      >\n        <Typography sx={{ fontSize: 22, fontWeight: 600, color: INK }}>\n          scatter-matrix · javascript · muix · anyplot.ai\n        </Typography>\n        <Box sx={{ display: \"flex\", gap: 2.5 }}>\n          {SPECIES.map((species) => (\n            <Box\n              key={species.name}\n              sx={{ display: \"flex\", alignItems: \"center\", gap: 0.75 }}\n            >\n              <Box\n                sx={{\n                  width: 11,\n                  height: 11,\n                  borderRadius: \"50%\",\n                  backgroundColor: species.color,\n                  flexShrink: 0,\n                }}\n              />\n              <Typography sx={{ fontSize: 13, color: INK_SOFT }}>\n                {species.name}\n              </Typography>\n            </Box>\n          ))}\n        </Box>\n      </Box>\n      <Box\n        sx={{\n          display: \"grid\",\n          gridTemplateColumns: `repeat(${N}, ${cell}px)`,\n          gridTemplateRows: `repeat(${N}, ${cell}px)`,\n          gap: `${CELL_GAP}px`,\n          margin: \"0 auto\",\n        }}\n      >\n        {cells}\n      </Box>\n    </Box>\n  );\n}\n"}