{"spec_id":"contour-basic","library":"muix","language":"javascript","code":"// anyplot.ai\n// contour-basic: Basic Contour Plot\n// Library: muix 7.29.1 | JavaScript 22.23.0\n// Quality: 84/100 | Created: 2026-06-25\n//# anyplot-orientation: landscape\n// anyplot.ai\n// contour-basic: Basic Contour Plot\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-06-25\n\nimport { ScatterChart } from \"@mui/x-charts/ScatterChart\";\nimport { useDrawingArea } from \"@mui/x-charts/hooks\";\nimport Box from \"@mui/material/Box\";\nimport Typography from \"@mui/material/Typography\";\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Grid setup ------------------------------------------------------------\nconst GRID = 60;\nconst X_MIN = -3, X_MAX = 3, Y_MIN = -3, Y_MAX = 3;\n\nconst xs = Array.from({ length: GRID }, (_, i) => X_MIN + (i / (GRID - 1)) * (X_MAX - X_MIN));\nconst ys = Array.from({ length: GRID }, (_, j) => Y_MIN + (j / (GRID - 1)) * (Y_MAX - Y_MIN));\n\n// --- Elevation: two Gaussian peaks + ridge ---------------------------------\nfunction elevation(x, y) {\n  const peak1 = Math.exp(-((x - 1) * (x - 1) + (y - 0.5) * (y - 0.5)) * 1.5);\n  const peak2 = 0.75 * Math.exp(-((x + 1) * (x + 1) + (y + 1) * (y + 1)) * 2.0);\n  const ridge = 0.175 * Math.exp(-(x - y) * (x - y) * 0.8);\n  return peak1 + peak2 + ridge;\n}\n\n// zGrid[j][i] = z at (xs[i], ys[j])\nconst zGrid = ys.map(y => xs.map(x => elevation(x, y)));\nconst allZ = zGrid.flat();\nconst zMin = Math.min(...allZ);\nconst zMax = Math.max(...allZ);\n\n// --- Imprint sequential colormap: t.seq[0] → t.seq[1] --------------------\nfunction seqColor(frac) {\n  const h1 = t.seq[0], h2 = t.seq[1];\n  const chan = (s, o) => parseInt(s.slice(o, o + 2), 16);\n  const lerp = (a, b) => Math.round(a + frac * (b - a));\n  const hex = n => n.toString(16).padStart(2, \"0\");\n  return `#${hex(lerp(chan(h1, 1), chan(h2, 1)))}${hex(lerp(chan(h1, 3), chan(h2, 3)))}${hex(lerp(chan(h1, 5), chan(h2, 5)))}`;\n}\n\n// --- Filled contour bands via scatter proxy --------------------------------\nconst NUM_BANDS = 6;\nconst bandW = (zMax - zMin) / NUM_BANDS;\n\nconst allPoints = xs.flatMap((x, i) => ys.map((y, j) => ({ x, y, z: zGrid[j][i] })));\n\nconst scatterSeries = Array.from({ length: NUM_BANDS }, (_, b) => {\n  const lo = zMin + b * bandW;\n  const hi = lo + bandW;\n  const isLast = b === NUM_BANDS - 1;\n  return {\n    data: allPoints\n      .filter(p => isLast ? p.z >= lo && p.z <= hi : p.z >= lo && p.z < hi)\n      .map((p, k) => ({ x: p.x, y: p.y, id: `${b}_${k}` })),\n    label: `${lo.toFixed(2)} – ${hi.toFixed(2)}`,\n    color: seqColor(b / (NUM_BANDS - 1)),\n    markerSize: 17,\n  };\n});\n\n// --- Contour isolines via marching squares ---------------------------------\nfunction marchingSquares(level) {\n  const lerpVal = (a, b, za, zb) => a + (b - a) * (level - za) / (zb - za);\n  const segs = [];\n  for (let j = 0; j < GRID - 1; j++) {\n    for (let i = 0; i < GRID - 1; i++) {\n      const sw = zGrid[j][i],     se = zGrid[j][i + 1];\n      const ne = zGrid[j + 1][i + 1], nw = zGrid[j + 1][i];\n      const swA = sw > level, seA = se > level, neA = ne > level, nwA = nw > level;\n\n      // Interpolated crossing point on each cell edge (null if not crossed)\n      const edgeS = swA !== seA ? [lerpVal(xs[i], xs[i + 1], sw, se), ys[j]]           : null;\n      const edgeE = seA !== neA ? [xs[i + 1], lerpVal(ys[j], ys[j + 1], se, ne)]       : null;\n      const edgeN = nwA !== neA ? [lerpVal(xs[i], xs[i + 1], nw, ne), ys[j + 1]]       : null;\n      const edgeW = swA !== nwA ? [xs[i],     lerpVal(ys[j], ys[j + 1], sw, nw)]       : null;\n\n      const pts = [edgeS, edgeE, edgeN, edgeW].filter(Boolean);\n      if (pts.length === 2) {\n        segs.push([pts[0], pts[1]]);\n      } else if (pts.length === 4) {\n        // Saddle point: use cell center value to resolve ambiguity\n        const mid = (sw + se + ne + nw) / 4;\n        if (mid > level) {\n          segs.push([edgeS, edgeE]); segs.push([edgeN, edgeW]);\n        } else {\n          segs.push([edgeS, edgeW]); segs.push([edgeN, edgeE]);\n        }\n      }\n    }\n  }\n  return segs;\n}\n\n// Isolines at the 5 band boundaries\nconst isolineData = Array.from({ length: NUM_BANDS - 1 }, (_, i) => ({\n  segments: marchingSquares(zMin + (i + 1) * bandW),\n}));\n\n// --- SVG isoline overlay (rendered as a ScatterChart child) ----------------\nfunction IsolineOverlay() {\n  const { left, top, width, height } = useDrawingArea();\n\n  // Map data coords → SVG pixel coords (y axis is inverted in SVG)\n  const toSVG = (dx, dy) => [\n    left + (dx - X_MIN) / (X_MAX - X_MIN) * width,\n    top  + (1 - (dy - Y_MIN) / (Y_MAX - Y_MIN)) * height,\n  ];\n\n  return (\n    <>\n      {isolineData.map(({ segments }, li) => {\n        const d = segments\n          .map(([p0, p1]) => {\n            const [x0, y0] = toSVG(p0[0], p0[1]);\n            const [x1, y1] = toSVG(p1[0], p1[1]);\n            return `M ${x0.toFixed(1)},${y0.toFixed(1)} L ${x1.toFixed(1)},${y1.toFixed(1)}`;\n          })\n          .join(\" \");\n        return d\n          ? <path key={li} d={d} stroke={t.ink} strokeWidth={1.5} strokeOpacity={0.5} fill=\"none\" />\n          : null;\n      })}\n    </>\n  );\n}\n\n// --- Chart -----------------------------------------------------------------\nconst TITLE = \"contour-basic · javascript · muix · anyplot.ai\";\n\nexport default function Chart() {\n  const W = window.ANYPLOT_SIZE.width;\n  const H = window.ANYPLOT_SIZE.height;\n  return (\n    <Box sx={{ width: W, height: H, display: \"flex\", flexDirection: \"column\",\n               bgcolor: t.pageBg, boxSizing: \"border-box\", pt: \"28px\", px: \"32px\", pb: \"8px\" }}>\n      <Typography component=\"div\"\n        sx={{ fontSize: 22, fontWeight: 500, color: t.ink, textAlign: \"center\",\n              lineHeight: 1.3, mb: \"6px\", flexShrink: 0 }}>\n        {TITLE}\n      </Typography>\n      <ScatterChart\n        width={W - 64}\n        height={H - 28 - 36 - 8}\n        skipAnimation\n        tooltip={{ trigger: \"none\" }}\n        series={scatterSeries}\n        xAxis={[{\n          min: X_MIN, max: X_MAX,\n          label: \"X\",\n          tickMinStep: 1,\n          disableLine: true,\n          labelStyle: { fontSize: 15, fill: t.ink },\n          tickLabelStyle: { fontSize: 14, fill: t.inkSoft },\n        }]}\n        yAxis={[{\n          min: Y_MIN, max: Y_MAX,\n          label: \"Y\",\n          tickMinStep: 1,\n          disableLine: true,\n          labelStyle: { fontSize: 15, fill: t.ink },\n          tickLabelStyle: { fontSize: 14, fill: t.inkSoft },\n        }]}\n        margin={{ left: 65, right: 200, top: 12, bottom: 58 }}\n        slotProps={{\n          legend: {\n            direction: \"column\",\n            position: { vertical: \"middle\", horizontal: \"right\" },\n            itemMarkWidth: 14,\n            itemMarkHeight: 14,\n            labelStyle: { fontSize: 13, fill: t.inkSoft },\n          },\n        }}\n      >\n        <IsolineOverlay />\n      </ScatterChart>\n    </Box>\n  );\n}\n"}