Density Contour Plot — Apache ECharts

A density contour plot (also known as a 2D KDE contour plot) displays the concentration of points in a 2D scatter plot using contour lines. The contours connect points of equal density, revealing clusters, patterns, and the overall bivariate distribution shape.

Density Contour Plot rendered with Apache ECharts

Renders

JavaScript source (Apache ECharts)

// anyplot.ai
// contour-density: Density Contour Plot
// Library: echarts 6.1.0 | JavaScript 22.23.2
// Quality: 90/100 | Created: 2026-09-04

const t = window.ANYPLOT_TOKENS;

// --- Data: coffee-shop visits — arrival time vs. dwell time (fixed-seed LCG) ----
let seed = 42;
function rand() {
  seed = (seed * 1664525 + 1013904223) % 4294967296;
  return seed / 4294967296;
}
function gaussianPair() {
  const u1 = Math.max(rand(), 1e-9);
  const u2 = rand();
  const r = Math.sqrt(-2 * Math.log(u1));
  return [r * Math.cos(2 * Math.PI * u2), r * Math.sin(2 * Math.PI * u2)];
}
function cluster(n, cx, cy, sx, sy) {
  const pts = [];
  for (let i = 0; i < n; i++) {
    const [z0, z1] = gaussianPair();
    pts.push([cx + z0 * sx, cy + z1 * sy]);
  }
  return pts;
}

// Two visit patterns: brief morning coffee runs, and long afternoon work sessions
const morningRush = cluster(380, 8.1, 14, 0.9, 5.5);
const afternoonWork = cluster(270, 14.6, 55, 1.6, 12);
const points = morningRush.concat(afternoonWork);

// --- Bivariate Gaussian KDE on a regular grid --------------------------------
const xs = points.map((p) => p[0]);
const ys = points.map((p) => p[1]);
const xMin = Math.min(...xs);
const xMax = Math.max(...xs);
const yMin = Math.min(...ys);
const yMax = Math.max(...ys);
const padX = (xMax - xMin) * 0.12;
const padY = (yMax - yMin) * 0.12;
const gx0 = xMin - padX;
const gx1 = xMax + padX;
const gy0 = yMin - padY;
const gy1 = yMax + padY;

const GRID = 60;
const bwX = (gx1 - gx0) * 0.09;
const bwY = (gy1 - gy0) * 0.09;
const gridX = Array.from({ length: GRID }, (_, i) => gx0 + (i * (gx1 - gx0)) / (GRID - 1));
const gridY = Array.from({ length: GRID }, (_, j) => gy0 + (j * (gy1 - gy0)) / (GRID - 1));

const density = Array.from({ length: GRID }, () => new Array(GRID).fill(0));
for (const [px, py] of points) {
  for (let i = 0; i < GRID; i++) {
    const dx = (gridX[i] - px) / bwX;
    const gaussX = Math.exp(-0.5 * dx * dx);
    if (gaussX < 1e-6) continue;
    for (let j = 0; j < GRID; j++) {
      const dy = (gridY[j] - py) / bwY;
      density[i][j] += gaussX * Math.exp(-0.5 * dy * dy);
    }
  }
}
let maxDensity = 0;
for (let i = 0; i < GRID; i++) {
  for (let j = 0; j < GRID; j++) maxDensity = Math.max(maxDensity, density[i][j]);
}

// --- Marching-triangles contour extraction -----------------------------------
// Each grid cell is split into 2 triangles so saddle points resolve without
// the extra case table a full marching-squares implementation would need.
function edgeCrossing(pa, pb, level) {
  const frac = (level - pa.z) / (pb.z - pa.z);
  return [pa.x + frac * (pb.x - pa.x), pa.y + frac * (pb.y - pa.y)];
}
function triangleSegment(p1, p2, p3, level) {
  const crossings = [];
  for (const [pa, pb] of [
    [p1, p2],
    [p2, p3],
    [p3, p1],
  ]) {
    if ((pa.z - level) * (pb.z - level) < 0) crossings.push(edgeCrossing(pa, pb, level));
  }
  return crossings.length === 2 ? crossings : null;
}

