Basic Density Plot — Apache ECharts

A density plot (also known as Kernel Density Estimation or KDE plot) visualizes the distribution of a continuous variable by smoothing the data into a continuous probability density curve. Unlike histograms which use discrete bins, density plots provide a smooth representation of the underlying distribution, making it easier to identify patterns such as skewness, modality, and overall shape.

Basic Density Plot rendered with Apache ECharts

Renders

JavaScript source (Apache ECharts)

// anyplot.ai
// density-basic: Basic Density Plot
// Library: echarts 6.1.0 | JavaScript 22.23.2
// Quality: 91/100 | Created: 2026-08-24

const t = window.ANYPLOT_TOKENS;

// --- Data: server response latency (ms), right-skewed --------------------
function mulberry32(seed) {
  return function () {
    seed |= 0;
    seed = (seed + 0x6d2b79f5) | 0;
    let x = Math.imul(seed ^ (seed >>> 15), 1 | seed);
    x = (x + Math.imul(x ^ (x >>> 7), 61 | x)) ^ x;
    return ((x ^ (x >>> 14)) >>> 0) / 4294967296;
  };
}
const rand = mulberry32(42);

function randNormal() {
  let u = 0;
  let v = 0;
  while (u === 0) u = rand();
  while (v === 0) v = rand();
  return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);
}

const sampleCount = 400;
const logMean = Math.log(120); // median latency ~120ms
const logStd = 0.35;
const latencies = [];
for (let i = 0; i < sampleCount; i++) {
  latencies.push(Math.exp(logMean + logStd * randNormal()));
}

// --- Gaussian KDE with Silverman's rule-of-thumb bandwidth ----------------
const mean = latencies.reduce((a, b) => a + b, 0) / sampleCount;
const variance =
  latencies.reduce((a, b) => a + (b - mean) ** 2, 0) / (sampleCount - 1);
const std = Math.sqrt(variance);
const bandwidth = 1.06 * std * Math.pow(sampleCount, -1 / 5);

function gaussianKernel(u) {
  return Math.exp(-0.5 * u * u) / Math.sqrt(2 * Math.PI);
}

const vmin = Math.min(...latencies);
const vmax = Math.max(...latencies);
const pad = bandwidth * 3;
const gridStart = Math.max(0, vmin - pad);
const gridEnd = vmax + pad;
const gridCount = 200;
const step = (gridEnd - gridStart) / (gridCount - 1);

const densityCurve = [];
let peakDensity = 0;
for (let i = 0; i < gridCount; i++) {
  const x = gridStart + i * step;
  let sum = 0;
  for (let j = 0; j < sampleCount; j++) {
    sum += gaussianKernel((x - latencies[j]) / bandwidth);
  }
  const density = sum / (sampleCount * bandwidth);
  peakDensity = Math.max(peakDensity, density);
  densityCurve.push([x, density]);
}

// Rug plot: individual observations as ticks below the curve
const rugY = -peakDensity * 0.06;
const rugData = latencies.map((v) => [v, rugY]);

// Round the x-axis to clean bounds instead of the raw KDE grid extent
const xMax = Math.ceil(gridEnd / 50) * 50;

