Density Contour Plot — Highcharts

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 Highcharts

Renders

JavaScript source (Highcharts)

// anyplot.ai
// contour-density: Density Contour Plot
// Library: highcharts 12.6.0 | JavaScript 22.23.2
// Quality: 90/100 | Created: 2026-09-04
//# anyplot-orientation: landscape

const THEME = window.ANYPLOT_THEME;
const t = window.ANYPLOT_TOKENS;
const INK_MUTED = THEME === "light" ? "#6B6A63" : "#A8A79F";

// --- Deterministic RNG (LCG) + Box-Muller normal deviates -------------------
function makeRng(seed) {
  let state = seed >>> 0;
  return function () {
    state = (state * 1664525 + 1013904223) >>> 0;
    return state / 4294967296;
  };
}
function randomNormal(rng) {
  const u1 = Math.max(rng(), 1e-12);
  const u2 = rng();
  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
}
function generateCluster(rng, n, meanX, meanY, sdX, sdY, rho) {
  const points = [];
  for (let k = 0; k < n; k++) {
    const z1 = randomNormal(rng);
    const z2 = randomNormal(rng);
    points.push({
      x: meanX + sdX * z1,
      y: meanY + sdY * (rho * z1 + Math.sqrt(1 - rho * rho) * z2),
    });
  }
  return points;
}

// --- Data: machined-part QC measurements, main batch + a tool-wear drift ---
const rng = makeRng(42);
const mainBatch = generateCluster(rng, 550, 25.0, 48.0, 0.14, 0.55, 0.55);
const driftBatch = generateCluster(rng, 150, 25.55, 49.1, 0.12, 0.45, 0.45);
const parts = mainBatch.concat(driftBatch);

// --- 2D kernel density estimate on a grid -----------------------------------
function std(values) {
  const mean = values.reduce((a, b) => a + b, 0) / values.length;
  const variance = values.reduce((a, b) => a + (b - mean) ** 2, 0) / values.length;
  return Math.sqrt(variance);
}
function computeDensityGrid(pts, gx, gy, bwX, bwY) {
  const ny = gy.length;
  const nx = gx.length;
  const grid = Array.from({ length: ny }, () => new Array(nx).fill(0));
  pts.forEach((p) => {
    for (let j = 0; j < ny; j++) {
      const dy = (gy[j] - p.y) / bwY;
      const wy = Math.exp(-0.5 * dy * dy);
      if (wy < 1e-6) continue;
      const row = grid[j];
      for (let i = 0; i < nx; i++) {
        const dx = (gx[i] - p.x) / bwX;
        row[i] += wy * Math.exp(-0.5 * dx * dx);
      }
    }
  });
  return grid;
}

const diameters = parts.map((p) => p.x);
const weights = parts.map((p) => p.y);
const n = parts.length;
const bwX = std(diameters) * Math.pow(n, -1 / 6);
const bwY = std(weights) * Math.pow(n, -1 / 6);
const xMin = Math.min(...diameters) - 3 * bwX;
const xMax = Math.max(...diameters) + 3 * bwX;
const yMin = Math.min(...weights) - 3 * bwY;
const yMax = Math.max(...weights) + 3 * bwY;

const GRID_N = 70;
const gridX = Array.from({ length: GRID_N }, (_, i) => xMin + (i * (xMax - xMin)) / (GRID_N - 1));
const gridY = Array.from({ length: GRID_N }, (_, j) => yMin + (j * (yMax - yMin)) / (GRID_N - 1));
const densityGrid = computeDensityGrid(parts, gridX, gridY, bwX, bwY);

let maxDensity = 0;
densityGrid.forEach((row) => row.forEach((v) => { if (v > maxDensity) maxDensity = v; }));