const LEVEL_FRACTIONS = [0.12, 0.28, 0.44, 0.6, 0.76, 0.92];
const contourData = [];
for (const frac of LEVEL_FRACTIONS) {
  const level = frac * maxDensity;
  for (let i = 0; i < GRID - 1; i++) {
    for (let j = 0; j < GRID - 1; j++) {
      const p00 = { x: gridX[i], y: gridY[j], z: density[i][j] };
      const p10 = { x: gridX[i + 1], y: gridY[j], z: density[i + 1][j] };
      const p11 = { x: gridX[i + 1], y: gridY[j + 1], z: density[i + 1][j + 1] };
      const p01 = { x: gridX[i], y: gridY[j + 1], z: density[i][j + 1] };
      const seg1 = triangleSegment(p00, p10, p11, level);
      if (seg1) contourData.push({ coords: seg1, value: frac, lineStyle: { width: 1.5 + frac * 2 } });
      const seg2 = triangleSegment(p00, p11, p01, level);
      if (seg2) contourData.push({ coords: seg2, value: frac, lineStyle: { width: 1.5 + frac * 2 } });
    }
  }
}

// --- Init ---------------------------------------------------------------------
const chart = echarts.init(document.getElementById("container"));

chart.setOption({
  animation: false,
  backgroundColor: "transparent",
  color: t.palette,
  title: {
    text: "contour-density · javascript · echarts · anyplot.ai",
    left: "center",
    textStyle: { color: t.ink, fontSize: 22, fontWeight: 500 },
  },
  grid: { left: 110, right: 230, top: 110, bottom: 100, containLabel: true },
  xAxis: {
    type: "value",
    name: "Arrival Time (hour of day)",
    nameLocation: "middle",
    nameGap: 44,
    nameTextStyle: { color: t.ink, fontSize: 16 },
    scale: true,
    axisLabel: { color: t.inkSoft, fontSize: 14 },
    axisLine: { lineStyle: { color: t.inkSoft } },
    splitLine: { show: false },
  },
  yAxis: {
    type: "value",
    name: "Dwell Time (minutes)",
    nameLocation: "middle",
    nameGap: 56,
    nameTextStyle: { color: t.ink, fontSize: 16 },
    scale: true,
    axisLabel: { color: t.inkSoft, fontSize: 14 },
    axisLine: { lineStyle: { color: t.inkSoft } },
    splitLine: { show: false },
  },
  visualMap: {
    type: "continuous",
    seriesIndex: 1,
    min: LEVEL_FRACTIONS[0],
    max: LEVEL_FRACTIONS[LEVEL_FRACTIONS.length - 1],
    inRange: { color: t.seq },
    text: ["High density", "Low density"],
    textStyle: { color: t.inkSoft, fontSize: 13 },
    itemWidth: 16,
    itemHeight: 200,
    right: 30,
    top: "middle",
    orient: "vertical",
    hoverLink: false,
    calculable: false,
  },
  series: [
    {
      // Raw visit records — muted context underlay beneath the contours
      type: "scatter",
      data: points,
      symbolSize: 5,
      itemStyle: { color: t.muted, opacity: 0.35 },
      z: 1,
    },
    {
      // KDE contour lines, colored by density level via the visualMap above
      type: "lines",
      coordinateSystem: "cartesian2d",
      xAxisIndex: 0,
      yAxisIndex: 0,
      polyline: false,
      data: contourData,
      z: 2,
    },
  ],
});

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/contour-density/echarts/code. Any spec id and library id listed in llms-full.txt fit the same URL shape; every URL below is complete and callable.

{
  "spec_id": "contour-density",
  "language": "javascript",
  "library": "echarts",
  "page": "https://anyplot.ai/contour-density/javascript/echarts",
  "hub": "https://anyplot.ai/contour-density",
  "code_json": "https://api.anyplot.ai/specs/contour-density/echarts/code",
  "spec_json": "https://api.anyplot.ai/specs/contour-density",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/contour-density/javascript/echarts/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/contour-density/javascript/echarts/plot-dark.png",
  "interactive_light_html": "https://storage.googleapis.com/anyplot-images/plots/contour-density/javascript/echarts/plot-light.html",
  "interactive_dark_html": "https://storage.googleapis.com/anyplot-images/plots/contour-density/javascript/echarts/plot-dark.html",
  "quality_score": 90.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

Part of Density Contour Plot on anyplot.ai.

Other implementations