{"spec_id":"surface-basic","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// surface-basic: Basic 3D Surface Plot\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 81/100 | Created: 2026-09-10\n//# anyplot-orientation: square\n\nconst t = window.ANYPLOT_TOKENS;\n\n// The harness loads only the core `highcharts.js` bundle (see prompts/library/\n// highcharts.md \"Forbidden patterns\") — no `highcharts-3d` (no surface/mesh\n// series exists in Highcharts at all, even in that module), no `modules/\n// coloraxis` or `modules/heatmap`, no `highcharts-more` (bubble). A true 3D\n// surface or a colorAxis-driven heatmap is therefore not representable.\n// Instead the height field is rendered as a tiled grid of square scatter\n// markers, colored per band by manual hex interpolation — a genuine static\n// encoding of z via color + position, not a simulated 3D effect.\n\n// --- Color interpolation (imprint_div — data is signed, meaningful zero) ----\nfunction hexToRgb(hex) {\n  const n = parseInt(hex.slice(1), 16);\n  return [(n >> 16) & 255, (n >> 8) & 255, n & 255];\n}\nfunction rgbToHex(rgb) {\n  return (\n    \"#\" +\n    rgb\n      .map((v) => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, \"0\"))\n      .join(\"\")\n  );\n}\nfunction lerpColor(hexA, hexB, frac) {\n  const a = hexToRgb(hexA);\n  const b = hexToRgb(hexB);\n  return rgbToHex(a.map((v, i) => v + (b[i] - v) * frac));\n}\nfunction divergingColor(frac) {\n  // frac in [0, 1]; 0 = most negative, 0.5 = zero, 1 = most positive\n  return frac < 0.5\n    ? lerpColor(t.div[0], t.div[1], frac / 0.5)\n    : lerpColor(t.div[1], t.div[2], (frac - 0.5) / 0.5);\n}\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Response surface z = sin(x) * cos(y), sampled on a 34x34 grid.\nconst NX = 34;\nconst NY = 34;\nconst HALF_RANGE = 4.5;\nconst xs = Array.from({ length: NX }, (_, i) => -HALF_RANGE + (2 * HALF_RANGE * i) / (NX - 1));\nconst ys = Array.from({ length: NY }, (_, i) => -HALF_RANGE + (2 * HALF_RANGE * i) / (NY - 1));\n\nlet zAbsMax = 0;\nconst points = [];\nfor (const y of ys) {\n  for (const x of xs) {\n    const z = Math.sin(x) * Math.cos(y);\n    points.push({ x, y, z });\n    zAbsMax = Math.max(zAbsMax, Math.abs(z));\n  }\n}\n\n// --- Bin into bands for a discrete, legend-backed color scale (pseudo-colorbar)\nconst NUM_BANDS = 7;\nconst bandsData = Array.from({ length: NUM_BANDS }, () => []);\nfor (const p of points) {\n  const frac = (p.z + zAbsMax) / (2 * zAbsMax);\n  const band = Math.min(NUM_BANDS - 1, Math.max(0, Math.floor(frac * NUM_BANDS)));\n  bandsData[band].push(p);\n}\n\nconst series = bandsData\n  .map((data, k) => {\n    const loEdge = -zAbsMax + (2 * zAbsMax * k) / NUM_BANDS;\n    const hiEdge = -zAbsMax + (2 * zAbsMax * (k + 1)) / NUM_BANDS;\n    const color = divergingColor((k + 0.5) / NUM_BANDS);\n    return {\n      name: `${loEdge.toFixed(2)} to ${hiEdge.toFixed(2)}`,\n      color,\n      data,\n      // A thin ink-toned stroke keeps every band visible even when its fill\n      // is near-identical to the page background (the middle diverging band\n      // sits within ~1 RGB unit of both #FAF8F1 and #1A1A17).\n      marker: { symbol: \"square\", radius: 6, fillColor: color, lineWidth: 1, lineColor: t.inkSoft },\n    };\n  })\n  .filter((s) => s.data.length > 0);\n\n// --- Chart -------------------------------------------------------------------\nHighcharts.chart(\"container\", {\n  chart: {\n    type: \"scatter\",\n    backgroundColor: \"transparent\",\n    animation: false,\n    style: { fontFamily: \"inherit\" },\n    events: {\n      // Markers must tile the (x, y) grid with no gaps to read as a\n      // continuous surface. The true plot-area pixel size only exists after\n      // Highcharts lays out the title/legend/axes, so the radius is derived\n      // from the rendered axis scale (toPixels) rather than guessed margins.\n      load: function () {\n        const chart = this;\n        // The y-axis title + tick labels reserve real width on the plot\n        // area's left edge; nothing reserves matching width on the right by\n        // default, so the tiled grid sits flush against the right canvas\n        // edge while the left keeps a full margin. Mirror the (larger) left\n        // margin onto the right before sizing markers, so both edges match.\n        if (chart.plotLeft > chart.marginRight) {\n          chart.update({ chart: { marginRight: chart.plotLeft } }, true);\n        }\n        const xPx = Math.abs(chart.xAxis[0].toPixels(xs[1]) - chart.xAxis[0].toPixels(xs[0]));\n        const yPx = Math.abs(chart.yAxis[0].toPixels(ys[1]) - chart.yAxis[0].toPixels(ys[0]));\n        const radius = Math.max(2, (Math.min(xPx, yPx) / 2) * 0.96);\n        chart.series.forEach((s) => s.update({ marker: { radius } }, false));\n        chart.redraw();\n      },\n    },\n  },\n  credits: { enabled: false },\n  colors: t.palette,\n  title: {\n    text: \"surface-basic · javascript · highcharts · anyplot.ai\",\n    style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n  },\n  subtitle: {\n    text: \"Response surface z = sin(x)·cos(y), sampled on a 34×34 grid — color encodes height\",\n    style: { color: t.inkSoft, fontSize: \"14px\" },\n  },\n  // Axis lines off: the tiled grid already delineates the data extent, and a\n  // drawn axis line renders on top of the boundary markers, slicing through\n  // them at every seam once markers are sized to tile edge-to-edge.\n  xAxis: {\n    title: { text: \"x\", style: { color: t.inkSoft, fontSize: \"16px\" } },\n    min: -HALF_RANGE,\n    max: HALF_RANGE,\n    startOnTick: false,\n    endOnTick: false,\n    minPadding: 0.03,\n    maxPadding: 0.03,\n    lineWidth: 0,\n    tickLength: 0,\n    gridLineWidth: 0,\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n  },\n  yAxis: {\n    title: { text: \"y\", style: { color: t.inkSoft, fontSize: \"16px\" } },\n    min: -HALF_RANGE,\n    max: HALF_RANGE,\n    startOnTick: false,\n    endOnTick: false,\n    minPadding: 0.03,\n    maxPadding: 0.03,\n    lineWidth: 0,\n    tickLength: 0,\n    gridLineWidth: 0,\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n  },\n  legend: {\n    title: { text: \"Height z\", style: { color: t.inkSoft, fontSize: \"14px\" } },\n    itemStyle: { color: t.inkSoft, fontSize: \"14px\" },\n    itemHoverStyle: { color: t.ink },\n    verticalAlign: \"bottom\",\n    align: \"center\",\n    layout: \"horizontal\",\n    symbolHeight: 14,\n    symbolWidth: 14,\n  },\n  tooltip: {\n    formatter: function () {\n      return `x: ${this.point.x.toFixed(2)}<br>y: ${this.point.y.toFixed(2)}<br>z: ${this.point.z.toFixed(3)}`;\n    },\n  },\n  plotOptions: {\n    series: { animation: false, states: { hover: { enabled: false } } },\n  },\n  series,\n});\n"}