{"spec_id":"contour-basic","library":"echarts","language":"javascript","code":"// anyplot.ai\n// contour-basic: Basic Contour Plot\n// Library: echarts 5.5.1 | JavaScript 22.23.0\n// Quality: 84/100 | Created: 2026-06-25\n//# anyplot-orientation: square\n\nconst t = window.ANYPLOT_TOKENS;\n\n// Grid: 61 points from -3.0 to 3.0 (step=0.1)\nconst N = 61;\nconst xVals = [], yVals = [];\nfor (let i = 0; i < N; i++) {\n  const v = parseFloat((-3 + i * 0.1).toFixed(1));\n  xVals.push(v);\n  yVals.push(v);\n}\n\n// Bivariate Gaussian: two distinct peaks\nfunction density(x, y) {\n  const g1 = Math.exp(-((x - 1.3) ** 2 + (y - 1.0) ** 2) / (2 * 0.36));\n  const g2 = 0.85 * Math.exp(-((x + 1.0) ** 2 + (y + 1.0) ** 2) / (2 * 0.64));\n  return g1 + g2;\n}\n\n// Build flat grid array and heatmap data\nconst gridZ = new Array(N * N);\nlet minVal = Infinity, maxVal = -Infinity;\nconst gridData = [];\nfor (let yi = 0; yi < N; yi++) {\n  for (let xi = 0; xi < N; xi++) {\n    const z = density(xVals[xi], yVals[yi]);\n    gridZ[yi * N + xi] = z;\n    gridData.push([xi, yi, z]);\n    if (z < minVal) minVal = z;\n    if (z > maxVal) maxVal = z;\n  }\n}\n\nconst xLabels = xVals.map(v => v.toFixed(1));\nconst yLabels = yVals.map(v => v.toFixed(1));\n\n// Marching squares: compute isoline segments for one contour level\nfunction contourSegments(threshold) {\n  const segs = [];\n  for (let yi = 0; yi < N - 1; yi++) {\n    for (let xi = 0; xi < N - 1; xi++) {\n      const vbl = gridZ[yi * N + xi];\n      const vbr = gridZ[yi * N + (xi + 1)];\n      const vtr = gridZ[(yi + 1) * N + (xi + 1)];\n      const vtl = gridZ[(yi + 1) * N + xi];\n\n      const abl = vbl >= threshold;\n      const abr = vbr >= threshold;\n      const atr = vtr >= threshold;\n      const atl = vtl >= threshold;\n\n      if (abl === abr && abr === atr && atr === atl) continue;\n\n      // Linear interpolation of crossing position along an edge\n      const lerp = (a, b, va, vb) => a + (b - a) * (threshold - va) / (vb - va);\n\n      // Edge crossings in order: bottom, right, top, left\n      const pts = [];\n      if (abl !== abr) pts.push([lerp(xi, xi + 1, vbl, vbr), yi]);\n      if (abr !== atr) pts.push([xi + 1, lerp(yi, yi + 1, vbr, vtr)]);\n      if (atr !== atl) pts.push([lerp(xi, xi + 1, vtl, vtr), yi + 1]);\n      if (atl !== abl) pts.push([xi, lerp(yi, yi + 1, vbl, vtl)]);\n\n      if (pts.length === 2) {\n        segs.push([pts[0], pts[1]]);\n      } else if (pts.length === 4) {\n        // Saddle point: disambiguate via cell-center value\n        // code5: bl+tr above; code10: br+tl above — require opposite pairing\n        const center = (vbl + vbr + vtr + vtl) / 4;\n        const code5 = abl && atr && !abr && !atl;\n        const swapped = code5 ? center < threshold : center >= threshold;\n        if (swapped) {\n          segs.push([pts[0], pts[3]]); // b↔l\n          segs.push([pts[1], pts[2]]); // r↔t\n        } else {\n          segs.push([pts[0], pts[1]]); // b↔r\n          segs.push([pts[2], pts[3]]); // t↔l\n        }\n      }\n    }\n  }\n  return segs;\n}\n\n// 8 contour levels evenly spaced between min and max\nconst N_LEVELS = 8;\nconst levels = [];\nfor (let i = 1; i <= N_LEVELS; i++) {\n  levels.push(minVal + i * (maxVal - minVal) / (N_LEVELS + 1));\n}\n\n// Build segment data for custom series: [x1, y1, x2, y2, labelStr]\n// One labeled segment per level — the one closest to the center of the grid\nconst segData = [];\nconst midN = (N - 1) / 2;\n\nlevels.forEach(threshold => {\n  const segs = contourSegments(threshold);\n  // Find segment nearest to center (avoid edges for readability)\n  let bestI = 0, bestDist = Infinity;\n  segs.forEach((seg, si) => {\n    const mx = (seg[0][0] + seg[1][0]) / 2;\n    const my = (seg[0][1] + seg[1][1]) / 2;\n    if (mx < 6 || mx > N - 6 || my < 6 || my > N - 6) return;\n    const d = (mx - midN) ** 2 + (my - midN) ** 2;\n    if (d < bestDist) { bestDist = d; bestI = si; }\n  });\n\n  const lbl = threshold.toFixed(2);\n  segs.forEach((seg, si) => {\n    segData.push([seg[0][0], seg[0][1], seg[1][0], seg[1][1], si === bestI ? lbl : '']);\n  });\n});\n\nconst chart = echarts.init(document.getElementById('container'));\n\nchart.setOption({\n  animation: false,\n  color: t.palette,\n  backgroundColor: 'transparent',\n  title: {\n    text: 'contour-basic · javascript · echarts · anyplot.ai',\n    left: 'center',\n    top: 24,\n    textStyle: { color: t.ink, fontSize: 24, fontWeight: 'bold' }\n  },\n  grid: { left: 100, right: 155, top: 90, bottom: 100 },\n  xAxis: {\n    type: 'category',\n    data: xLabels,\n    name: 'X',\n    nameLocation: 'middle',\n    nameGap: 45,\n    nameTextStyle: { color: t.inkSoft, fontSize: 16 },\n    axisLabel: { color: t.inkSoft, fontSize: 14, interval: 9 },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    axisTick: { show: false },\n    splitLine: { show: false }\n  },\n  yAxis: {\n    type: 'category',\n    data: yLabels,\n    name: 'Y',\n    nameLocation: 'middle',\n    nameGap: 55,\n    nameTextStyle: { color: t.inkSoft, fontSize: 16 },\n    axisLabel: { color: t.inkSoft, fontSize: 14, interval: 9 },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    axisTick: { show: false },\n    splitLine: { show: false }\n  },\n  visualMap: {\n    min: minVal,\n    max: maxVal,\n    seriesIndex: [0],\n    calculable: false,\n    orient: 'vertical',\n    right: 25,\n    top: 'center',\n    itemWidth: 22,\n    inRange: { color: t.seq },\n    textStyle: { color: t.inkSoft, fontSize: 13 },\n    text: ['High', 'Low'],\n    formatter: v => v.toFixed(2)\n  },\n  series: [\n    {\n      type: 'heatmap',\n      data: gridData,\n      emphasis: { disabled: true }\n    },\n    {\n      // Contour isolines via marching squares, drawn as a custom series\n      type: 'custom',\n      coordinateSystem: 'cartesian2d',\n      renderItem(params, api) {\n        const x1 = api.value(0), y1 = api.value(1);\n        const x2 = api.value(2), y2 = api.value(3);\n        const lbl = api.value(4);\n        const p1 = api.coord([x1, y1]);\n        const p2 = api.coord([x2, y2]);\n\n        const lineEl = {\n          type: 'line',\n          shape: { x1: p1[0], y1: p1[1], x2: p2[0], y2: p2[1] },\n          style: { stroke: t.ink, lineWidth: 1.5, opacity: 0.5 }\n        };\n\n        if (!lbl) return lineEl;\n\n        // Label: small rect background + text for readability over heatmap\n        const mx = (p1[0] + p2[0]) / 2;\n        const my = (p1[1] + p2[1]) / 2;\n        return {\n          type: 'group',\n          children: [\n            lineEl,\n            {\n              type: 'rect',\n              shape: { x: mx - 16, y: my - 17, width: 32, height: 14, r: 2 },\n              style: { fill: t.pageBg, opacity: 0.78 }\n            },\n            {\n              type: 'text',\n              x: mx,\n              y: my - 15,\n              style: {\n                text: lbl,\n                fill: t.ink,\n                fontSize: 10,\n                fontFamily: 'sans-serif',\n                textAlign: 'center',\n                opacity: 0.9\n              }\n            }\n          ]\n        };\n      },\n      data: segData,\n      encode: { x: 0, y: 1 },\n      z: 10,\n      silent: true\n    }\n  ]\n});\n"}