// --- Marching squares: extract iso-density contour lines from the grid -----
function buildPaths(segments) {
  const pointSegs = new Map();
  segments.forEach((seg) => {
    seg.forEach((p) => {
      if (!pointSegs.has(p)) pointSegs.set(p, []);
      pointSegs.get(p).push(seg);
    });
  });
  const visited = new Set();
  const paths = [];
  segments.forEach((seg) => {
    if (visited.has(seg)) return;
    visited.add(seg);
    const path = [seg[0], seg[1]];
    let extended = true;
    while (extended) {
      extended = false;
      const last = path[path.length - 1];
      const next = pointSegs.get(last).find((s) => !visited.has(s));
      if (next) {
        visited.add(next);
        const nextPoint = next[0] === last ? next[1] : next[0];
        path.push(nextPoint);
        extended = nextPoint !== path[0];
      }
    }
    extended = true;
    while (extended) {
      extended = false;
      const first = path[0];
      const next = pointSegs.get(first).find((s) => !visited.has(s));
      if (next) {
        visited.add(next);
        path.unshift(next[0] === first ? next[1] : next[0]);
        extended = true;
      }
    }
    paths.push(path);
  });
  return paths;
}

function marchingSquaresPaths(values, gx, gy, level) {
  const ny = values.length;
  const nx = values[0].length;

  // Shared edge crossings, computed once so neighboring cells reference the
  // same point object (exact chaining without floating-point epsilon compares).
  const hCross = [];
  for (let j = 0; j < ny; j++) {
    const row = [];
    for (let i = 0; i < nx - 1; i++) {
      const v0 = values[j][i];
      const v1 = values[j][i + 1];
      if ((v0 >= level) !== (v1 >= level)) {
        const frac = (level - v0) / (v1 - v0);
        row.push({ x: gx[i] + frac * (gx[i + 1] - gx[i]), y: gy[j] });
      } else {
        row.push(null);
      }
    }
    hCross.push(row);
  }
  const vCross = [];
  for (let i = 0; i < nx; i++) {
    const col = [];
    for (let j = 0; j < ny - 1; j++) {
      const v0 = values[j][i];
      const v1 = values[j + 1][i];
      if ((v0 >= level) !== (v1 >= level)) {
        const frac = (level - v0) / (v1 - v0);
        col.push({ x: gx[i], y: gy[j] + frac * (gy[j + 1] - gy[j]) });
      } else {
        col.push(null);
      }
    }
    vCross.push(col);
  }

  const segments = [];
  for (let j = 0; j < ny - 1; j++) {
    for (let i = 0; i < nx - 1; i++) {
      const v00 = values[j][i];
      const v10 = values[j][i + 1];
      const v11 = values[j + 1][i + 1];
      const v01 = values[j + 1][i];
      const c00 = v00 >= level;
      const c10 = v10 >= level;
      const c11 = v11 >= level;
      const c01 = v01 >= level;
      const caseIndex = (c00 ? 1 : 0) | (c10 ? 2 : 0) | (c11 ? 4 : 0) | (c01 ? 8 : 0);
      if (caseIndex === 0 || caseIndex === 15) continue;
      const edgeB = hCross[j][i];
      const edgeT = hCross[j + 1][i];
      const edgeL = vCross[i][j];
      const edgeR = vCross[i + 1][j];
      const center = (v00 + v10 + v11 + v01) / 4;
      let pairs;
      if (caseIndex === 5) pairs = center >= level ? [[edgeL, edgeT], [edgeB, edgeR]] : [[edgeL, edgeB], [edgeT, edgeR]];
      else if (caseIndex === 10) pairs = center >= level ? [[edgeL, edgeB], [edgeT, edgeR]] : [[edgeL, edgeT], [edgeB, edgeR]];
      else if (caseIndex === 1 || caseIndex === 14) pairs = [[edgeL, edgeB]];
      else if (caseIndex === 2 || caseIndex === 13) pairs = [[edgeB, edgeR]];
      else if (caseIndex === 3 || caseIndex === 12) pairs = [[edgeL, edgeR]];
      else if (caseIndex === 4 || caseIndex === 11) pairs = [[edgeR, edgeT]];
      else if (caseIndex === 6 || caseIndex === 9) pairs = [[edgeB, edgeT]];
      else pairs = [[edgeL, edgeT]]; // caseIndex 7 or 8
      pairs.forEach((pair) => segments.push(pair));
    }
  }
  return buildPaths(segments);
}

