{"spec_id":"surface-basic","library":"muix","language":"javascript","code":"// anyplot.ai\n// surface-basic: Basic 3D Surface Plot\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 84/100 | Created: 2026-09-10\n//# anyplot-orientation: landscape\n// anyplot.ai\n// surface-basic: Basic 3D Surface 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-10\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\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// --- Data: a smooth response surface z = sin(x) * cos(y) over a 30x30 grid --\n// @mui/x-charts has no native 3D surface component (community or Pro), so the\n// surface is hand-projected: an isometric transform turns each (x, y, z) grid\n// vertex into a 2D (px, py) point, then adjacent vertices are joined into\n// filled quads and painted back-to-front, the same technique used for the\n// hand-rolled axis frame in scatter-3d — here driving the fill itself.\nconst GRID_N = 30;\nconst X_MIN = -3;\nconst X_MAX = 3;\nconst Y_MIN = -3;\nconst Y_MAX = 3;\nconst HEIGHT_SCALE = 3.2; // visually exaggerates the [-1, 1] function range\n\nconst grid = [];\nfor (let row = 0; row < GRID_N; row += 1) {\n  const y = Y_MIN + (row / (GRID_N - 1)) * (Y_MAX - Y_MIN);\n  const line = [];\n  for (let col = 0; col < GRID_N; col += 1) {\n    const x = X_MIN + (col / (GRID_N - 1)) * (X_MAX - X_MIN);\n    const z = Math.sin(x) * Math.cos(y);\n    line.push({ x, y, z });\n  }\n  grid.push(line);\n}\n\n// --- Isometric projection: (x, y, z) -> 2D (px, py) data-space coordinates -\n// x/y form the ground plane (down-right / down-left), z (the function value)\n// is height, scaled up so the relief reads clearly at isometric angle.\nconst ISO_ANGLE = Math.PI / 6; // 30 degrees\nfunction project(x, y, z) {\n  return {\n    px: (x - y) * Math.cos(ISO_ANGLE),\n    py: z * HEIGHT_SCALE - (x + y) * Math.sin(ISO_ANGLE),\n  };\n}\n\ngrid.forEach((line) =>\n  line.forEach((node) => {\n    const projected = project(node.x, node.y, node.z);\n    node.px = projected.px;\n    node.py = projected.py;\n  })\n);\n\n// --- Quads: one filled polygon per grid cell, colored by average height ----\nconst heightValues = grid.flat().map((node) => node.z);\nconst heightMin = Math.min(...heightValues);\nconst heightMax = Math.max(...heightValues);\n\n// Diverging Imprint scale (matte red -> theme midpoint -> blue): the surface\n// has a genuine zero baseline (a flat plane), so low/high are signed\n// deviations around it — the textbook case for imprint_div.\nconst midpointHex = t.div[1];\nconst lowHex = t.div[0];\nconst highHex = t.div[2];\nconst hexToRgb = (hex) => {\n  const n = parseInt(hex.slice(1), 16);\n  return [(n >> 16) & 255, (n >> 8) & 255, n & 255];\n};\nconst lerpHex = (a, b, ratio) => {\n  const [ar, ag, ab] = hexToRgb(a);\n  const [br, bg, bb] = hexToRgb(b);\n  const r = Math.round(ar + (br - ar) * ratio);\n  const g = Math.round(ag + (bg - ag) * ratio);\n  const bch = Math.round(ab + (bb - ab) * ratio);\n  return `rgb(${r}, ${g}, ${bch})`;\n};\nconst colorForUnit = (unitT) =>\n  unitT <= 0.5\n    ? lerpHex(lowHex, midpointHex, unitT / 0.5)\n    : lerpHex(midpointHex, highHex, (unitT - 0.5) / 0.5);\nconst colorForHeight = (value) =>\n  colorForUnit((value - heightMin) / (heightMax - heightMin));\n\nconst quads = [];\nfor (let row = 0; row < GRID_N - 1; row += 1) {\n  for (let col = 0; col < GRID_N - 1; col += 1) {\n    const p00 = grid[row][col];\n    const p10 = grid[row][col + 1];\n    const p11 = grid[row + 1][col + 1];\n    const p01 = grid[row + 1][col];\n    const avgHeight = (p00.z + p10.z + p11.z + p01.z) / 4;\n    quads.push({\n      points: [p00, p10, p11, p01],\n      depthKey: row + col, // painter's algorithm: far (low row+col) first\n      fill: colorForHeight(avgHeight),\n    });\n  }\n}\nquads.sort((a, b) => a.depthKey - b.depthKey);\n\n// --- Domain bounds for the linear x/y scales, padded for axis labels -------\n// Padding is a fraction of the projected data range (not a fixed data-unit\n// constant) — the isometric domain here is ~O(20) units, an order of\n// magnitude smaller than scatter-3d's ~O(100)-unit spatial domain, so a fixed\n// pad borrowed from that scale would swallow most of the plot area.\nconst allProjected = grid.flat();\nconst rawPxMin = Math.min(...allProjected.map((n) => n.px));\nconst rawPxMax = Math.max(...allProjected.map((n) => n.px));\nconst rawPyMin = Math.min(...allProjected.map((n) => n.py));\nconst rawPyMax = Math.max(...allProjected.map((n) => n.py));\nconst padX = (rawPxMax - rawPxMin) * 0.08;\nconst padY = (rawPyMax - rawPyMin) * 0.08;\nconst pxMin = rawPxMin - padX;\nconst pxMax = rawPxMax + padX;\nconst pyMin = rawPyMin - padY;\nconst pyMax = rawPyMax + padY * 2.8; // extra headroom for the z-axis label\n\n// --- Reference frame: ground corners + a height axis, drawn in data space\n// via the chart's own scale hooks (same pattern as scatter-3d's Iso3DFrame) -\nconst originPoint = project(X_MIN, Y_MIN, 0);\nconst xEndPoint = project(X_MAX, Y_MIN, 0);\nconst yEndPoint = project(X_MIN, Y_MAX, 0);\nconst heightEndPoint = project(X_MIN, Y_MIN, heightMax);\nconst farCorner = project(X_MAX, Y_MAX, 0);\n\nfunction IsoFrame() {\n  const xScale = useXScale();\n  const yScale = useYScale();\n  const toPixels = (p) => ({ x: xScale(p.px), y: yScale(p.py) });\n\n  const origin = toPixels(originPoint);\n  const xEnd = toPixels(xEndPoint);\n  const yEnd = toPixels(yEndPoint);\n  const heightEnd = toPixels(heightEndPoint);\n  const far = toPixels(farCorner);\n\n  const groundEdge = (from, to) => (\n    <line\n      x1={from.x}\n      y1={from.y}\n      x2={to.x}\n      y2={to.y}\n      stroke={t.grid}\n      strokeWidth={1.5}\n      strokeDasharray=\"6 5\"\n    />\n  );\n  const axisLabel = (point, text, dx, dy) => (\n    <text\n      x={point.x + dx}\n      y={point.y + dy}\n      fill={t.ink}\n      fontSize={16}\n      fontWeight={600}\n      fontFamily=\"system-ui, sans-serif\"\n      textAnchor=\"middle\"\n    >\n      {text}\n    </text>\n  );\n\n  return (\n    <g>\n      {groundEdge(xEnd, far)}\n      {groundEdge(yEnd, far)}\n      <line\n        x1={origin.x}\n        y1={origin.y}\n        x2={heightEnd.x}\n        y2={heightEnd.y}\n        stroke={t.inkSoft}\n        strokeWidth={2}\n      />\n      {axisLabel(xEnd, `x · [${X_MIN}, ${X_MAX}]`, 30, 10)}\n      {axisLabel(yEnd, `y · [${Y_MIN}, ${Y_MAX}]`, -32, 10)}\n      {axisLabel(heightEnd, \"z = sin(x)·cos(y)\", 0, -26)}\n    </g>\n  );\n}\n\nconst chartTitle = \"surface-basic · javascript · muix · anyplot.ai\";\n\n// --- Chart (default-exported component — the harness mounts it) ------------\nexport default function Chart() {\n  return (\n    <ChartContainer\n      width={size.width}\n      height={size.height}\n      series={[]}\n      zAxis={[\n        {\n          id: \"heightColor\",\n          min: heightMin,\n          max: heightMax,\n          colorMap: { type: \"continuous\", min: heightMin, max: heightMax, color: colorForUnit },\n        },\n      ]}\n      xAxis={[{ id: \"iso-x\", scaleType: \"linear\", min: pxMin, max: pxMax }]}\n      yAxis={[{ id: \"iso-y\", scaleType: \"linear\", min: pyMin, max: pyMax }]}\n      margin={{ top: 70, right: 70, bottom: 90, left: 70 }}\n      disableAxisListener\n      skipAnimation\n    >\n      <text\n        x={size.width / 2}\n        y={38}\n        textAnchor=\"middle\"\n        fontSize={26}\n        fontWeight={600}\n        fill={t.ink}\n        fontFamily=\"system-ui, sans-serif\"\n      >\n        {chartTitle}\n      </text>\n      <SurfaceMesh />\n      <IsoFrame />\n      <ContinuousColorLegend\n        axisDirection=\"z\"\n        axisId=\"heightColor\"\n        direction=\"row\"\n        position={{ horizontal: \"middle\", vertical: \"bottom\" }}\n        length=\"26%\"\n        thickness={10}\n        minLabel={() => `${heightMin.toFixed(2)} (valley)`}\n        maxLabel={() => `${heightMax.toFixed(2)} (peak)`}\n        labelStyle={{ fontSize: 15, fill: t.inkSoft }}\n      />\n    </ChartContainer>\n  );\n}\n\n// --- The surface itself: painter's-algorithm-sorted filled quads -----------\nfunction SurfaceMesh() {\n  const xScale = useXScale();\n  const yScale = useYScale();\n  const toPixels = (p) => `${xScale(p.px)},${yScale(p.py)}`;\n\n  return (\n    <g>\n      {quads.map((quad, i) => (\n        <polygon\n          key={`quad-${quad.depthKey}-${i}`}\n          points={quad.points.map(toPixels).join(\" \")}\n          fill={quad.fill}\n          stroke={t.pageBg}\n          strokeWidth={0.6}\n        />\n      ))}\n    </g>\n  );\n}\n"}