{"spec_id":"heatmap-mandelbrot","library":"echarts","language":"javascript","code":"// anyplot.ai\n// heatmap-mandelbrot: Mandelbrot Set Fractal Visualization\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 82/100 | Created: 2026-08-25\n\n//# anyplot-orientation: landscape\nconst t = window.ANYPLOT_TOKENS;\nconst size = window.ANYPLOT_SIZE || { width: 1600, height: 900 };\n\n// --- Complex-plane window & escape parameters -------------------------------\nconst X_MIN = -2.5;\nconst X_MAX = 1.0;\nconst Y_MIN = -1.25;\nconst Y_MAX = 1.25;\nconst MAX_ITER = 100;\nconst DATA_ASPECT = (X_MAX - X_MIN) / (Y_MAX - Y_MIN);\n\n// --- Raster resolution (>= spec's 800x600 minimum, matches the plane's aspect) --\nconst GRID_H = 800;\nconst GRID_W = Math.round(GRID_H * DATA_ASPECT);\n\n// --- Colors: Imprint sequential gradient for escaped points, a fixed solid --\n// --- color for the bounded interior (a data color, so it must stay identical\n// --- across themes rather than following the theme-adaptive ink token) ------\nfunction hexToRgb(hex) {\n  const n = parseInt(hex.slice(1), 16);\n  return [(n >> 16) & 255, (n >> 8) & 255, n & 255];\n}\nconst seqStart = hexToRgb(t.seq[0]);\nconst seqEnd = hexToRgb(t.seq[1]);\nconst insideRgb = hexToRgb(\"#1A1A17\");\n\n// z(n+1) = z(n)^2 + c, smooth-colored via the normalized (fractional) escape\n// count so bands don't appear at integer iteration boundaries.\nfunction escapeColor(cx, cy) {\n  let zx = 0;\n  let zy = 0;\n  let n = 0;\n  while (zx * zx + zy * zy <= 4 && n < MAX_ITER) {\n    const zxNext = zx * zx - zy * zy + cx;\n    zy = 2 * zx * zy + cy;\n    zx = zxNext;\n    n++;\n  }\n  if (n === MAX_ITER) return insideRgb;\n  const logZn = Math.log(zx * zx + zy * zy) / 2;\n  const smoothN = n + 1 - Math.log(logZn / Math.LN2) / Math.LN2;\n  // Gentle gamma so mid-range iteration counts don't crowd the dark end.\n  const frac = Math.pow(Math.max(0, Math.min(1, smoothN / MAX_ITER)), 0.42);\n  return [\n    Math.round(seqStart[0] + (seqEnd[0] - seqStart[0]) * frac),\n    Math.round(seqStart[1] + (seqEnd[1] - seqStart[1]) * frac),\n    Math.round(seqStart[2] + (seqEnd[2] - seqStart[2]) * frac),\n  ];\n}\n\n// --- Render the raster once into an offscreen canvas -------------------------\nconst raster = document.createElement(\"canvas\");\nraster.width = GRID_W;\nraster.height = GRID_H;\nconst rasterCtx = raster.getContext(\"2d\");\nconst imageData = rasterCtx.createImageData(GRID_W, GRID_H);\nfor (let row = 0; row < GRID_H; row++) {\n  const cy = Y_MAX - (row / (GRID_H - 1)) * (Y_MAX - Y_MIN);\n  for (let col = 0; col < GRID_W; col++) {\n    const cx = X_MIN + (col / (GRID_W - 1)) * (X_MAX - X_MIN);\n    const [r, g, b] = escapeColor(cx, cy);\n    const idx = (row * GRID_W + col) * 4;\n    imageData.data[idx] = r;\n    imageData.data[idx + 1] = g;\n    imageData.data[idx + 2] = b;\n    imageData.data[idx + 3] = 255;\n  }\n}\nrasterCtx.putImageData(imageData, 0, 0);\nconst fractalImage = raster.toDataURL(\"image/png\");\n\n// --- Fit the plane's true aspect ratio inside the mount (letterboxed) --------\nconst MARGIN = { top: 130, bottom: 100, left: 120, right: 50 };\nconst availW = size.width - MARGIN.left - MARGIN.right;\nconst availH = size.height - MARGIN.top - MARGIN.bottom;\nlet plotW = availW;\nlet plotH = plotW / DATA_ASPECT;\nif (plotH > availH) {\n  plotH = availH;\n  plotW = plotH * DATA_ASPECT;\n}\nconst padW = (availW - plotW) / 2;\nconst padH = (availH - plotH) / 2;\nconst gridLeft = MARGIN.left + padW;\nconst gridRight = MARGIN.right + padW;\nconst gridTop = MARGIN.top + padH;\nconst gridBottom = MARGIN.bottom + padH;\n\n// --- Init ---------------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\n// --- Option ---------------------------------------------------------------------\nchart.setOption({\n  animation: false,\n  backgroundColor: \"transparent\",\n  title: {\n    text: \"heatmap-mandelbrot · javascript · echarts · anyplot.ai\",\n    subtext: \"Color: smooth escape-iteration count · Solid fill: bounded orbit (never escapes)\",\n    left: \"center\",\n    top: 18,\n    textStyle: { color: t.ink, fontSize: 22, fontWeight: 500 },\n    subtextStyle: { color: t.inkSoft, fontSize: 14 },\n  },\n  grid: { left: gridLeft, right: gridRight, top: gridTop, bottom: gridBottom },\n  xAxis: {\n    type: \"value\",\n    min: X_MIN,\n    max: X_MAX,\n    name: \"Real axis (Re)\",\n    nameLocation: \"middle\",\n    nameGap: 36,\n    nameTextStyle: { color: t.ink, fontSize: 16 },\n    axisLabel: { color: t.inkSoft, fontSize: 14, formatter: (v) => v.toFixed(2) },\n    axisLine: { onZero: false, lineStyle: { color: t.inkSoft } },\n    axisTick: { onZero: false, lineStyle: { color: t.inkSoft } },\n    splitLine: { show: false },\n  },\n  yAxis: {\n    type: \"value\",\n    min: Y_MIN,\n    max: Y_MAX,\n    name: \"Imaginary axis (Im)\",\n    nameLocation: \"middle\",\n    nameGap: 46,\n    nameTextStyle: { color: t.ink, fontSize: 16 },\n    axisLabel: { color: t.inkSoft, fontSize: 14, formatter: (v) => v.toFixed(2) },\n    axisLine: { onZero: false, lineStyle: { color: t.inkSoft } },\n    axisTick: { onZero: false, lineStyle: { color: t.inkSoft } },\n    splitLine: { show: false },\n  },\n  series: [\n    {\n      type: \"custom\",\n      coordinateSystem: \"cartesian2d\",\n      data: [[X_MIN, Y_MIN, 0]],\n      renderItem: (_params, api) => {\n        const corner1 = api.coord([X_MIN, Y_MIN]);\n        const corner2 = api.coord([X_MAX, Y_MAX]);\n        return {\n          type: \"image\",\n          style: {\n            image: fractalImage,\n            x: Math.min(corner1[0], corner2[0]),\n            y: Math.min(corner1[1], corner2[1]),\n            width: Math.abs(corner2[0] - corner1[0]),\n            height: Math.abs(corner1[1] - corner2[1]),\n          },\n        };\n      },\n    },\n  ],\n});\nchart.on(\"finished\", () => {\n  window.__anyplotReady = true;\n});\n"}