{"spec_id":"contour-basic","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// contour-basic: Basic Contour Plot\n// Library: chartjs 4.4.7 | JavaScript 22.23.0\n// Quality: 82/100 | Created: 2026-06-25\n\n//# anyplot-orientation: square\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data ---\nconst N = 70;\nconst RANGE = 3;\nconst NUM_LEVELS = 28;\n\n// Grid coordinates (symmetric grid — xArr === yArr but kept separate for clarity)\nconst xArr = [], yArr = [];\nfor (let k = 0; k < N; k++) {\n  xArr.push(-RANGE + (2 * RANGE * k) / (N - 1));\n  yArr.push(-RANGE + (2 * RANGE * k) / (N - 1));\n}\n\n// Peaks function on grid — builds zGrid and tracks Z_MIN/Z_MAX in one pass\nlet Z_MIN = Infinity, Z_MAX = -Infinity;\nconst zGrid = [];\nfor (let i = 0; i < N; i++) {\n  zGrid.push([]);\n  for (let j = 0; j < N; j++) {\n    const x = xArr[i], y = yArr[j];\n    const z =\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);\n    zGrid[i].push(z);\n    if (z < Z_MIN) Z_MIN = z;\n    if (z > Z_MAX) Z_MAX = z;\n  }\n}\n\nconst Z_ZERO_FRAC = (0 - Z_MIN) / (Z_MAX - Z_MIN);\nconst step = (Z_MAX - Z_MIN) / NUM_LEVELS;\n\n// --- Imprint diverging colormap (t.div tokens) ---\nfunction hexToRgb(hex) {\n  return [parseInt(hex.slice(1, 3), 16), parseInt(hex.slice(3, 5), 16), parseInt(hex.slice(5, 7), 16)];\n}\nfunction lerpColor(h1, h2, frac) {\n  const [r1, g1, b1] = hexToRgb(h1);\n  const [r2, g2, b2] = hexToRgb(h2);\n  return `rgb(${Math.round(r1 + (r2 - r1) * frac)},${Math.round(g1 + (g2 - g1) * frac)},${Math.round(b1 + (b2 - b1) * frac)})`;\n}\nfunction divColor(z) {\n  const frac = Math.max(0, Math.min(1, (z - Z_MIN) / (Z_MAX - Z_MIN)));\n  if (frac <= Z_ZERO_FRAC) return lerpColor(t.div[0], t.div[1], frac / Z_ZERO_FRAC);\n  return lerpColor(t.div[1], t.div[2], (frac - Z_ZERO_FRAC) / (1 - Z_ZERO_FRAC));\n}\n\n// --- Contour band datasets (filled regions) ---\nconst datasets = [];\nfor (let lvl = 0; lvl < NUM_LEVELS; lvl++) {\n  const zLow = Z_MIN + lvl * step;\n  const zHigh = zLow + step;\n  const color = divColor((zLow + zHigh) / 2);\n  const isLast = lvl === NUM_LEVELS - 1;\n  const points = [];\n  for (let i = 0; i < N; i++) {\n    for (let j = 0; j < N; j++) {\n      const z = zGrid[i][j];\n      if (z >= zLow && (isLast ? z <= zHigh : z < zHigh)) {\n        points.push({ x: xArr[i], y: yArr[j] });\n      }\n    }\n  }\n  if (points.length > 0) {\n    datasets.push({\n      data: points,\n      backgroundColor: color,\n      borderColor: color,\n      borderWidth: 0,\n      pointRadius: 10,\n      pointHoverRadius: 10,\n      showLine: false,\n    });\n  }\n}\n\n// --- Marching squares for contour isolines ---\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: all below\n  [[0, 3]],         // 1: BL\n  [[0, 1]],         // 2: BR\n  [[3, 1]],         // 3: BL,BR\n  [[1, 2]],         // 4: TR\n  [[0, 3], [1, 2]], // 5: BL,TR (saddle)\n  [[0, 2]],         // 6: BR,TR\n  [[3, 2]],         // 7: BL,BR,TR\n  [[3, 2]],         // 8: TL\n  [[0, 2]],         // 9: BL,TL\n  [[0, 1], [2, 3]], // 10: BR,TL (saddle)\n  [[1, 2]],         // 11: BL,BR,TL\n  [[3, 1]],         // 12: TR,TL\n  [[0, 1]],         // 13: BL,TR,TL\n  [[0, 3]],         // 14: BR,TR,TL\n  [],               // 15: all above\n];\n\n// Isoline thresholds — every other band boundary (14 lines) for clarity\nconst isoThresholds = [];\nfor (let lvl = 1; lvl < NUM_LEVELS; lvl += 2) {\n  isoThresholds.push(Z_MIN + lvl * step);\n}\n\n// --- Colorbar + isoline plugin ---\nconst colorbarPlugin = {\n  id: 'colorbar',\n  afterDraw(chart) {\n    const ctx = chart.ctx;\n    const ca = chart.chartArea;\n    if (!ca) return;\n\n    const xs = chart.scales.x;\n    const ys = chart.scales.y;\n\n    // Precompute pixel positions for all grid nodes (avoids repeated getPixelForValue calls)\n    const xPx = xArr.map((v) => xs.getPixelForValue(v));\n    const yPx = yArr.map((v) => ys.getPixelForValue(v));\n\n    // Returns the pixel [px, py] where the isoline crosses a given edge of cell (i,j)\n    function edgePx(e, i, j, z00, z10, z11, z01, thresh) {\n      const f = (a, b, za, zb) => a + ((thresh - za) / (zb - za)) * (b - a);\n      switch (e) {\n        case 0: return [f(xPx[i], xPx[i + 1], z00, z10), yPx[j]];\n        case 1: return [xPx[i + 1], f(yPx[j], yPx[j + 1], z10, z11)];\n        case 2: return [f(xPx[i], xPx[i + 1], z01, z11), yPx[j + 1]];\n        case 3: return [xPx[i], f(yPx[j], yPx[j + 1], z00, z01)];\n      }\n    }\n\n    // Draw isolines clipped to chart area\n    ctx.save();\n    ctx.beginPath();\n    ctx.rect(ca.left, ca.top, ca.right - ca.left, ca.bottom - ca.top);\n    ctx.clip();\n\n    ctx.beginPath();\n    ctx.strokeStyle = t.ink;\n    ctx.lineWidth = 0.8;\n    ctx.globalAlpha = 0.35;\n\n    for (const thresh of isoThresholds) {\n      for (let i = 0; i < N - 1; i++) {\n        for (let j = 0; j < N - 1; j++) {\n          const z00 = zGrid[i][j], z10 = zGrid[i + 1][j];\n          const z11 = zGrid[i + 1][j + 1], z01 = zGrid[i][j + 1];\n          const code =\n            (z00 >= thresh ? 1 : 0) |\n            (z10 >= thresh ? 2 : 0) |\n            (z11 >= thresh ? 4 : 0) |\n            (z01 >= thresh ? 8 : 0);\n          for (const [e0, e1] of SEG[code]) {\n            const [ax, ay] = edgePx(e0, i, j, z00, z10, z11, z01, thresh);\n            const [bx, by] = edgePx(e1, i, j, z00, z10, z11, z01, thresh);\n            ctx.moveTo(ax, ay);\n            ctx.lineTo(bx, by);\n          }\n        }\n      }\n    }\n    ctx.stroke();\n    ctx.restore();\n\n    // --- Colorbar ---\n    const barX = ca.right + 22;\n    const barW = 22;\n    const barH = ca.bottom - ca.top;\n\n    const grad = ctx.createLinearGradient(0, ca.bottom, 0, ca.top);\n    grad.addColorStop(0, t.div[0]);\n    grad.addColorStop(Z_ZERO_FRAC, t.div[1]);\n    grad.addColorStop(1, t.div[2]);\n    ctx.fillStyle = grad;\n    ctx.fillRect(barX, ca.top, barW, barH);\n\n    ctx.strokeStyle = t.inkSoft;\n    ctx.lineWidth = 1;\n    ctx.strokeRect(barX, ca.top, barW, barH);\n\n    // Zero-line marks the valley/peak boundary\n    const zeroBarY = ca.bottom - Z_ZERO_FRAC * barH;\n    ctx.strokeStyle = t.ink;\n    ctx.lineWidth = 1.5;\n    ctx.beginPath();\n    ctx.moveTo(barX, zeroBarY);\n    ctx.lineTo(barX + barW, zeroBarY);\n    ctx.stroke();\n\n    // \"z\" label\n    ctx.fillStyle = t.ink;\n    ctx.font = 'bold 15px sans-serif';\n    ctx.textAlign = 'center';\n    ctx.fillText('z', barX + barW / 2, ca.top - 6);\n\n    // Tick marks at max, 0, min\n    const ticks = [\n      { frac: 1, label: `+${Z_MAX.toFixed(1)}` },\n      { frac: Z_ZERO_FRAC, label: '0' },\n      { frac: 0, label: Z_MIN.toFixed(1) },\n    ];\n    ctx.strokeStyle = t.inkSoft;\n    ctx.lineWidth = 1;\n    ctx.fillStyle = t.inkSoft;\n    ctx.font = '15px sans-serif';\n    ctx.textAlign = 'left';\n    for (const tk of ticks) {\n      const ty = ca.bottom - tk.frac * barH;\n      ctx.beginPath();\n      ctx.moveTo(barX + barW, ty);\n      ctx.lineTo(barX + barW + 5, ty);\n      ctx.stroke();\n      ctx.fillText(tk.label, barX + barW + 8, ty + 5);\n    }\n  },\n};\n\n// --- Mount ---\nconst canvas = document.createElement('canvas');\ndocument.getElementById('container').appendChild(canvas);\n\n// --- Chart ---\nnew Chart(canvas, {\n  type: 'scatter',\n  data: { datasets },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    layout: { padding: { right: 100, bottom: 10 } },\n    plugins: {\n      title: {\n        display: true,\n        text: 'contour-basic · javascript · chartjs · anyplot.ai',\n        color: t.ink,\n        font: { size: 22 },\n        padding: { top: 12, bottom: 12 },\n      },\n      legend: { display: false },\n    },\n    scales: {\n      x: {\n        type: 'linear',\n        min: -RANGE,\n        max: RANGE,\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n        grid: { color: t.grid },\n        title: { display: true, text: 'X', color: t.ink, font: { size: 16 } },\n      },\n      y: {\n        type: 'linear',\n        min: -RANGE,\n        max: RANGE,\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n        grid: { color: t.grid },\n        title: { display: true, text: 'Y', color: t.ink, font: { size: 16 } },\n      },\n    },\n  },\n  plugins: [colorbarPlugin],\n});\n"}