// Detect the secondary shoulder in the tail: not a true local maximum (the
// curve keeps descending overall), but a flattening of the descent — a local
// maximum in the curve's slope. Look for the point in the tail (past the
// global peak) where the descent visibly slows before continuing downward.
let globalPeakIdx = 0;
for (let i = 1; i < densityCurve.length; i++) {
  if (densityCurve[i][1] > densityCurve[globalPeakIdx][1]) globalPeakIdx = i;
}
// Widened second derivative (concavity): a shoulder is the point of
// strongest local "bulge" (concave-up interruption of the otherwise
// concave-down decline). A window of several grid steps rides over the
// sample-noise wobble that a point-to-point derivative would chase.
const w = 8;
let bulgeStartIdx = -1;
let bestBulge = 0;
for (let i = globalPeakIdx + w + 2; i < densityCurve.length - w - 2; i++) {
  const bulge =
    densityCurve[i - w][1] - 2 * densityCurve[i][1] + densityCurve[i + w][1];
  if (bulge > bestBulge) {
    bestBulge = bulge;
    bulgeStartIdx = i;
  }
}
// The bulge marks where the descent starts to flatten; walk forward a bit
// further to land the label on the flattest part of the shelf itself.
let shoulderIdx = bulgeStartIdx;
if (bulgeStartIdx >= 0) {
  let flattest = Infinity;
  const scanEnd = Math.min(bulgeStartIdx + 30, densityCurve.length - 2);
  for (let i = bulgeStartIdx; i <= scanEnd; i++) {
    const localSlope = Math.abs(
      densityCurve[i + 1][1] - densityCurve[i - 1][1],
    );
    if (localSlope < flattest) {
      flattest = localSlope;
      shoulderIdx = i;
    }
  }
}
const shoulderPoint = shoulderIdx >= 0 ? densityCurve[shoulderIdx] : null;

// Vertical gradient fill: fuller near the curve, fading toward the baseline.
function hexToRgba(hex, alpha) {
  const n = parseInt(hex.slice(1), 16);
  return `rgba(${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}, ${alpha})`;
}
const areaGradient = new echarts.graphic.LinearGradient(0, 0, 0, 1, [
  { offset: 0, color: hexToRgba(t.palette[0], 0.4) },
  { offset: 1, color: hexToRgba(t.palette[0], 0.06) },
]);

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

// --- Option -----------------------------------------------------------------
const title =
  "Server Response Latency · density-basic · javascript · echarts · anyplot.ai";

chart.setOption({
  animation: false,
  color: t.palette,
  backgroundColor: "transparent",
  title: {
    text: title,
    left: "center",
    top: 30,
    textStyle: { color: t.ink, fontSize: 20, fontWeight: 500 },
  },
  grid: { left: 110, right: 70, top: 110, bottom: 100 },
  xAxis: {
    type: "value",
    name: "Response Latency (ms)",
    nameLocation: "middle",
    nameGap: 45,
    nameTextStyle: { color: t.ink, fontSize: 16 },
    min: 0,
    max: xMax,
    axisLabel: { color: t.inkSoft, fontSize: 14 },
    axisLine: { lineStyle: { color: t.inkSoft } },
    splitLine: { show: false },
  },
  yAxis: {
    type: "value",
    name: "Density",
    nameLocation: "middle",
    nameGap: 70,
    nameTextStyle: { color: t.ink, fontSize: 16 },
    min: rugY * 1.6,
    axisLabel: {
      color: t.inkSoft,
      fontSize: 14,
      formatter: (v) => (v < 0 ? "" : v.toFixed(3)),
    },
    axisLine: { lineStyle: { color: t.inkSoft } },
    splitLine: { lineStyle: { color: t.grid } },
  },
  series: [
    {
      name: "Density",
      type: "line",
      data: densityCurve,
      symbol: "none",
      smooth: false,
      lineStyle: { color: t.palette[0], width: 3.5 },
      areaStyle: { color: areaGradient },
      markPoint: shoulderPoint
        ? {
            silent: true,
            symbol: "circle",
            symbolSize: 8,
            itemStyle: {
              color: t.palette[0],
              borderColor: t.pageBg,
              borderWidth: 2,
            },
            label: {
              show: true,
              formatter: `Secondary shoulder\n~${Math.round(shoulderPoint[0])}ms`,
              color: t.ink,
              fontSize: 13,
              fontWeight: 500,
              align: "left",
              position: [12, -36],
              lineHeight: 16,
            },
            data: [{ coord: shoulderPoint, name: "shoulder" }],
          }
        : undefined,
      z: 2,
    },
    {
      name: "Observations",
      type: "scatter",
      data: rugData,
      symbol: "rect",
      symbolSize: [1, 10],
      itemStyle: { color: t.inkSoft, opacity: 0.32 },
      z: 1,
    },
  ],
});

Part of Basic Density Plot on anyplot.ai.

Other implementations