function pathsToSeriesData(paths) {
  // A bare `null` array element would get an auto-incremented index x (0, 1, 2…)
  // from Highcharts' tuple parser, corrupting the xAxis extremes. Give the gap
  // an explicit, in-range x instead.
  const data = [];
  paths.forEach((path, idx) => {
    if (idx > 0) data.push({ x: path[0].x, y: null });
    path.forEach((p) => data.push([p.x, p.y]));
  });
  return data;
}

// --- Sequential Imprint colormap for the density levels ---------------------
function hexToRgb(hex) {
  const value = parseInt(hex.slice(1), 16);
  return [(value >> 16) & 255, (value >> 8) & 255, value & 255];
}
function lerpColor(hex1, hex2, frac) {
  const a = hexToRgb(hex1);
  const b = hexToRgb(hex2);
  const r = Math.round(a[0] + (b[0] - a[0]) * frac);
  const g = Math.round(a[1] + (b[1] - a[1]) * frac);
  const bl = Math.round(a[2] + (b[2] - a[2]) * frac);
  return `rgb(${r}, ${g}, ${bl})`;
}

const levelFractions = [0.12, 0.28, 0.46, 0.64, 0.82];
const contourSeries = levelFractions.map((frac, idx) => {
  const paths = marchingSquaresPaths(densityGrid, gridX, gridY, frac * maxDensity);
  return {
    type: "spline",
    name: `Density ${Math.round(frac * 100)}%`,
    color: lerpColor(t.seq[0], t.seq[1], idx / (levelFractions.length - 1)),
    data: pathsToSeriesData(paths),
    lineWidth: 2.5,
    marker: { enabled: false },
    enableMouseTracking: false,
    showInLegend: true,
  };
});

const scatterRgb = hexToRgb(INK_MUTED);
const scatterSeries = {
  type: "scatter",
  name: "Measured parts",
  color: `rgba(${scatterRgb[0]}, ${scatterRgb[1]}, ${scatterRgb[2]}, 0.35)`,
  data: parts.map((p) => [p.x, p.y]),
  marker: { radius: 2.5, symbol: "circle", lineWidth: 0 },
  showInLegend: false,
  tooltip: { pointFormat: "Diameter: {point.x:.2f} mm<br/>Weight: {point.y:.2f} g" },
};

// --- Chart -------------------------------------------------------------------
const title = "Process QC · contour-density · javascript · highcharts · anyplot.ai";
const titleFontSize = Math.round(22 * Math.min(1, 67 / title.length));

Highcharts.chart("container", {
  chart: {
    type: "spline",
    backgroundColor: "transparent",
    animation: false,
    style: { fontFamily: "inherit" },
  },
  credits: { enabled: false },
  title: {
    text: title,
    style: { color: t.ink, fontSize: `${titleFontSize}px`, fontWeight: "600" },
  },
  xAxis: {
    title: { text: "Part Diameter (mm)", style: { color: t.inkSoft, fontSize: "16px" } },
    lineColor: t.inkSoft,
    tickColor: t.inkSoft,
    gridLineColor: t.grid,
    labels: { style: { color: t.inkSoft, fontSize: "14px" } },
  },
  yAxis: {
    title: { text: "Part Weight (g)", style: { color: t.inkSoft, fontSize: "16px" } },
    gridLineColor: t.grid,
    labels: { style: { color: t.inkSoft, fontSize: "14px" } },
  },
  legend: {
    itemStyle: { color: t.inkSoft, fontSize: "14px" },
    itemHoverStyle: { color: t.ink },
  },
  plotOptions: {
    series: { animation: false },
  },
  series: [scatterSeries, ...contourSeries],
});

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/contour-density/highcharts/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": "highcharts",
  "page": "https://anyplot.ai/contour-density/javascript/highcharts",
  "hub": "https://anyplot.ai/contour-density",
  "code_json": "https://api.anyplot.ai/specs/contour-density/highcharts/code",
  "spec_json": "https://api.anyplot.ai/specs/contour-density",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/contour-density/javascript/highcharts/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/contour-density/javascript/highcharts/plot-dark.png",
  "interactive_light_html": "https://storage.googleapis.com/anyplot-images/plots/contour-density/javascript/highcharts/plot-light.html",
  "interactive_dark_html": "https://storage.googleapis.com/anyplot-images/plots/contour-density/javascript/highcharts/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