Basic Ridgeline Plot — Apache ECharts

A ridgeline plot (also known as Joy Plot, named after the Joy Division album cover) displays the distribution of multiple groups by stacking partially overlapping density curves vertically. This creates a mountain ridge appearance that allows efficient comparison of many distributions simultaneously while maintaining a compact and visually striking presentation.

Basic Ridgeline Plot rendered with Apache ECharts

Renders

JavaScript source (Apache ECharts)

// anyplot.ai
// ridgeline-basic: Basic Ridgeline Plot
// Library: echarts 6.1.0 | JavaScript 22.23.1
// Quality: 89/100 | Created: 2026-07-25

const t = window.ANYPLOT_TOKENS;
const size = window.ANYPLOT_SIZE;

// --- Deterministic PRNG (LCG + Box-Muller, no seeded RNG in the browser) ----
let seed = 42;
function nextUniform() {
  seed = (seed * 1103515245 + 12345) & 0x7fffffff;
  return seed / 0x7fffffff;
}
function nextGaussian() {
  const u1 = Math.max(nextUniform(), 1e-9);
  const u2 = nextUniform();
  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
}

// --- Data: call-center wait times (minutes) across 8 regional support hubs -
// Ordered by mean wait time, best to worst, top to bottom.
const REGIONS = [
  { label: "North", mean: 4.5, std: 1.4 },
  { label: "East", mean: 5.2, std: 1.3 },
  { label: "Pacific", mean: 5.8, std: 1.6 },
  { label: "South", mean: 6.0, std: 1.8 },
  { label: "Atlantic", mean: 6.8, std: 1.7 },
  { label: "Central", mean: 7.0, std: 1.9 },
  { label: "West", mean: 8.5, std: 2.4 },
  { label: "Mountain", mean: 9.5, std: 2.6 },
];
const SAMPLES_PER_GROUP = 300;

const groupSamples = REGIONS.map((region) => {
  const samples = [];
  for (let i = 0; i < SAMPLES_PER_GROUP; i++) {
    samples.push(Math.max(0.2, region.mean + region.std * nextGaussian()));
  }
  return samples;
});

// --- Gaussian KDE over a shared support --------------------------------------
const X_MIN = 0;
const X_MAX = 18; // multiple of the 3-unit tick interval so ticks land evenly
const GRID_POINTS = 120;
const xGrid = Array.from(
  { length: GRID_POINTS },
  (_, i) => X_MIN + (i * (X_MAX - X_MIN)) / (GRID_POINTS - 1)
);

function kde(samples, xValues) {
  const n = samples.length;
  const mean = samples.reduce((a, b) => a + b, 0) / n;
  const variance = samples.reduce((a, b) => a + (b - mean) ** 2, 0) / n;
  const std = Math.sqrt(variance);
  const bandwidth = 1.06 * std * Math.pow(n, -0.2); // Silverman's rule of thumb
  const norm = 1 / (n * bandwidth * Math.sqrt(2 * Math.PI));
  return xValues.map((xi) =>
    norm * samples.reduce((sum, s) => sum + Math.exp(-0.5 * ((xi - s) / bandwidth) ** 2), 0)
  );
}

const densities = groupSamples.map((samples) => kde(samples, xGrid));
const maxDensity = Math.max(...densities.flat());

// --- Layout: N overlapping grids, one per ridge -----------------------------
const N = REGIONS.length;
const LEFT = 130;
const RIGHT = 50;
const TOP = 150;
const BOTTOM = 90;
const OVERLAP = 2.0; // ridgeHeight / bandStep -> ~50% vertical overlap
const plotW = size.width - LEFT - RIGHT;
const plotH = size.height - TOP - BOTTOM;
const bandStep = plotH / N;
const ridgeHeight = bandStep * OVERLAP;

const grids = [];
const xAxes = [];
const yAxes = [];
const series = [];

REGIONS.forEach((region, i) => {
  const baseline = TOP + bandStep * (i + 1);

  grids.push({
    left: LEFT,
    width: plotW,
    top: baseline - ridgeHeight,
    height: ridgeHeight,
  });

  xAxes.push({
    gridIndex: i,
    type: "value",
    min: X_MIN,
    max: X_MAX,
    show: i === N - 1,
    axisLine: { lineStyle: { color: t.inkSoft } },
    axisTick: { show: false },
    axisLabel: { color: t.inkSoft, fontSize: 15 },
    splitLine: {
      show: i === N - 1,
      lineStyle: { color: t.grid, opacity: 0.5 },
    },
    name: i === N - 1 ? "Wait Time (minutes)" : undefined,
    nameLocation: "middle",
    nameGap: 38,
    nameTextStyle: { color: t.ink, fontSize: 17 },
  });

  yAxes.push({
    gridIndex: i,
    type: "value",
    min: 0,
    max: maxDensity * 1.05,
    show: true,
    axisLine: { show: false },
    axisTick: { show: false },
    splitLine: { show: false },
    axisLabel: {
      formatter: (_value, idx) => (idx === 0 ? region.label : ""),
      color: t.ink,
      fontSize: 16,
      fontWeight: 500,
      margin: 14,
    },
  });

  series.push({
    type: "line",
    xAxisIndex: i,
    yAxisIndex: i,
    data: xGrid.map((x, j) => [x, densities[i][j]]),
    smooth: true,
    symbol: "none",
    lineStyle: { width: 2, color: t.palette[i] },
    areaStyle: { color: t.palette[i], opacity: 0.68 },
    z: i + 2,
  });
});

// --- Title (fontsize scales with length, see plot-generator.md) -------------
const TITLE = "Call Center Wait Times · ridgeline-basic · javascript · echarts · anyplot.ai";
const titleFontSize = Math.max(15, Math.round(22 * Math.min(1, 67 / TITLE.length)));

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

// --- Option ---------------------------------------------------------------------
chart.setOption({
  animation: false,
  backgroundColor: "transparent",
  title: {
    text: TITLE,
    left: "center",
    top: 28,
    textStyle: { color: t.ink, fontSize: titleFontSize, fontWeight: 500 },
  },
  grid: grids,
  xAxis: xAxes,
  yAxis: yAxes,
  series: series,
});

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/ridgeline-basic/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": "ridgeline-basic",
  "language": "javascript",
  "library": "echarts",
  "page": "https://anyplot.ai/ridgeline-basic/javascript/echarts",
  "hub": "https://anyplot.ai/ridgeline-basic",
  "code_json": "https://api.anyplot.ai/specs/ridgeline-basic/echarts/code",
  "spec_json": "https://api.anyplot.ai/specs/ridgeline-basic",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/ridgeline-basic/javascript/echarts/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/ridgeline-basic/javascript/echarts/plot-dark.png",
  "interactive_light_html": "https://storage.googleapis.com/anyplot-images/plots/ridgeline-basic/javascript/echarts/plot-light.html",
  "interactive_dark_html": "https://storage.googleapis.com/anyplot-images/plots/ridgeline-basic/javascript/echarts/plot-dark.html",
  "quality_score": 89.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

Part of Basic Ridgeline Plot on anyplot.ai.

Other implementations