{"spec_id":"contour-filled","library":"muix","language":"javascript","code":"// anyplot.ai\n// contour-filled: Filled Contour Plot\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 84/100 | Updated: 2026-09-05\n//# anyplot-orientation: landscape\n// anyplot.ai\n// contour-filled: Filled 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-09-04\n\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { ChartsXAxis } from \"@mui/x-charts/ChartsXAxis\";\nimport { ChartsYAxis } from \"@mui/x-charts/ChartsYAxis\";\nimport { ChartsText } from \"@mui/x-charts/ChartsText\";\nimport { ContinuousColorLegend } from \"@mui/x-charts/ChartsLegend\";\nimport { useXScale, useYScale } from \"@mui/x-charts/hooks\";\n\nconst t = window.ANYPLOT_TOKENS;\nconst SIZE = window.ANYPLOT_SIZE;\n\n// --- Grid setup (in-memory, deterministic) ----------------------------------\nconst GRID = 36;\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// --- Scalar field: sea-surface temperature anomaly (warm patch + cool patch) ---\n// Two Gaussian bumps only — kept free of high-frequency terms so the grid\n// resolution below fully resolves every extremum (no sub-cell artifacts).\nfunction anomaly(x, y) {\n  const warmPatch = 2.0 * Math.exp(-((x - 1.3) * (x - 1.3) + (y - 0.9) * (y - 0.9)) * 0.55);\n  const coolPatch = -1.6 * Math.exp(-((x + 1.4) * (x + 1.4) + (y + 1.0) * (y + 1.0)) * 0.65);\n  return warmPatch + coolPatch;\n}\n\n// zGrid[j][i] = z at (xs[i], ys[j])\nconst zGrid = ys.map((y) => xs.map((x) => anomaly(x, y)));\nconst allZ = zGrid.flat();\nconst zMin = Math.min(...allZ);\nconst zMax = Math.max(...allZ);\nconst vAbs = Math.max(Math.abs(zMin), Math.abs(zMax));\n\n// --- Imprint diverging colormap: t.div = [red, midpoint, blue] -------------\nfunction hexToRgb(hex) {\n  const int = parseInt(hex.slice(1), 16);\n  return [(int >> 16) & 255, (int >> 8) & 255, int & 255];\n}\n\nfunction lerpChannel(a, b, ratio) {\n  return Math.round(a + (b - a) * ratio);\n}\n\nfunction imprintDivInterpolator(stops) {\n  const [low, mid, high] = stops.map(hexToRgb);\n  return (position) => {\n    const [start, end, localRatio] =\n      position < 0.5 ? [low, mid, position / 0.5] : [mid, high, (position - 0.5) / 0.5];\n    const [r, g, b] = [0, 1, 2].map((channel) =>\n      lerpChannel(start[channel], end[channel], localRatio),\n    );\n    return `rgb(${r}, ${g}, ${b})`;\n  };\n}\n\n// Reversed so position 0 (coolest/lowest z) lands on the blue end and\n// position 1 (warmest/highest z) lands on the red end -- matching the\n// universal warm=red / cool=blue temperature-anomaly convention.\nconst divColor = imprintDivInterpolator([...t.div].reverse());\n\n// --- Band levels: symmetric around zero so the midpoint band sits at anomaly=0 ---\nconst NUM_BANDS = 10;\nconst levels = Array.from({ length: NUM_BANDS }, (_, k) => -vAbs + (k * 2 * vAbs) / NUM_BANDS);\nconst bandColors = Array.from({ length: NUM_BANDS }, (_, k) => divColor(k / (NUM_BANDS - 1)));\n\n// --- Marching-triangles filled-contour geometry -----------------------------\n// Each grid cell is split into 4 triangles around its centroid so every\n// super-level-set boundary resolves without the marching-squares saddle\n// ambiguity. Super-level sets are always nested (threshold_hi >= threshold_lo\n// implies region_hi ⊆ region_lo), so painting bands low-to-high with a\n// standard painter's algorithm produces correct filled contour bands\n// regardless of how many disjoint blobs the field has.\nfunction buildTriangles() {\n  const tris = [];\n  for (let j = 0; j < GRID - 1; j += 1) {\n    for (let i = 0; i < GRID - 1; i += 1) {\n      const sw = { x: xs[i], y: ys[j], z: zGrid[j][i] };\n      const se = { x: xs[i + 1], y: ys[j], z: zGrid[j][i + 1] };\n      const ne = { x: xs[i + 1], y: ys[j + 1], z: zGrid[j + 1][i + 1] };\n      const nw = { x: xs[i], y: ys[j + 1], z: zGrid[j + 1][i] };\n      const center = {\n        x: (sw.x + se.x) / 2,\n        y: (sw.y + nw.y) / 2,\n        z: (sw.z + se.z + ne.z + nw.z) / 4,\n      };\n      tris.push([sw, se, center], [se, ne, center], [ne, nw, center], [nw, sw, center]);\n    }\n  }\n  return tris;\n}\n\nconst triangles = buildTriangles();\n\n// Filled sub-polygon(s) of one triangle lying at/above `threshold`, plus the\n// interpolated edge (if any) that traces the exact level curve through it.\nfunction triangleFill(a, b, c, threshold) {\n  const inA = a.z >= threshold, inB = b.z >= threshold, inC = c.z >= threshold;\n  const nIn = (inA ? 1 : 0) + (inB ? 1 : 0) + (inC ? 1 : 0);\n  const cross = (p, q) => {\n    const ratio = (threshold - p.z) / (q.z - p.z);\n    return { x: p.x + ratio * (q.x - p.x), y: p.y + ratio * (q.y - p.y) };\n  };\n\n  if (nIn === 0) return { polys: [], cut: null };\n  if (nIn === 3) return { polys: [[a, b, c]], cut: null };\n\n  if (nIn === 1) {\n    if (inA) { const ab = cross(a, b), ca = cross(c, a); return { polys: [[a, ab, ca]], cut: [ab, ca] }; }\n    if (inB) { const ab = cross(a, b), bc = cross(b, c); return { polys: [[b, bc, ab]], cut: [bc, ab] }; }\n    const ca = cross(c, a), bc = cross(b, c);\n    return { polys: [[c, ca, bc]], cut: [ca, bc] };\n  }\n\n  // nIn === 2 (exactly one vertex out)\n  if (!inC) { const bc = cross(b, c), ca = cross(c, a); return { polys: [[a, b, bc, ca]], cut: [ca, bc] }; }\n  if (!inA) { const ca = cross(c, a), ab = cross(a, b); return { polys: [[b, c, ca, ab]], cut: [ab, ca] }; }\n  const ab = cross(a, b), bc = cross(b, c);\n  return { polys: [[c, a, ab, bc]], cut: [bc, ab] };\n}\n\n// Bands k=1..NUM_BANDS-1 are computed from the triangulation; band k=0 is the\n// full domain rect (everything is above zMin), painted first as the base layer.\nconst bandGeometry = [];\nconst isolineGeometry = [];\nfor (let k = 1; k < NUM_BANDS; k += 1) {\n  const threshold = levels[k];\n  const polys = [];\n  const segments = [];\n  for (const tri of triangles) {\n    const { polys: p, cut } = triangleFill(tri[0], tri[1], tri[2], threshold);\n    if (p.length) polys.push(...p);\n    if (cut) segments.push(cut);\n  }\n  bandGeometry.push(polys);\n  isolineGeometry.push(segments);\n}\n\n// --- Custom SVG layer: filled bands + isolines, mapped through the chart's own scales ---\nfunction FilledContourLayer() {\n  const xScale = useXScale();\n  const yScale = useYScale();\n  const toSVG = (x, y) => [xScale(x), yScale(y)];\n\n  const polysToPath = (polys) =>\n    polys\n      .map((poly) => {\n        const pts = poly.map((p) => toSVG(p.x, p.y));\n        const head = `M ${pts[0][0].toFixed(1)},${pts[0][1].toFixed(1)}`;\n        const tail = pts.slice(1).map(([px, py]) => `L ${px.toFixed(1)},${py.toFixed(1)}`).join(\" \");\n        return `${head} ${tail} Z`;\n      })\n      .join(\" \");\n\n  const segmentsToPath = (segments) =>\n    segments\n      .map(([p0, p1]) => {\n        const [x0, y0] = toSVG(p0.x, p0.y);\n        const [x1, y1] = toSVG(p1.x, p1.y);\n        return `M ${x0.toFixed(1)},${y0.toFixed(1)} L ${x1.toFixed(1)},${y1.toFixed(1)}`;\n      })\n      .join(\" \");\n\n  const [rx0, ry0] = toSVG(X_MIN, Y_MIN);\n  const [rx1, ry1] = toSVG(X_MAX, Y_MAX);\n  const baseRect = `M ${rx0.toFixed(1)},${ry0.toFixed(1)} L ${rx1.toFixed(1)},${ry0.toFixed(1)} L ${rx1.toFixed(1)},${ry1.toFixed(1)} L ${rx0.toFixed(1)},${ry1.toFixed(1)} Z`;\n\n  return (\n    <g>\n      {/* Base band: fills the whole domain, subsequent bands paint over it (painter's algorithm) */}\n      <path d={baseRect} fill={bandColors[0]} stroke={bandColors[0]} strokeWidth={0.75} />\n      {bandGeometry.map((polys, idx) => (\n        <path\n          key={`band-${idx}`}\n          d={polysToPath(polys)}\n          fill={bandColors[idx + 1]}\n          stroke={bandColors[idx + 1]}\n          strokeWidth={0.75}\n        />\n      ))}\n      {isolineGeometry.map((segments, idx) => (\n        <path\n          key={`iso-${idx}`}\n          d={segmentsToPath(segments)}\n          stroke={t.ink}\n          strokeOpacity={0.28}\n          strokeWidth={1}\n          fill=\"none\"\n        />\n      ))}\n    </g>\n  );\n}\n\n// --- Chart (default-exported component — the harness mounts it) -------------\nconst TITLE = \"Sea-Surface Temperature Anomaly · contour-filled · javascript · muix · anyplot.ai\";\nconst TITLE_FONT_SIZE = Math.max(15, Math.round(22 * Math.min(1, 67 / TITLE.length)));\nconst MARGIN = { top: 130, right: 200, bottom: 90, left: 115 };\n\n// ContinuousColorLegend anchors flush against the literal SVG width, ignoring\n// MARGIN.right entirely (its `position: \"right\"` offset is `svgWidth -\n// legendWidth`, ie. the very last canvas column) -- so the whole right-side\n// cluster (legend + its rotated axis title) is wrapped in this leftward shift\n// to keep tick-label glyphs off the true edge.\nconst RIGHT_EDGE_INSET = 48;\n\nexport default function Chart() {\n  return (\n    <ChartContainer\n      width={SIZE.width}\n      height={SIZE.height}\n      series={[]}\n      margin={MARGIN}\n      skipAnimation\n      xAxis={[\n        {\n          scaleType: \"linear\",\n          min: X_MIN,\n          max: X_MAX,\n          label: \"Zonal offset (°)\",\n          labelStyle: { fontSize: 15, fill: t.ink },\n          tickLabelStyle: { fontSize: 14, fill: t.inkSoft },\n        },\n      ]}\n      yAxis={[\n        {\n          scaleType: \"linear\",\n          min: Y_MIN,\n          max: Y_MAX,\n          label: \"Meridional offset (°)\",\n          labelStyle: { fontSize: 15, fill: t.ink },\n          tickLabelStyle: { fontSize: 14, fill: t.inkSoft },\n          // Push the rotated axis label further from the axis line than the\n          // library's default offset, which collides with wide tick digits\n          // like \"-3.0\" -- see ChartsYAxis's labelRefPoint formula.\n          slotProps: { axisLabel: { x: -62 } },\n        },\n      ]}\n      zAxis={[\n        {\n          colorMap: {\n            type: \"continuous\",\n            min: -vAbs,\n            max: vAbs,\n            color: divColor,\n          },\n        },\n      ]}\n    >\n      <FilledContourLayer />\n      <ChartsXAxis />\n      <ChartsYAxis />\n      <g transform={`translate(${-RIGHT_EDGE_INSET}, 0)`}>\n        <ContinuousColorLegend\n          position={{ horizontal: \"right\", vertical: \"middle\" }}\n          direction=\"column\"\n          length=\"55%\"\n          thickness={18}\n          labelStyle={{ fontSize: 13, fill: t.inkSoft }}\n          minLabel={({ value }) => value.toFixed(1)}\n          maxLabel={({ value }) => value.toFixed(1)}\n        />\n        <ChartsText\n          text=\"Temperature anomaly (°C)\"\n          x={SIZE.width - 26}\n          y={SIZE.height / 2}\n          style={{ fontSize: 12, fill: t.inkSoft, textAnchor: \"middle\", angle: -90 }}\n        />\n      </g>\n      <ChartsText\n        text={TITLE}\n        x={SIZE.width / 2}\n        y={50}\n        style={{ fontSize: TITLE_FONT_SIZE, fontWeight: 500, fill: t.ink, textAnchor: \"middle\" }}\n      />\n    </ChartContainer>\n  );\n}\n"}