{"spec_id":"contour-3d","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// contour-3d: 3D Contour Plot\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 84/100 | Created: 2026-09-10\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data: \"peaks\" response surface z = f(x, y) on a 33x33 grid, read as ---\n// terrain elevation relative to sea level (z = 0) ---------------------------\nconst GRID_N = 33;\nconst RANGE = 3;\nconst grid = Array.from({ length: GRID_N }, (_, i) => -RANGE + (2 * RANGE * i) / (GRID_N - 1));\nconst peaks = (x, y) =>\n  3 * (1 - x) ** 2 * Math.exp(-(x ** 2) - (y + 1) ** 2) -\n  10 * (x / 5 - x ** 3 - y ** 5) * Math.exp(-(x ** 2) - y ** 2) -\n  (1 / 3) * Math.exp(-((x + 1) ** 2) - y ** 2);\nconst zGrid = grid.map((y) => grid.map((x) => peaks(x, y)));\nlet zMin = Infinity;\nlet zMax = -Infinity;\nzGrid.forEach((row) =>\n  row.forEach((z) => {\n    if (z < zMin) zMin = z;\n    if (z > zMax) zMax = z;\n  })\n);\nconst M = Math.max(Math.abs(zMin), Math.abs(zMax));\nconst floorZ = zMin - (zMax - zMin) * 0.3;\nconst topZ = zMax + (zMax - zMin) * 0.15;\nconst fmt = (v) => {\n  const s = v.toFixed(1);\n  return s === \"-0.0\" ? \"0.0\" : s;\n};\n\n// --- 3D -> 2D orthographic projection, parameterized so the whole scene can\n// be reprojected live on pointer-drag (see \"Rotation\" near the bottom).\n// Highcharts core has no chart3d/highcharts-3d module (and no polygon/\n// colorAxis — those live in highcharts-more, also unloaded), so the surface,\n// its isolines, and the legend are all projected/drawn by hand as ordinary\n// `line`/`scatter` series — the same math a native 3D engine applies before\n// rasterizing, just computed here instead of in an unavailable add-on.\nconst makeProjector = (azimDeg, elevDeg) => {\n  const azim = (azimDeg * Math.PI) / 180;\n  const elev = (elevDeg * Math.PI) / 180;\n  const cosAz = Math.cos(azim);\n  const sinAz = Math.sin(azim);\n  const cosEl = Math.cos(elev);\n  const sinEl = Math.sin(elev);\n  return (x, y, z) => {\n    const xr = x * cosAz + y * sinAz;\n    const yr = -x * sinAz + y * cosAz;\n    const zScreen = yr * sinEl + z * cosEl;\n    return [xr, zScreen];\n  };\n};\nconst AZIM0 = -50;\nconst ELEV0 = 30;\n\n// --- Color: diverging imprint_div gradient, centered on sea level (z = 0) --\nconst hexToRgb = (hex) => {\n  const n = parseInt(hex.slice(1), 16);\n  return [(n >> 16) & 255, (n >> 8) & 255, n & 255];\n};\nconst divLo = hexToRgb(t.div[0]);\nconst divHi = hexToRgb(t.div[2]);\n// The canonical imprint_div midpoint equals the page background (by design,\n// for area fills that should \"fade to neutral\" at zero) — but that midpoint\n// is theme-adaptive, so blending isoline strokes toward it (or toward any\n// other t.* token) would render the SAME elevation level in a different hue\n// per theme. Data colors must be pixel-identical across themes, so strokes\n// blend toward this fixed literal neutral instead — still the two Imprint\n// diverging anchors, just a theme-invariant midpoint for stroked geometry.\nconst NEUTRAL_MID = [0x81, 0x80, 0x7a];\nconst lerpRgb = (a, b, f) => [0, 1, 2].map((i) => Math.round(a[i] + (b[i] - a[i]) * f));\nconst rgbaStr = ([r, g, b], a) => `rgba(${r}, ${g}, ${b}, ${a})`;\nconst colorAt = (z, alpha = 1) => {\n  const raw = Math.min(1, Math.max(-1, z / M)); // -1..1 around sea level\n  const f = Math.sign(raw) * Math.sqrt(Math.abs(raw)); // sqrt eases weak levels toward a legible tint\n  return f < 0 ? rgbaStr(lerpRgb(NEUTRAL_MID, divLo, -f), alpha) : rgbaStr(lerpRgb(NEUTRAL_MID, divHi, f), alpha);\n};\n\n// --- Contour levels: 6 isolines spanning the elevation range, centered on 0\nconst N_LEVELS = 6;\nconst levels = Array.from({ length: N_LEVELS }, (_, i) => -M + ((i + 1) * (2 * M)) / (N_LEVELS + 1));\n\n// --- Isolines via marching-triangles: split each grid cell into 2 -----------\n// triangles, then for every triangle + level, linearly interpolate along the\n// crossing edges to get a 2-point segment lying exactly on that contour.\n// Segments stay in 3D space (a trailing `null` marks a break between\n// segments) so they can be reprojected on every rotation frame.\nconst lerpPt = (a, b, level) => {\n  const f = (level - a[2]) / (b[2] - a[2]);\n  return [a[0] + (b[0] - a[0]) * f, a[1] + (b[1] - a[1]) * f, level];\n};\nconst triangleSegment = (v0, v1, v2, level) => {\n  const pts = [];\n  [\n    [v0, v1],\n    [v1, v2],\n    [v2, v0],\n  ].forEach(([a, b]) => {\n    if ((a[2] - level) * (b[2] - level) < 0) pts.push(lerpPt(a, b, level));\n  });\n  return pts.length === 2 ? pts : null;\n};\n\nconst surfaceSegments3D = levels.map(() => []);\nconst baseSegments3D = levels.map(() => []);\nfor (let j = 0; j < GRID_N - 1; j++) {\n  for (let i = 0; i < GRID_N - 1; i++) {\n    const c00 = [grid[i], grid[j], zGrid[j][i]];\n    const c10 = [grid[i + 1], grid[j], zGrid[j][i + 1]];\n    const c01 = [grid[i], grid[j + 1], zGrid[j + 1][i]];\n    const c11 = [grid[i + 1], grid[j + 1], zGrid[j + 1][i + 1]];\n    [\n      [c00, c10, c01],\n      [c10, c11, c01],\n    ].forEach((tri) => {\n      levels.forEach((level, li) => {\n        const seg = triangleSegment(tri[0], tri[1], tri[2], level);\n        if (!seg) return;\n        const [p1, p2] = seg;\n        surfaceSegments3D[li].push(p1, p2, null);\n        baseSegments3D[li].push([p1[0], p1[1], floorZ], [p2[0], p2[1], floorZ], null);\n      });\n    });\n  }\n}\n\n// --- Sparse wireframe mesh: carries the surface geometry between isolines --\n// Drawn in a fixed neutral (not value-coded) so it reads as structure — the\n// diverging colormap is reserved for the isolines, the actual data encoding.\n// Thinned further (larger stride, lower alpha) so the isoline rings read\n// clearly instead of getting lost in crossing mesh lines.\nconst MESH_STRIDE = 3;\nconst meshColor = rgbaStr(hexToRgb(t.inkSoft), 0.35);\nconst meshRows3D = [];\nfor (let j = 0; j < GRID_N; j += MESH_STRIDE) {\n  meshRows3D.push(grid.map((x, i) => [x, grid[j], zGrid[j][i]]));\n}\nconst meshCols3D = [];\nfor (let i = 0; i < GRID_N; i += MESH_STRIDE) {\n  meshCols3D.push(grid.map((y, j) => [grid[i], y, zGrid[j][i]]));\n}\n\n// --- Axis ticks + titles as labeled points (core dataLabels, no modules) ---\n// Kept as 3D coordinate + label definitions so they rotate along with the\n// frame; the per-point pixel offsets are tuned for the initial pose and stay\n// a good-enough approximation while dragging.\nconst tickVals = [-RANGE, 0, RANGE];\nconst tickPointsDef = [];\ntickVals.forEach((v) => {\n  const cornerBoost = v === -RANGE ? 16 : 0;\n  tickPointsDef.push({ p: [v, -RANGE, floorZ], name: String(v), dataLabels: { x: 0, y: 18 + cornerBoost } });\n});\ntickVals.forEach((v) => {\n  const cornerBoost = v === -RANGE ? 16 : 0;\n  tickPointsDef.push({ p: [-RANGE, v, floorZ], name: String(v), dataLabels: { x: -22 - cornerBoost, y: 0 } });\n});\n[zMin, 0, zMax].forEach((v) => {\n  tickPointsDef.push({ p: [-RANGE, -RANGE, v], name: fmt(v), dataLabels: { x: -26, y: 0 } });\n});\n\nconst titlePointsDef = [\n  { p: [RANGE * 1.22, -RANGE, floorZ], name: \"X\" },\n  { p: [-RANGE, RANGE * 1.22, floorZ], name: \"Y\" },\n  { p: [-RANGE, -RANGE, topZ * 1.12], name: \"Elevation (m)\" },\n];\n\n// --- Frame builder: reprojects every series + refits the axes/legend for a --\n// given (azimuth, elevation) pair. Called once for the initial static pose\n// and again on every pointer-drag frame (see \"Rotation\" below).\nconst buildFrame = (azimDeg, elevDeg) => {\n  const proj = makeProjector(azimDeg, elevDeg);\n  const reproj = (pts3D) => pts3D.map((p) => (p === null ? [null, null] : proj(...p)));\n\n  const meshRowsData = meshRows3D.map(reproj);\n  const meshColsData = meshCols3D.map(reproj);\n  const isolineSurfaceData = surfaceSegments3D.map(reproj);\n  const isolineBaseData = baseSegments3D.map(reproj);\n\n  const corner = proj(-RANGE, -RANGE, floorZ);\n  const xEnd = proj(RANGE, -RANGE, floorZ);\n  const yEnd = proj(-RANGE, RANGE, floorZ);\n  const zEnd = proj(-RANGE, -RANGE, topZ);\n  const axisFrameData = [\n    [corner, xEnd],\n    [corner, yEnd],\n    [corner, zEnd],\n  ];\n\n  const tickData = tickPointsDef.map(({ p, name, dataLabels }) => {\n    const [sx, sy] = proj(...p);\n    return { x: sx, y: sy, name, dataLabels };\n  });\n  const titleData = titlePointsDef.map(({ p, name }) => {\n    const [sx, sy] = proj(...p);\n    return { x: sx, y: sy, name };\n  });\n\n  // --- Preliminary bounds (everything except the manual colorbar legend) ---\n  const prelimPts = [];\n  meshRowsData.forEach((s) => s.forEach((p) => prelimPts.push(p)));\n  meshColsData.forEach((s) => s.forEach((p) => prelimPts.push(p)));\n  isolineSurfaceData.forEach((s) => s.forEach((p) => p[1] !== null && prelimPts.push(p)));\n  [corner, xEnd, yEnd, zEnd].forEach((p) => prelimPts.push(p));\n  [...tickData, ...titleData].forEach((p) => prelimPts.push([p.x, p.y]));\n  const prelimX = prelimPts.map((p) => p[0]);\n  const prelimY = prelimPts.map((p) => p[1]);\n  const pMinX = Math.min(...prelimX);\n  const pMaxX = Math.max(...prelimX);\n  const pMinY = Math.min(...prelimY);\n  const pMaxY = Math.max(...prelimY);\n\n  // --- Manual colorbar: one swatch + value label per isoline level ---------\n  // Highcharts core has no ColorAxis (map/heatmap-only), so the level legend\n  // is built from ordinary labeled scatter points — the same technique used\n  // for the axis ticks above.\n  const legendX = pMaxX + (pMaxX - pMinX) * 0.12;\n  const legendTop = pMaxY - (pMaxY - pMinY) * 0.08;\n  const legendStep = (pMaxY - pMinY) * 0.12;\n  const swatchLevels = [...levels].sort((a, b) => b - a);\n  const legendSwatchData = swatchLevels.map((level, k) => ({\n    x: legendX,\n    y: legendTop - (k + 1) * legendStep,\n    name: fmt(level),\n    color: colorAt(level),\n  }));\n  const legendHeaderData = [{ x: legendX, y: legendTop, name: \"Elevation levels\" }];\n\n  // --- Final bounds: everything, with extra right-side room for the legend -\n  const allX = [...prelimX, ...legendSwatchData.map((p) => p.x + (pMaxX - pMinX) * 0.12)];\n  const allY = [...prelimY, legendTop + legendStep, ...legendSwatchData.map((p) => p.y)];\n  const padX = (Math.max(...allX) - Math.min(...allX)) * 0.06;\n  const padY = (Math.max(...allY) - Math.min(...allY)) * 0.08;\n\n  return {\n    meshRowsData,\n    meshColsData,\n    isolineSurfaceData,\n    isolineBaseData,\n    axisFrameData,\n    tickData,\n    titleData,\n    legendSwatchData,\n    legendHeaderData,\n    xMin: Math.min(...allX) - padX,\n    xMax: Math.max(...allX) + padX,\n    yMin: Math.min(...allY) - padY,\n    yMax: Math.max(...allY) + padY,\n  };\n};\n\nconst frame0 = buildFrame(AZIM0, ELEV0);\n\nconst meshRowSeries = frame0.meshRowsData.map((data) => ({\n  type: \"line\",\n  data,\n  color: meshColor,\n  lineWidth: 1,\n  marker: { enabled: false },\n  enableMouseTracking: false,\n  showInLegend: false,\n}));\nconst meshColSeries = frame0.meshColsData.map((data) => ({\n  type: \"line\",\n  data,\n  color: meshColor,\n  lineWidth: 1,\n  marker: { enabled: false },\n  enableMouseTracking: false,\n  showInLegend: false,\n}));\nconst isolineBaseSeries = levels.map((level, li) => ({\n  type: \"line\",\n  data: frame0.isolineBaseData[li],\n  color: colorAt(level, 0.55),\n  lineWidth: 1.4,\n  dashStyle: \"ShortDot\",\n  marker: { enabled: false },\n  enableMouseTracking: false,\n  showInLegend: false,\n}));\nconst isolineSurfaceSeries = levels.map((level, li) => ({\n  type: \"line\",\n  data: frame0.isolineSurfaceData[li],\n  color: colorAt(level),\n  lineWidth: 3.2,\n  marker: { enabled: false },\n  enableMouseTracking: false,\n  showInLegend: false,\n}));\nconst axisFrameSeries = frame0.axisFrameData.map((data) => ({\n  type: \"line\",\n  data,\n  color: t.inkSoft,\n  lineWidth: 2,\n  marker: { enabled: false },\n  enableMouseTracking: false,\n  showInLegend: false,\n}));\nconst tickSeries = {\n  type: \"scatter\",\n  data: frame0.tickData,\n  marker: { enabled: false },\n  enableMouseTracking: false,\n  showInLegend: false,\n  dataLabels: {\n    enabled: true,\n    format: \"{point.name}\",\n    allowOverlap: true,\n    style: { color: t.inkSoft, fontSize: \"13px\", textOutline: \"none\" },\n  },\n};\nconst titleSeries = {\n  type: \"scatter\",\n  data: frame0.titleData,\n  marker: { enabled: false },\n  enableMouseTracking: false,\n  showInLegend: false,\n  dataLabels: {\n    enabled: true,\n    format: \"{point.name}\",\n    allowOverlap: true,\n    style: { color: t.ink, fontSize: \"16px\", fontWeight: \"600\", textOutline: \"none\" },\n  },\n};\nconst legendSwatchSeries = {\n  type: \"scatter\",\n  data: frame0.legendSwatchData,\n  marker: { symbol: \"square\", radius: 9, lineWidth: 0 },\n  enableMouseTracking: false,\n  showInLegend: false,\n  dataLabels: {\n    enabled: true,\n    format: \"{point.name}\",\n    align: \"left\",\n    x: 18,\n    allowOverlap: true,\n    style: { color: t.inkSoft, fontSize: \"13px\", textOutline: \"none\" },\n  },\n};\nconst legendHeaderSeries = {\n  type: \"scatter\",\n  data: frame0.legendHeaderData,\n  marker: { enabled: false },\n  enableMouseTracking: false,\n  showInLegend: false,\n  dataLabels: {\n    enabled: true,\n    format: \"{point.name}\",\n    align: \"left\",\n    x: -9,\n    allowOverlap: true,\n    style: { color: t.ink, fontSize: \"14px\", fontWeight: \"600\", textOutline: \"none\" },\n  },\n};\n\n// --- Chart -------------------------------------------------------------\nconst chart = Highcharts.chart(\"container\", {\n  chart: {\n    type: \"line\",\n    backgroundColor: \"transparent\",\n    animation: false,\n    style: { fontFamily: \"inherit\" },\n  },\n  credits: { enabled: false },\n  title: {\n    text: \"contour-3d · javascript · highcharts · anyplot.ai\",\n    style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n  },\n  subtitle: {\n    text: \"Terrain elevation relative to sea level, with isolines on the surface and projected onto the base plane · drag to rotate\",\n    style: { color: t.inkSoft, fontSize: \"14px\" },\n  },\n  xAxis: {\n    visible: false,\n    min: frame0.xMin,\n    max: frame0.xMax,\n    startOnTick: false,\n    endOnTick: false,\n  },\n  yAxis: {\n    visible: false,\n    min: frame0.yMin,\n    max: frame0.yMax,\n    startOnTick: false,\n    endOnTick: false,\n    title: { text: null },\n  },\n  legend: { enabled: false },\n  tooltip: { enabled: false },\n  plotOptions: { series: { animation: false } },\n  series: [\n    ...meshRowSeries,\n    ...meshColSeries,\n    ...isolineBaseSeries,\n    ...isolineSurfaceSeries,\n    ...axisFrameSeries,\n    tickSeries,\n    titleSeries,\n    legendSwatchSeries,\n    legendHeaderSeries,\n  ],\n});\n\n// --- Rotation: pointer-drag recomputes azimuth/elevation and reprojects ----\n// every series (plus refits the axes/legend), so the HTML export can be\n// explored interactively even though Highcharts core has no native 3D/orbit\n// controls. The static PNG screenshot never fires a pointer event, so the\n// initial AZIM0/ELEV0 pose above is exactly what gets captured.\nconst applyFrame = (frame) => {\n  let idx = 0;\n  frame.meshRowsData.forEach((data) => chart.series[idx++].setData(data, false));\n  frame.meshColsData.forEach((data) => chart.series[idx++].setData(data, false));\n  frame.isolineBaseData.forEach((data) => chart.series[idx++].setData(data, false));\n  frame.isolineSurfaceData.forEach((data) => chart.series[idx++].setData(data, false));\n  frame.axisFrameData.forEach((data) => chart.series[idx++].setData(data, false));\n  chart.series[idx++].setData(frame.tickData, false);\n  chart.series[idx++].setData(frame.titleData, false);\n  chart.series[idx++].setData(frame.legendSwatchData, false);\n  chart.series[idx++].setData(frame.legendHeaderData, false);\n  chart.xAxis[0].setExtremes(frame.xMin, frame.xMax, false);\n  chart.yAxis[0].setExtremes(frame.yMin, frame.yMax, false);\n  chart.redraw();\n};\n\nconst ROTATE_SENSITIVITY = 0.35; // degrees per pixel dragged\nconst ELEV_MIN = 5;\nconst ELEV_MAX = 85; // stay short of 90° to avoid a gimbal flip\nlet azimDeg = AZIM0;\nlet elevDeg = ELEV0;\nlet dragging = false;\nlet lastX = 0;\nlet lastY = 0;\n\nchart.container.style.cursor = \"grab\";\nchart.container.addEventListener(\"pointerdown\", (evt) => {\n  dragging = true;\n  lastX = evt.clientX;\n  lastY = evt.clientY;\n  chart.container.style.cursor = \"grabbing\";\n});\nwindow.addEventListener(\"pointermove\", (evt) => {\n  if (!dragging) return;\n  const dx = evt.clientX - lastX;\n  const dy = evt.clientY - lastY;\n  lastX = evt.clientX;\n  lastY = evt.clientY;\n  azimDeg -= dx * ROTATE_SENSITIVITY;\n  elevDeg = Math.min(ELEV_MAX, Math.max(ELEV_MIN, elevDeg + dy * ROTATE_SENSITIVITY));\n  applyFrame(buildFrame(azimDeg, elevDeg));\n});\nwindow.addEventListener(\"pointerup\", () => {\n  dragging = false;\n  chart.container.style.cursor = \"grab\";\n});\n"}