Basic Contour Plot — Apache ECharts

A contour plot displays isolines (level curves) of a 2D scalar field, connecting points of equal value across a surface. It transforms 3D data into an intuitive 2D representation, making it easy to identify regions of high and low values, gradients, and patterns. Contour plots are essential for visualizing continuous surfaces where the relationship between X, Y coordinates and a Z value needs to be understood.

Basic Contour Plot rendered with Apache ECharts

Renders

JavaScript source (Apache ECharts)

// anyplot.ai
// contour-basic: Basic Contour Plot
// Library: echarts 5.5.1 | JavaScript 22.23.0
// Quality: 84/100 | Created: 2026-06-25
//# anyplot-orientation: square

const t = window.ANYPLOT_TOKENS;

// Grid: 61 points from -3.0 to 3.0 (step=0.1)
const N = 61;
const xVals = [], yVals = [];
for (let i = 0; i < N; i++) {
  const v = parseFloat((-3 + i * 0.1).toFixed(1));
  xVals.push(v);
  yVals.push(v);
}

// Bivariate Gaussian: two distinct peaks
function density(x, y) {
  const g1 = Math.exp(-((x - 1.3) ** 2 + (y - 1.0) ** 2) / (2 * 0.36));
  const g2 = 0.85 * Math.exp(-((x + 1.0) ** 2 + (y + 1.0) ** 2) / (2 * 0.64));
  return g1 + g2;
}

// Build flat grid array and heatmap data
const gridZ = new Array(N * N);
let minVal = Infinity, maxVal = -Infinity;
const gridData = [];
for (let yi = 0; yi < N; yi++) {
  for (let xi = 0; xi < N; xi++) {
    const z = density(xVals[xi], yVals[yi]);
    gridZ[yi * N + xi] = z;
    gridData.push([xi, yi, z]);
    if (z < minVal) minVal = z;
    if (z > maxVal) maxVal = z;
  }
}

const xLabels = xVals.map(v => v.toFixed(1));
const yLabels = yVals.map(v => v.toFixed(1));

// Marching squares: compute isoline segments for one contour level
function contourSegments(threshold) {
  const segs = [];
  for (let yi = 0; yi < N - 1; yi++) {
    for (let xi = 0; xi < N - 1; xi++) {
      const vbl = gridZ[yi * N + xi];
      const vbr = gridZ[yi * N + (xi + 1)];
      const vtr = gridZ[(yi + 1) * N + (xi + 1)];
      const vtl = gridZ[(yi + 1) * N + xi];

      const abl = vbl >= threshold;
      const abr = vbr >= threshold;
      const atr = vtr >= threshold;
      const atl = vtl >= threshold;

      if (abl === abr && abr === atr && atr === atl) continue;

      // Linear interpolation of crossing position along an edge
      const lerp = (a, b, va, vb) => a + (b - a) * (threshold - va) / (vb - va);

      // Edge crossings in order: bottom, right, top, left
      const pts = [];
      if (abl !== abr) pts.push([lerp(xi, xi + 1, vbl, vbr), yi]);
      if (abr !== atr) pts.push([xi + 1, lerp(yi, yi + 1, vbr, vtr)]);
      if (atr !== atl) pts.push([lerp(xi, xi + 1, vtl, vtr), yi + 1]);
      if (atl !== abl) pts.push([xi, lerp(yi, yi + 1, vbl, vtl)]);

      if (pts.length === 2) {
        segs.push([pts[0], pts[1]]);
      } else if (pts.length === 4) {
        // Saddle point: disambiguate via cell-center value
        // code5: bl+tr above; code10: br+tl above — require opposite pairing
        const center = (vbl + vbr + vtr + vtl) / 4;
        const code5 = abl && atr && !abr && !atl;
        const swapped = code5 ? center < threshold : center >= threshold;
        if (swapped) {
          segs.push([pts[0], pts[3]]); // b↔l
          segs.push([pts[1], pts[2]]); // r↔t
        } else {
          segs.push([pts[0], pts[1]]); // b↔r
          segs.push([pts[2], pts[3]]); // t↔l
        }
      }
    }
  }
  return segs;
}

// 8 contour levels evenly spaced between min and max
const N_LEVELS = 8;
const levels = [];
for (let i = 1; i <= N_LEVELS; i++) {
  levels.push(minVal + i * (maxVal - minVal) / (N_LEVELS + 1));
}

