{"spec_id":"scatter-matrix","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// scatter-matrix: Scatter Plot Matrix\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-09-09\n//# anyplot-orientation: square\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic fixed-seed PRNG) -------------------------\n// Rose cultivar bloom measurements — 3 cultivars, 4 continuous traits, 45\n// specimens each. The cultivars cluster distinctly across the traits, which is\n// what a scatter plot matrix is for: spot the pairwise correlations and the\n// group separation at a glance.\nfunction mulberry32(seed) {\n  return function () {\n    seed = (seed + 0x6d2b79f5) | 0;\n    let x = Math.imul(seed ^ (seed >>> 15), 1 | seed);\n    x = (x + Math.imul(x ^ (x >>> 7), 61 | x)) ^ x;\n    return ((x ^ (x >>> 14)) >>> 0) / 4294967296;\n  };\n}\nconst rand = mulberry32(42);\nfunction randNormal(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 + z * std;\n}\n\nconst variables = [\n  { key: \"bloom\", label: \"Bloom Diameter (cm)\" },\n  { key: \"petals\", label: \"Petal Count\" },\n  { key: \"stem\", label: \"Stem Length (cm)\" },\n  { key: \"fragrance\", label: \"Fragrance Score\" },\n];\n\nconst cultivars = [\n  {\n    name: \"Hybrid Tea\",\n    n: 45,\n    mean: { bloom: 11.0, petals: 35, stem: 55, fragrance: 6.5 },\n    std: { bloom: 1.1, petals: 4, stem: 6, fragrance: 1.0 },\n  },\n  {\n    name: \"Floribunda\",\n    n: 45,\n    mean: { bloom: 7.0, petals: 25, stem: 40, fragrance: 4.5 },\n    std: { bloom: 0.8, petals: 3, stem: 5, fragrance: 1.1 },\n  },\n  {\n    name: \"Climbing\",\n    n: 45,\n    mean: { bloom: 8.5, petals: 20, stem: 90, fragrance: 7.5 },\n    std: { bloom: 0.9, petals: 3, stem: 10, fragrance: 0.9 },\n  },\n];\n\ncultivars.forEach((c) => {\n  c.data = { bloom: [], petals: [], stem: [], fragrance: [] };\n  for (let k = 0; k < c.n; k++) {\n    variables.forEach((v) => {\n      c.data[v.key].push(randNormal(c.mean[v.key], c.std[v.key]));\n    });\n  }\n});\n\nconst combined = {};\nvariables.forEach((v) => {\n  combined[v.key] = cultivars.flatMap((c) => c.data[v.key]);\n  const rawMin = Math.min(...combined[v.key]);\n  const rawMax = Math.max(...combined[v.key]);\n  const pad = (rawMax - rawMin) * 0.08;\n  v.rawMin = rawMin;\n  v.rawMax = rawMax;\n  v.min = rawMin - pad;\n  v.max = rawMax + pad;\n});\n\n// Identify the pairwise relationship with the clearest cultivar separation so\n// the matrix can give the viewer a \"headline\" cell to anchor on, rather than\n// relying purely on the viewer noticing cluster separation unaided. The score\n// is the worst-case (minimum) standardized distance between any two cultivar\n// centroids for that variable pair — the pair that maximizes this is the one\n// where every cultivar is most confidently distinguishable from every other.\nfunction stdDev(values) {\n  const mean = values.reduce((a, b) => a + b, 0) / values.length;\n  return Math.sqrt(values.reduce((a, b) => a + (b - mean) ** 2, 0) / values.length);\n}\nfunction pairSeparation(iKey, jKey) {\n  const si = stdDev(combined[iKey]);\n  const sj = stdDev(combined[jKey]);\n  let minDist = Infinity;\n  for (let a = 0; a < cultivars.length; a++) {\n    for (let b = a + 1; b < cultivars.length; b++) {\n      const dx = (cultivars[a].mean[iKey] - cultivars[b].mean[iKey]) / si;\n      const dy = (cultivars[a].mean[jKey] - cultivars[b].mean[jKey]) / sj;\n      minDist = Math.min(minDist, Math.hypot(dx, dy));\n    }\n  }\n  return minDist;\n}\nlet bestPair = [0, 1];\nlet bestScore = -Infinity;\nfor (let i = 0; i < variables.length; i++) {\n  for (let j = i + 1; j < variables.length; j++) {\n    const score = pairSeparation(variables[i].key, variables[j].key);\n    if (score > bestScore) {\n      bestScore = score;\n      bestPair = [i, j];\n    }\n  }\n}\n\nfunction histogram(values, min, max, bins) {\n  const width = (max - min) / bins;\n  const counts = new Array(bins).fill(0);\n  values.forEach((value) => {\n    let idx = Math.floor((value - min) / width);\n    if (idx >= bins) idx = bins - 1;\n    if (idx < 0) idx = 0;\n    counts[idx] += 1;\n  });\n  return counts.map((count, idx) => ({ x: min + width * (idx + 0.5), y: count }));\n}\n\n// --- Grid geometry -----------------------------------------------------------\n// Highcharts core has no SPLOM series type, so the matrix is built from n*n\n// independent xAxis/yAxis pairs positioned as percentages of the plot area —\n// one axis pair per cell, aligned min/max down each column and across each\n// row. Only the outer edge axes carry tick labels and titles.\nconst n = variables.length;\nconst gapPct = 3;\nconst cellPct = (100 - (n - 1) * gapPct) / n;\nconst cellStart = (idx) => idx * (cellPct + gapPct);\n\nconst xAxes = [];\nconst yAxes = [];\nconst series = [];\n\nfor (let row = 0; row < n; row++) {\n  for (let col = 0; col < n; col++) {\n    const colVar = variables[col];\n    const rowVar = variables[row];\n    const isDiagonal = row === col;\n    const isBottomRow = row === n - 1;\n    const isLeftCol = col === 0;\n    const isHeadlinePair =\n      !isDiagonal && ((row === bestPair[0] && col === bestPair[1]) || (row === bestPair[1] && col === bestPair[0]));\n    const left = `${cellStart(col)}%`;\n    const top = `${cellStart(row)}%`;\n    const width = `${cellPct}%`;\n    const height = `${cellPct}%`;\n\n    // `lineWidth` on a multi-axis grid like this one draws the axis line at\n    // its \"crossing\" value on the paired axis (often 0), not at this cell's\n    // own box edge — a `plotLines` entry at the axis's own min is a reliable\n    // substitute since it resolves purely through this axis's own toPixels().\n    // `tickAmount` (even with startOnTick/endOnTick disabled) makes Highcharts\n    // silently round the rendered extremes to \"nice\" numbers away from the\n    // explicit min/max, which then desyncs that plotLine from the true edge —\n    // explicit `tickPositions` sidesteps the rounding entirely.\n    const bins = isDiagonal ? histogram(combined[colVar.key], colVar.rawMin, colVar.rawMax, 11) : null;\n    const yMin = isDiagonal ? 0 : rowVar.min;\n    const yMax = isDiagonal ? Math.max(...bins.map((b) => b.y)) * 1.15 : rowVar.max;\n    const tickFormatter = function () {\n      return Highcharts.numberFormat(this.value, 1);\n    };\n\n    xAxes.push({\n      left,\n      top,\n      width,\n      height,\n      // Every axis defaults to accumulating offset with sibling axes on the\n      // same side (as if stacking multiple y-axes outward) — with 16 of them\n      // that pushes later cells' labels far past the fixed chart margin.\n      // offset: 0 pins each axis's labels flush to its own box instead.\n      offset: 0,\n      min: colVar.min,\n      max: colVar.max,\n      startOnTick: false,\n      endOnTick: false,\n      tickPositions: [colVar.min, (colVar.min + colVar.max) / 2, colVar.max],\n      gridLineWidth: 1,\n      gridLineColor: t.grid,\n      lineWidth: 0,\n      tickColor: t.inkSoft,\n      plotLines: [{ value: colVar.min, color: t.inkSoft, width: 1, zIndex: 5 }],\n      // A faint full-height tint on the headline pair's own axis box (each\n      // mini-axis renders plotBands within its own left/top/width/height —\n      // the same isolation that keeps plotLines confined per cell) spotlights\n      // the most cleanly separated relationship without touching the others.\n      plotBands: isHeadlinePair\n        ? [{ from: colVar.min, to: colVar.max, color: Highcharts.color(t.palette[0]).setOpacity(0.08).get(), zIndex: 0 }]\n        : undefined,\n      labels: {\n        enabled: isBottomRow,\n        formatter: tickFormatter,\n        style: { color: t.inkSoft, fontSize: \"13px\" },\n      },\n      title: isBottomRow\n        ? { text: colVar.label, style: { color: t.inkSoft, fontSize: \"13px\" } }\n        : { text: null },\n    });\n\n    yAxes.push({\n      left,\n      top,\n      width,\n      height,\n      offset: 0,\n      min: yMin,\n      max: yMax,\n      startOnTick: false,\n      endOnTick: false,\n      tickPositions: isDiagonal ? [yMin, yMax] : [yMin, (yMin + yMax) / 2, yMax],\n      gridLineWidth: 1,\n      gridLineColor: t.grid,\n      lineWidth: 0,\n      tickColor: t.inkSoft,\n      plotLines: [{ value: yMin, color: t.inkSoft, width: 1, zIndex: 5 }],\n      plotBands: isHeadlinePair\n        ? [{ from: yMin, to: yMax, color: Highcharts.color(t.palette[0]).setOpacity(0.08).get(), zIndex: 0 }]\n        : undefined,\n      labels: {\n        enabled: !isDiagonal && isLeftCol,\n        formatter: tickFormatter,\n        style: { color: t.inkSoft, fontSize: \"13px\" },\n      },\n      title:\n        !isDiagonal && isLeftCol\n          ? { text: rowVar.label, style: { color: t.inkSoft, fontSize: \"13px\" } }\n          : { text: null },\n    });\n\n    const idx = row * n + col;\n    if (isDiagonal) {\n      series.push({\n        type: \"column\",\n        name: `${colVar.label} distribution`,\n        data: bins,\n        xAxis: idx,\n        yAxis: idx,\n        color: Highcharts.color(t.ink).setOpacity(0.35).get(),\n        borderWidth: 0,\n        pointPadding: 0.05,\n        groupPadding: 0,\n        showInLegend: false,\n        enableMouseTracking: false,\n      });\n    } else {\n      cultivars.forEach((c, cIdx) => {\n        const data = c.data[colVar.key].map((xValue, k) => [xValue, c.data[rowVar.key][k]]);\n        series.push({\n          type: \"scatter\",\n          name: c.name,\n          data,\n          xAxis: idx,\n          yAxis: idx,\n          color: t.palette[cIdx],\n          marker: isHeadlinePair\n            ? { radius: 4.6, symbol: \"circle\", fillOpacity: 0.8, lineWidth: 0.75, lineColor: t.ink }\n            : { radius: 3.6, symbol: \"circle\", fillOpacity: 0.7, lineWidth: 0 },\n          showInLegend: row === 1 && col === 0,\n        });\n      });\n    }\n  }\n}\n\n// --- Chart ---------------------------------------------------------------\nHighcharts.chart(\"container\", {\n  chart: {\n    type: \"scatter\",\n    backgroundColor: \"transparent\",\n    animation: false,\n    style: { fontFamily: \"inherit\" },\n    marginLeft: 150,\n    marginRight: 40,\n    marginTop: 165,\n    marginBottom: 130,\n  },\n  credits: { enabled: false },\n  accessibility: { enabled: false },\n  title: {\n    text: \"scatter-matrix · javascript · highcharts · anyplot.ai\",\n    align: \"left\",\n    x: 10,\n    style: { color: t.ink, fontSize: \"26px\", fontWeight: \"600\" },\n  },\n  subtitle: {\n    text: \"Rose cultivar bloom measurements · n=135, colored by cultivar\",\n    align: \"left\",\n    x: 10,\n    style: { color: t.inkSoft, fontSize: \"14px\" },\n  },\n  legend: {\n    align: \"right\",\n    verticalAlign: \"top\",\n    layout: \"vertical\",\n    x: -10,\n    y: 100,\n    itemStyle: { color: t.inkSoft, fontSize: \"13px\" },\n    itemHoverStyle: { color: t.ink },\n    symbolRadius: 6,\n  },\n  xAxis: xAxes,\n  yAxis: yAxes,\n  plotOptions: {\n    series: { animation: false },\n  },\n  tooltip: {\n    pointFormat: \"{series.name}<br/>x: {point.x:.1f}, y: {point.y:.1f}\",\n  },\n  series,\n});\n"}