{"spec_id":"surface-basic","library":"echarts","language":"javascript","code":"// anyplot.ai\n// surface-basic: Basic 3D Surface Plot\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 90/100 | Created: 2026-09-10\n\n//# anyplot-orientation: landscape\nconst t = window.ANYPLOT_TOKENS;\nconst size = window.ANYPLOT_SIZE;\n\n// --- Data: chemical process yield as a function of temperature and pressure -\n// A response-surface-methodology scenario: reaction yield peaks near\n// 200 degC / 3 bar and falls off away from that optimum, with a small\n// interaction term so the surface isn't a perfectly symmetric bowl.\nconst N = 34;\nconst T_MIN = 150,\n  T_MAX = 250; // degrees C\nconst P_MIN = 1,\n  P_MAX = 5; // bar\nfunction yieldAt(temp, pressure) {\n  const dt = temp - 200;\n  const dp = pressure - 3;\n  return 92 - 0.018 * dt * dt - 3.2 * dp * dp + 0.004 * dt * dp;\n}\n\nconst temps = Array.from({ length: N }, (_, i) => T_MIN + (i * (T_MAX - T_MIN)) / (N - 1));\nconst pressures = Array.from({ length: N }, (_, j) => P_MIN + (j * (P_MAX - P_MIN)) / (N - 1));\nconst zGrid = temps.map((temp) => pressures.map((pressure) => yieldAt(temp, pressure)));\nconst zFlat = zGrid.flat();\nconst zMin = Math.min(...zFlat);\nconst zMax = Math.max(...zFlat);\n\n// --- Isometric projection: normalize each axis to a shared unit scale, then\n// apply the classic 30-degree axonometric transform (ground-A to the lower\n// right, height straight up, ground-B to the lower left).\nconst GROUND_SPAN = 10;\nconst HEIGHT_SPAN = 5.5;\nconst normTemp = (v) => ((v - T_MIN) / (T_MAX - T_MIN)) * GROUND_SPAN;\nconst normPressure = (v) => ((v - P_MIN) / (P_MAX - P_MIN)) * GROUND_SPAN;\nconst normHeight = (v) => ((v - zMin) / (zMax - zMin)) * HEIGHT_SPAN;\n\nconst COS30 = Math.cos(Math.PI / 6);\nconst SIN30 = Math.sin(Math.PI / 6);\nfunction project(groundA, height, groundB) {\n  return [(groundA - groundB) * COS30, (groundA + groundB) * SIN30 + height];\n}\n\n// Iso-projected position of every grid vertex, indexed [i][j].\nconst isoGrid = temps.map((temp, i) =>\n  pressures.map((pressure, j) => project(normTemp(temp), normHeight(zGrid[i][j]), normPressure(pressure)))\n);\n\n// One facet per grid cell: [avgYield, x0,y0, x1,y1, x2,y2, x3,y3, depth].\n// Depth (sum of ground indices) drives back-to-front painter's-algorithm order.\nconst facets = [];\nfor (let i = 0; i < N - 1; i++) {\n  for (let j = 0; j < N - 1; j++) {\n    const c00 = isoGrid[i][j];\n    const c10 = isoGrid[i + 1][j];\n    const c11 = isoGrid[i + 1][j + 1];\n    const c01 = isoGrid[i][j + 1];\n    const avgYield = (zGrid[i][j] + zGrid[i + 1][j] + zGrid[i + 1][j + 1] + zGrid[i][j + 1]) / 4;\n    facets.push({\n      depth: i + j,\n      value: [avgYield, c00[0], c00[1], c10[0], c10[1], c11[0], c11[1], c01[0], c01[1]],\n    });\n  }\n}\nfacets.sort((a, b) => b.depth - a.depth); // farthest cells first, nearest painted last (on top)\nconst facetData = facets.map((f) => f.value);\n\n// Axis guides run past the data extent so they read as open axes, not a box.\nconst originP = project(0, 0, 0);\nconst tempTipP = project(GROUND_SPAN * 1.15, 0, 0);\nconst pressureTipP = project(0, 0, GROUND_SPAN * 1.15);\nconst yieldTipP = project(0, HEIGHT_SPAN * 1.15, 0);\n\n// --- Frame geometry: lock x/y to one pixels-per-unit scale so the isometric\n// angles survive echarts' cartesian grid (which has no built-in aspect lock).\nconst allX = isoGrid.flat().map((p) => p[0]).concat([originP[0], tempTipP[0], pressureTipP[0], yieldTipP[0]]);\nconst allY = isoGrid.flat().map((p) => p[1]).concat([originP[1], tempTipP[1], pressureTipP[1], yieldTipP[1]]);\nconst padX = (Math.max(...allX) - Math.min(...allX)) * 0.1;\nconst padY = (Math.max(...allY) - Math.min(...allY)) * 0.08;\nconst xAxisMin = Math.min(...allX) - padX;\nconst xAxisMax = Math.max(...allX) + padX;\nconst yAxisMin = Math.min(...allY) - padY;\nconst yAxisMax = Math.max(...allY) + padY;\n\nconst marginTop = 110;\nconst marginBottom = 50;\nconst marginLeft = 130;\nconst marginRight = 210;\nconst availW = size.width - marginLeft - marginRight;\nconst availH = size.height - marginTop - marginBottom;\nconst dataW = xAxisMax - xAxisMin;\nconst dataH = yAxisMax - yAxisMin;\nconst gridScale = Math.min(availW / dataW, availH / dataH);\nconst gridWidth = dataW * gridScale;\nconst gridHeight = dataH * gridScale;\nconst gridLeft = marginLeft + (availW - gridWidth) / 2;\nconst gridTop = marginTop + (availH - gridHeight) / 2;\n\n// --- Title (fontsize scaled to the 67-char baseline) -------------------------\nconst titleText = \"Yield Response Surface · surface-basic · javascript · echarts · anyplot.ai\";\nconst titleFontSize = Math.max(15, Math.round(22 * Math.min(1, 67 / titleText.length)));\n\n// --- Init ---------------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\n// --- Option -------------------------------------------------------------------\nconst option = {\n  animation: false,\n  backgroundColor: \"transparent\",\n  color: t.palette,\n  title: {\n    text: titleText,\n    left: \"center\",\n    top: 30,\n    textStyle: { color: t.ink, fontSize: titleFontSize, fontWeight: \"medium\" },\n  },\n  visualMap: {\n    seriesIndex: 2,\n    dimension: 0,\n    min: zMin,\n    max: zMax,\n    inRange: { color: t.seq },\n    orient: \"vertical\",\n    right: 30,\n    top: \"middle\",\n    text: [\"High yield (%)\", \"Low yield (%)\"],\n    textStyle: { color: t.inkSoft, fontSize: 13 },\n    itemWidth: 14,\n    itemHeight: 140,\n  },\n  grid: { left: gridLeft, top: gridTop, width: gridWidth, height: gridHeight },\n  xAxis: { type: \"value\", min: xAxisMin, max: xAxisMax, show: false },\n  yAxis: { type: \"value\", min: yAxisMin, max: yAxisMax, show: false },\n  series: [\n    {\n      // Temperature axis guide\n      type: \"line\",\n      data: [\n        [originP[0], originP[1]],\n        [tempTipP[0], tempTipP[1]],\n      ],\n      showSymbol: false,\n      silent: true,\n      lineStyle: { color: t.inkSoft, width: 1.5 },\n      z: 1,\n    },\n    {\n      // Pressure axis guide\n      type: \"line\",\n      data: [\n        [originP[0], originP[1]],\n        [pressureTipP[0], pressureTipP[1]],\n      ],\n      showSymbol: false,\n      silent: true,\n      lineStyle: { color: t.inkSoft, width: 1.5 },\n      z: 1,\n    },\n    {\n      // The surface itself: one quad per grid cell, colored by yield.\n      type: \"custom\",\n      coordinateSystem: \"cartesian2d\",\n      xAxisIndex: 0,\n      yAxisIndex: 0,\n      data: facetData,\n      renderItem: (params, api) => {\n        const p0 = api.coord([api.value(1), api.value(2)]);\n        const p1 = api.coord([api.value(3), api.value(4)]);\n        const p2 = api.coord([api.value(5), api.value(6)]);\n        const p3 = api.coord([api.value(7), api.value(8)]);\n        return {\n          type: \"polygon\",\n          shape: { points: [p0, p1, p2, p3] },\n          style: { fill: api.visual(\"color\"), stroke: t.pageBg, lineWidth: 0.6 },\n        };\n      },\n      z: 2,\n    },\n  ],\n};\n\nchart.setOption(option);\n\n// Axis labels and unit ticks, anchored to real data coordinates converted to\n// pixels (not a decorative overlay) so they read directly off the static PNG.\nconst toPixel = (dataPoint) => chart.convertToPixel({ xAxisIndex: 0, yAxisIndex: 0 }, dataPoint);\nconst axisLabels = [\n  { point: tempTipP, text: \"Temperature (°C)\", dx: 15, dy: -8, font: `16px sans-serif` },\n  { point: pressureTipP, text: \"Pressure (bar)\", dx: -170, dy: -8, font: `16px sans-serif` },\n];\n\n// Temperature owns the shared ground origin (150 degC / 1 bar); pressure's\n// own min tick is dropped so the two labels don't land on the same pixel.\nconst tempTicks = [0, 0.5, 1].map((f) => ({\n  point: project(GROUND_SPAN * f, 0, 0),\n  text: `${Math.round(T_MIN + f * (T_MAX - T_MIN))}°C`,\n  dx: 4,\n  dy: 4,\n  font: \"11px sans-serif\",\n}));\nconst pressureTicks = [0.5, 1].map((f) => ({\n  point: project(0, 0, GROUND_SPAN * f),\n  text: `${(P_MIN + f * (P_MAX - P_MIN)).toFixed(0)} bar`,\n  dx: -46,\n  dy: -2,\n  font: \"11px sans-serif\",\n}));\n\nchart.setOption({\n  graphic: [...axisLabels, ...tempTicks, ...pressureTicks].map((a) => {\n    const pixel = toPixel([a.point[0], a.point[1]]);\n    return {\n      type: \"text\",\n      left: pixel[0] + a.dx,\n      top: pixel[1] + a.dy,\n      silent: true,\n      style: { text: a.text, fill: t.inkSoft, font: a.font },\n    };\n  }).concat([\n    {\n      // Z-axis title above the colorbar — height is encoded by both the\n      // surface's elevation and this scale, so the colorbar is the z-axis.\n      type: \"text\",\n      right: 15,\n      top: size.height / 2 - 130,\n      silent: true,\n      style: { text: \"Yield (%)\", fill: t.ink, font: \"16px sans-serif\", textAlign: \"right\" },\n    },\n  ]),\n});\n"}