{"spec_id":"contour-3d","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// contour-3d: 3D Contour Plot\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-09-10\n//# anyplot-orientation: square\n\nconst t = window.ANYPLOT_TOKENS;\nconst INK = t.ink;\nconst INK_SOFT = t.inkSoft;\n\n// --- Data: optimization landscape — two local maxima joined by a saddle ridge, so\n// both the overall shape and specific level curves matter (spec application #1) --\nconst GRID_N = 42; // grid points per axis (spec recommends 30x30 - 50x50)\nconst X_MIN = -4, X_MAX = 4, Y_MIN = -4, Y_MAX = 4;\n\nconst bump = (x, y, cx, cy, sx, sy, amp) =>\n  amp * Math.exp(-(((x - cx) ** 2) / (2 * sx * sx) + ((y - cy) ** 2) / (2 * sy * sy)));\n\nconst heightFn = (x, y) =>\n  bump(x, y, 1.6, 1.5, 1.3, 1.3, 2.4) +\n  bump(x, y, -1.8, -1.6, 1.5, 1.5, 1.9) +\n  bump(x, y, 0, 0, 2.6, 2.6, 0.55) +\n  0.12;\n\nconst xs = Array.from({ length: GRID_N }, (_, i) => X_MIN + ((X_MAX - X_MIN) * i) / (GRID_N - 1));\nconst ys = Array.from({ length: GRID_N }, (_, j) => Y_MIN + ((Y_MAX - Y_MIN) * j) / (GRID_N - 1));\nconst Z = ys.map((y) => xs.map((x) => heightFn(x, y)));\n\nlet zMin = Infinity, zMax = -Infinity;\nfor (const row of Z) for (const v of row) { if (v < zMin) zMin = v; if (v > zMax) zMax = v; }\n\n// --- Normalize into a stable cube + camera (elevation/azimuth, true perspective) --\n// Standard axonometric-camera technique: build a right/up/forward basis from\n// elevation + azimuth, then divide by depth-along-view for perspective.\nconst xHalf = (X_MAX - X_MIN) / 2, xMid = (X_MAX + X_MIN) / 2;\nconst yHalf = (Y_MAX - Y_MIN) / 2, yMid = (Y_MAX + Y_MIN) / 2;\nconst zHalf = (zMax - zMin) / 2, zMid = (zMax + zMin) / 2;\nconst Z_SCALE = 0.85; // vertical exaggeration relative to the xy half-extent\nconst norm = (x, y, z) => [(x - xMid) / xHalf, (y - yMid) / yHalf, ((z - zMid) / zHalf) * Z_SCALE];\n\nconst ELEV_DEG = 30, AZIM_DEG = -52;\nconst elev = (ELEV_DEG * Math.PI) / 180;\nconst azim = (AZIM_DEG * Math.PI) / 180;\nconst camDir = [Math.cos(elev) * Math.cos(azim), Math.cos(elev) * Math.sin(azim), Math.sin(elev)];\nconst worldUp = [0, 0, 1];\nconst cross = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];\nconst normalize = (a) => { const l = Math.hypot(a[0], a[1], a[2]); return [a[0] / l, a[1] / l, a[2] / l]; };\nconst right = normalize(cross(camDir, worldUp));\nconst camUp = cross(right, camDir);\n\nconst CAM_DIST = 5.0, FOCAL = 5.0;\nconst projectNorm = (nx, ny, nz) => {\n  const px = nx * right[0] + ny * right[1] + nz * right[2];\n  const py = nx * camUp[0] + ny * camUp[1] + nz * camUp[2];\n  const pd = nx * camDir[0] + ny * camDir[1] + nz * camDir[2];\n  const depth = CAM_DIST - pd;\n  const scale = FOCAL / depth;\n  return { x: px * scale, y: py * scale, depth, scale };\n};\nconst project = (x, y, z) => projectNorm(...norm(x, y, z));\n\n// --- Height -> Imprint sequential colour, quantized into discrete contour bands --\nconst hexToRgb = (h) => [1, 3, 5].map((i) => parseInt(h.slice(i, i + 2), 16));\nconst seqLo = hexToRgb(t.seq[0]), seqHi = hexToRgb(t.seq[1]);\nconst lerpRgb = (a, b, f) => a.map((v, i) => Math.round(v + (b[i] - v) * f));\nconst lerp = (a, b, f) => a + (b - a) * f;\nconst clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));\n\nconst NUM_BANDS = 12;\nconst bandIndex = (z) => clamp(Math.floor(((z - zMin) / (zMax - zMin)) * NUM_BANDS), 0, NUM_BANDS - 1);\nconst bandColor = (z) => {\n  const [r, g, b] = lerpRgb(seqLo, seqHi, (bandIndex(z) + 0.5) / NUM_BANDS);\n  return `rgb(${r},${g},${b})`;\n};\n\nconst isoThresholds = [];\nfor (let k = 1; k < NUM_BANDS; k++) isoThresholds.push(zMin + ((zMax - zMin) * k) / NUM_BANDS);\n\n// --- Marching squares (same edge/segment convention as anyplot's 2D contour entries) --\n// For each 4-bit corner code (BL=bit0, BR=bit1, TR=bit2, TL=bit3, 1=above threshold),\n// which pairs of edge indices to connect as a line segment.\n// Edges: 0=bottom (BL-BR), 1=right (BR-TR), 2=top (TL-TR), 3=left (BL-TL)\nconst SEG = [\n  [], [[0, 3]], [[0, 1]], [[3, 1]], [[1, 2]], [[0, 3], [1, 2]], [[0, 2]], [[3, 2]],\n  [[3, 2]], [[0, 2]], [[0, 1], [2, 3]], [[1, 2]], [[3, 1]], [[0, 1]], [[0, 3]], [],\n];\nconst edgeXY = (e, i, j, z00, z10, z11, z01, thresh) => {\n  switch (e) {\n    case 0: return [lerp(xs[i], xs[i + 1], (thresh - z00) / (z10 - z00)), ys[j]];\n    case 1: return [xs[i + 1], lerp(ys[j], ys[j + 1], (thresh - z10) / (z11 - z10))];\n    case 2: return [lerp(xs[i], xs[i + 1], (thresh - z01) / (z11 - z01)), ys[j + 1]];\n    case 3: return [xs[i], lerp(ys[j], ys[j + 1], (thresh - z00) / (z01 - z00))];\n  }\n};\n\n// --- Build surface quads (painter's-algorithm depth) + on-surface isolines ------\nconst BASE_LINE_W = 1.6;\nconst drawItems = []; // { kind: \"quad\" | \"line\", ..., depth }\n\nfor (let j = 0; j < GRID_N - 1; j++) {\n  for (let i = 0; i < GRID_N - 1; i++) {\n    const z00 = Z[j][i], z10 = Z[j][i + 1], z11 = Z[j + 1][i + 1], z01 = Z[j + 1][i];\n    const corners = [\n      [xs[i], ys[j], z00], [xs[i + 1], ys[j], z10],\n      [xs[i + 1], ys[j + 1], z11], [xs[i], ys[j + 1], z01],\n    ];\n    const pts = corners.map(([x, y, z]) => project(x, y, z));\n    const depth = pts.reduce((s, p) => s + p.depth, 0) / 4;\n    drawItems.push({ kind: \"quad\", pts, color: bandColor((z00 + z10 + z11 + z01) / 4), depth });\n\n    for (const thresh of isoThresholds) {\n      const code =\n        (z00 >= thresh ? 1 : 0) | (z10 >= thresh ? 2 : 0) |\n        (z11 >= thresh ? 4 : 0) | (z01 >= thresh ? 8 : 0);\n      for (const [e0, e1] of SEG[code]) {\n        const [x1, y1] = edgeXY(e0, i, j, z00, z10, z11, z01, thresh);\n        const [x2, y2] = edgeXY(e1, i, j, z00, z10, z11, z01, thresh);\n        const p1 = project(x1, y1, thresh), p2 = project(x2, y2, thresh);\n        drawItems.push({\n          kind: \"line\", p1, p2,\n          width: BASE_LINE_W * clamp((p1.scale + p2.scale) / 2, 0.85, 1.3),\n          depth: (p1.depth + p2.depth) / 2,\n        });\n      }\n    }\n  }\n}\ndrawItems.sort((a, b) => b.depth - a.depth); // painter's algorithm: farthest first\n\n// --- Floor reference: same band fill + isolines, flattened onto the base plane --\nconst floorQuads = [];\nconst floorLines = [];\nfor (let j = 0; j < GRID_N - 1; j++) {\n  for (let i = 0; i < GRID_N - 1; i++) {\n    const z00 = Z[j][i], z10 = Z[j][i + 1], z11 = Z[j + 1][i + 1], z01 = Z[j + 1][i];\n    const pts = [[xs[i], ys[j]], [xs[i + 1], ys[j]], [xs[i + 1], ys[j + 1]], [xs[i], ys[j + 1]]]\n      .map(([x, y]) => project(x, y, zMin));\n    floorQuads.push({ pts, color: bandColor((z00 + z10 + z11 + z01) / 4) });\n\n    for (const thresh of isoThresholds) {\n      const code =\n        (z00 >= thresh ? 1 : 0) | (z10 >= thresh ? 2 : 0) |\n        (z11 >= thresh ? 4 : 0) | (z01 >= thresh ? 8 : 0);\n      for (const [e0, e1] of SEG[code]) {\n        const [x1, y1] = edgeXY(e0, i, j, z00, z10, z11, z01, thresh);\n        const [x2, y2] = edgeXY(e1, i, j, z00, z10, z11, z01, thresh);\n        floorLines.push({ p1: project(x1, y1, zMin), p2: project(x2, y2, zMin) });\n      }\n    }\n  }\n}\n\n// --- Axis box: pick the farthest corner so axes sit behind the mesh -------------\nlet axisCorner = null, bestDepth = -Infinity;\nfor (const sx of [-1, 1]) for (const sy of [-1, 1]) for (const sz of [-1, 1]) {\n  const d = projectNorm(sx, sy, sz * Z_SCALE).depth;\n  if (d > bestDepth) { bestDepth = d; axisCorner = [sx, sy, sz]; }\n}\nconst [cSignX, cSignY, cSignZ] = axisCorner;\nconst xAtCorner = cSignX > 0 ? X_MAX : X_MIN;\nconst yAtCorner = cSignY > 0 ? Y_MAX : Y_MIN;\nconst zAtCorner = cSignZ > 0 ? zMax : zMin;\n\nconst zTicks = [0, 1, 2, 3, 4].map((k) => +(zMin + ((zMax - zMin) * k) / 4).toFixed(2));\nconst axisEdges = [\n  { from: [X_MIN, yAtCorner, zAtCorner], to: [X_MAX, yAtCorner, zAtCorner], ticks: [-4, -2, 0, 2, 4], label: \"X\", fmt: (v) => `${v}` },\n  { from: [xAtCorner, Y_MIN, zAtCorner], to: [xAtCorner, Y_MAX, zAtCorner], ticks: [-4, -2, 0, 2, 4], label: \"Y\", fmt: (v) => `${v}` },\n  { from: [xAtCorner, yAtCorner, zMin], to: [xAtCorner, yAtCorner, zMax], ticks: zTicks, label: \"Z\", fmt: (v) => v.toFixed(2) },\n];\n\n// --- Fit chart scales to the projected content (no clipping, no guessing) -------\nlet minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;\nconst consider = (p) => { if (p.x < minX) minX = p.x; if (p.x > maxX) maxX = p.x; if (p.y < minY) minY = p.y; if (p.y > maxY) maxY = p.y; };\ndrawItems.forEach((it) => { if (it.kind === \"quad\") it.pts.forEach(consider); else { consider(it.p1); consider(it.p2); } });\nfloorQuads.forEach((q) => q.pts.forEach(consider));\naxisEdges.forEach((e) => { consider(project(...e.from)); consider(project(...e.to)); });\n\nconst MARGIN = 0.32; // room for tick labels + axis titles outside the box\nlet halfX = ((maxX - minX) / 2) * (1 + MARGIN);\nlet halfY = ((maxY - minY) / 2) * (1 + MARGIN);\nconst midX = (minX + maxX) / 2, midY = (minY + maxY) / 2;\n// This camera angle projects the cube into a roughly square bounding box — a square\n// canvas (vs. the 16:9 default) keeps the surface undistorted and fills the frame.\nconst TARGET_ASPECT = 1.0;\nif (halfX / halfY < TARGET_ASPECT) halfX = halfY * TARGET_ASPECT; else halfY = halfX / TARGET_ASPECT;\n\n// --- Mount --------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Plugin: floor contour map, depth-sorted surface + isolines, axis box, colorbar --\nconst contour3dPlugin = {\n  id: \"contour3d\",\n  beforeDatasetsDraw(chart) {\n    const { ctx, scales: { x, y } } = chart;\n    const toPx = (X, Y) => [x.getPixelForValue(X), y.getPixelForValue(Y)];\n    const fillQuad = (pts, color, alpha) => {\n      ctx.globalAlpha = alpha;\n      ctx.fillStyle = color;\n      ctx.beginPath();\n      pts.forEach((p, k) => { const [px, py] = toPx(p.x, p.y); k === 0 ? ctx.moveTo(px, py) : ctx.lineTo(px, py); });\n      ctx.closePath();\n      ctx.fill();\n    };\n\n    // Floor: flattened contour-band map, muted, as a spatial reference (spec note).\n    ctx.save();\n    floorQuads.forEach((q) => fillQuad(q.pts, q.color, 0.45));\n    ctx.globalAlpha = 0.4;\n    ctx.strokeStyle = INK;\n    ctx.lineWidth = 1;\n    floorLines.forEach((s) => {\n      const [ax, ay] = toPx(s.p1.x, s.p1.y), [bx, by] = toPx(s.p2.x, s.p2.y);\n      ctx.beginPath(); ctx.moveTo(ax, ay); ctx.lineTo(bx, by); ctx.stroke();\n    });\n    ctx.restore();\n\n    // Surface: depth-sorted contour bands + on-surface isolines, back-to-front.\n    ctx.save();\n    ctx.lineCap = \"round\";\n    ctx.lineJoin = \"round\";\n    for (const it of drawItems) {\n      if (it.kind === \"quad\") {\n        fillQuad(it.pts, it.color, 1);\n      } else {\n        ctx.globalAlpha = 0.55;\n        ctx.strokeStyle = INK;\n        ctx.lineWidth = it.width;\n        const [ax, ay] = toPx(it.p1.x, it.p1.y), [bx, by] = toPx(it.p2.x, it.p2.y);\n        ctx.beginPath(); ctx.moveTo(ax, ay); ctx.lineTo(bx, by); ctx.stroke();\n      }\n    }\n    ctx.restore();\n\n    // Axis box edges + ticks + labels (always on top of the mesh).\n    ctx.save();\n    ctx.globalAlpha = 1;\n    ctx.strokeStyle = INK_SOFT;\n    ctx.fillStyle = INK_SOFT;\n    ctx.font = \"600 13px -apple-system, Segoe UI, Roboto, sans-serif\";\n    ctx.textAlign = \"center\";\n    ctx.textBaseline = \"middle\";\n    const originPx = toPx(0, 0);\n\n    for (const edge of axisEdges) {\n      const pA = project(...edge.from), pB = project(...edge.to);\n      const [ax, ay] = toPx(pA.x, pA.y), [bx, by] = toPx(pB.x, pB.y);\n      ctx.lineWidth = 2;\n      ctx.beginPath(); ctx.moveTo(ax, ay); ctx.lineTo(bx, by); ctx.stroke();\n\n      const dx = bx - ax, dy = by - ay;\n      const len = Math.hypot(dx, dy) || 1;\n      let perpX = -dy / len, perpY = dx / len;\n      const midx = (ax + bx) / 2, midy = (ay + by) / 2;\n      if (perpX * (midx - originPx[0]) + perpY * (midy - originPx[1]) < 0) { perpX = -perpX; perpY = -perpY; }\n\n      const tickSpan = edge.ticks[edge.ticks.length - 1] - edge.ticks[0];\n      for (let k = 0; k < edge.ticks.length; k++) {\n        const f = (edge.ticks[k] - edge.ticks[0]) / tickSpan;\n        const px3 = edge.from[0] + (edge.to[0] - edge.from[0]) * f;\n        const py3 = edge.from[1] + (edge.to[1] - edge.from[1]) * f;\n        const pz3 = edge.from[2] + (edge.to[2] - edge.from[2]) * f;\n        const pt = project(px3, py3, pz3);\n        const [tx, ty] = toPx(pt.x, pt.y);\n        ctx.lineWidth = 1.4;\n        ctx.beginPath(); ctx.moveTo(tx, ty); ctx.lineTo(tx + perpX * 9, ty + perpY * 9); ctx.stroke();\n        ctx.fillText(edge.fmt(edge.ticks[k]), tx + perpX * 26, ty + perpY * 26);\n      }\n\n      ctx.save();\n      ctx.font = \"700 15px -apple-system, Segoe UI, Roboto, sans-serif\";\n      ctx.fillStyle = INK;\n      ctx.fillText(edge.label, bx + perpX * 44, by + perpY * 44);\n      ctx.restore();\n    }\n    ctx.restore();\n  },\n\n  afterDatasetsDraw(chart) {\n    const { ctx, chartArea } = chart;\n    ctx.save();\n    const keyX = chartArea.left + 24;\n    const keyY = chartArea.bottom - 40;\n    const keyW = 190, keyH = 14;\n    const grad = ctx.createLinearGradient(keyX, 0, keyX + keyW, 0);\n    grad.addColorStop(0, t.seq[0]);\n    grad.addColorStop(1, t.seq[1]);\n    ctx.fillStyle = grad;\n    ctx.fillRect(keyX, keyY, keyW, keyH);\n    ctx.strokeStyle = INK_SOFT;\n    ctx.lineWidth = 1;\n    ctx.strokeRect(keyX, keyY, keyW, keyH);\n\n    ctx.font = \"600 13px -apple-system, Segoe UI, Roboto, sans-serif\";\n    ctx.fillStyle = INK_SOFT;\n    ctx.textBaseline = \"bottom\";\n    ctx.textAlign = \"left\";\n    ctx.fillText(\"f(x, y)\", keyX, keyY - 6);\n    ctx.textBaseline = \"top\";\n    ctx.textAlign = \"left\";\n    ctx.fillText(zMin.toFixed(2), keyX, keyY + keyH + 4);\n    ctx.textAlign = \"center\";\n    ctx.fillText(((zMin + zMax) / 2).toFixed(2), keyX + keyW / 2, keyY + keyH + 4);\n    ctx.textAlign = \"right\";\n    ctx.fillText(zMax.toFixed(2), keyX + keyW, keyY + keyH + 4);\n    ctx.restore();\n  },\n};\n\n// --- Chart --------------------------------------------------------------\nnew Chart(canvas, {\n  type: \"scatter\",\n  data: { datasets: [{ data: [], showLine: false, pointRadius: 0 }] },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    layout: { padding: 16 },\n    plugins: {\n      title: {\n        display: true,\n        text: \"contour-3d · javascript · chartjs · anyplot.ai\",\n        color: INK,\n        font: { size: 22, weight: \"600\" },\n        padding: { top: 4, bottom: 14 },\n      },\n      legend: { display: false },\n      tooltip: { enabled: false },\n    },\n    scales: {\n      x: { type: \"linear\", min: midX - halfX, max: midX + halfX, display: false },\n      y: { type: \"linear\", min: midY - halfY, max: midY + halfY, display: false },\n    },\n  },\n  plugins: [contour3dPlugin],\n});\n"}