// Build segment data for custom series: [x1, y1, x2, y2, labelStr]
// One labeled segment per level — the one closest to the center of the grid
const segData = [];
const midN = (N - 1) / 2;

levels.forEach(threshold => {
  const segs = contourSegments(threshold);
  // Find segment nearest to center (avoid edges for readability)
  let bestI = 0, bestDist = Infinity;
  segs.forEach((seg, si) => {
    const mx = (seg[0][0] + seg[1][0]) / 2;
    const my = (seg[0][1] + seg[1][1]) / 2;
    if (mx < 6 || mx > N - 6 || my < 6 || my > N - 6) return;
    const d = (mx - midN) ** 2 + (my - midN) ** 2;
    if (d < bestDist) { bestDist = d; bestI = si; }
  });

  const lbl = threshold.toFixed(2);
  segs.forEach((seg, si) => {
    segData.push([seg[0][0], seg[0][1], seg[1][0], seg[1][1], si === bestI ? lbl : '']);
  });
});

const chart = echarts.init(document.getElementById('container'));

chart.setOption({
  animation: false,
  color: t.palette,
  backgroundColor: 'transparent',
  title: {
    text: 'contour-basic · javascript · echarts · anyplot.ai',
    left: 'center',
    top: 24,
    textStyle: { color: t.ink, fontSize: 24, fontWeight: 'bold' }
  },
  grid: { left: 100, right: 155, top: 90, bottom: 100 },
  xAxis: {
    type: 'category',
    data: xLabels,
    name: 'X',
    nameLocation: 'middle',
    nameGap: 45,
    nameTextStyle: { color: t.inkSoft, fontSize: 16 },
    axisLabel: { color: t.inkSoft, fontSize: 14, interval: 9 },
    axisLine: { lineStyle: { color: t.inkSoft } },
    axisTick: { show: false },
    splitLine: { show: false }
  },
  yAxis: {
    type: 'category',
    data: yLabels,
    name: 'Y',
    nameLocation: 'middle',
    nameGap: 55,
    nameTextStyle: { color: t.inkSoft, fontSize: 16 },
    axisLabel: { color: t.inkSoft, fontSize: 14, interval: 9 },
    axisLine: { lineStyle: { color: t.inkSoft } },
    axisTick: { show: false },
    splitLine: { show: false }
  },
  visualMap: {
    min: minVal,
    max: maxVal,
    seriesIndex: [0],
    calculable: false,
    orient: 'vertical',
    right: 25,
    top: 'center',
    itemWidth: 22,
    inRange: { color: t.seq },
    textStyle: { color: t.inkSoft, fontSize: 13 },
    text: ['High', 'Low'],
    formatter: v => v.toFixed(2)
  },
  series: [
    {
      type: 'heatmap',
      data: gridData,
      emphasis: { disabled: true }
    },
    {
      // Contour isolines via marching squares, drawn as a custom series
      type: 'custom',
      coordinateSystem: 'cartesian2d',
      renderItem(params, api) {
        const x1 = api.value(0), y1 = api.value(1);
        const x2 = api.value(2), y2 = api.value(3);
        const lbl = api.value(4);
        const p1 = api.coord([x1, y1]);
        const p2 = api.coord([x2, y2]);

        const lineEl = {
          type: 'line',
          shape: { x1: p1[0], y1: p1[1], x2: p2[0], y2: p2[1] },
          style: { stroke: t.ink, lineWidth: 1.5, opacity: 0.5 }
        };

        if (!lbl) return lineEl;

        // Label: small rect background + text for readability over heatmap
        const mx = (p1[0] + p2[0]) / 2;
        const my = (p1[1] + p2[1]) / 2;
        return {
          type: 'group',
          children: [
            lineEl,
            {
              type: 'rect',
              shape: { x: mx - 16, y: my - 17, width: 32, height: 14, r: 2 },
              style: { fill: t.pageBg, opacity: 0.78 }
            },
            {
              type: 'text',
              x: mx,
              y: my - 15,
              style: {
                text: lbl,
                fill: t.ink,
                fontSize: 10,
                fontFamily: 'sans-serif',
                textAlign: 'center',
                opacity: 0.9
              }
            }
          ]
        };
      },
      data: segData,
      encode: { x: 0, y: 1 },
      z: 10,
      silent: true
    }
  ]
});

Part of Basic Contour Plot on anyplot.ai.

Other implementations