Density Plot with Rug Marks — Apache ECharts

A kernel density estimation (KDE) plot combined with rug marks along the x-axis, showing both the smoothed probability distribution and the exact location of each individual data point. This combination provides the best of both worlds: the KDE reveals the overall shape, modality, and smoothed density of the distribution, while the rug marks preserve transparency about where actual observations fall, highlighting data density and potential gaps.

Density Plot with Rug Marks rendered with Apache ECharts

Renders

JavaScript source (Apache ECharts)

// anyplot.ai
// density-rug: Density Plot with Rug Marks
// Library: echarts 6.1.0 | JavaScript 22.23.2
// Quality: 91/100 | Created: 2026-09-02

const t = window.ANYPLOT_TOKENS;

// --- Data (in-memory, deterministic fixed-seed LCG) -------------------------
function makeLcg(seed) {
  let state = seed >>> 0;
  return () => {
    state = (1103515245 * state + 12345) >>> 0;
    return state / 4294967296;
  };
}
const rand = makeLcg(42);

function gaussianSample() {
  const u1 = Math.max(rand(), 1e-9);
  const u2 = rand();
  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
}

// Reaction times (ms) from a two-speed mixture of responders — a KDE reveals
// the bimodal shape that a plain histogram bin width could easily hide, and
// the rug preserves exactly which trials produced each reading.
const sampleSize = 180;
const reactionTimesMs = [];
for (let i = 0; i < sampleSize; i++) {
  const isQuickResponder = rand() < 0.55;
  const mean = isQuickResponder ? 320 : 480;
  const std = isQuickResponder ? 35 : 50;
  reactionTimesMs.push(mean + gaussianSample() * std);
}

// --- Kernel density estimate --------------------------------------------------
const n = reactionTimesMs.length;
const meanMs = reactionTimesMs.reduce((sum, v) => sum + v, 0) / n;
const variance =
  reactionTimesMs.reduce((sum, v) => sum + (v - meanMs) ** 2, 0) / (n - 1);
const stdMs = Math.sqrt(variance);
const bandwidth = 1.06 * stdMs * Math.pow(n, -1 / 5); // Silverman's rule of thumb

function densityAt(x) {
  const sum = reactionTimesMs.reduce((acc, xi) => {
    const u = (x - xi) / bandwidth;
    return acc + Math.exp(-0.5 * u * u) / Math.sqrt(2 * Math.PI);
  }, 0);
  return sum / (n * bandwidth);
}

const minObs = Math.min(...reactionTimesMs);
const maxObs = Math.max(...reactionTimesMs);
const gridStart = minObs - 3 * bandwidth;
const gridEnd = maxObs + 3 * bandwidth;
const gridSteps = 220;
const densityPoints = [];
for (let i = 0; i <= gridSteps; i++) {
  const x = gridStart + ((gridEnd - gridStart) * i) / gridSteps;
  densityPoints.push([x, densityAt(x)]);
}
const maxDensity = Math.max(...densityPoints.map((p) => p[1]));

// Reserve a band below zero (never rendered as a labelled tick) for the rug.
// Ticks are shorter than the band and jittered vertically within it so a
// dense cluster of nearby x-values scatters into visible texture instead of
// merging into a solid block.
const rugBandTop = 0;
const rugBandBottom = -maxDensity * 0.16;
const yAxisMin = rugBandBottom * 1.1;
const yAxisMax = maxDensity * 1.15;

const rugTickHeight = (rugBandTop - rugBandBottom) * 0.55;
const rugJitterRange = rugBandTop - rugBandBottom - rugTickHeight;
const jitterRand = makeLcg(7);
const rugTickTops = reactionTimesMs.map(() => -jitterRand() * rugJitterRange);

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

// --- Option ---------------------------------------------------------------------
chart.setOption({
  animation: false,
  color: t.palette,
  backgroundColor: "transparent",
  title: {
    text: "density-rug · javascript · echarts · anyplot.ai",
    left: "center",
    textStyle: { color: t.ink, fontSize: 28, fontWeight: 500 },
  },
  legend: {
    top: 46,
    data: ["KDE density", "Individual trials (rug)"],
    textStyle: { color: t.ink, fontSize: 16 },
  },
  grid: { left: 110, right: 60, top: 110, bottom: 90 },
  tooltip: {
    trigger: "item",
    backgroundColor: t.elevatedBg,
    borderColor: t.grid,
    textStyle: { color: t.ink },
  },
  xAxis: {
    type: "value",
    name: "Reaction time (ms)",
    nameLocation: "middle",
    nameGap: 46,
    nameTextStyle: { color: t.ink, fontSize: 16 },
    min: gridStart,
    max: gridEnd,
    axisLine: { onZero: false, lineStyle: { color: t.inkSoft } },
    axisLabel: {
      color: t.inkSoft,
      fontSize: 14,
      formatter: (value) => Math.round(value).toString(),
    },
    splitLine: { show: false },
  },
  yAxis: {
    type: "value",
    name: "Density",
    nameLocation: "middle",
    nameGap: 80,
    nameTextStyle: { color: t.ink, fontSize: 16 },
    min: yAxisMin,
    max: yAxisMax,
    axisLine: { lineStyle: { color: t.inkSoft } },
    axisLabel: {
      color: t.inkSoft,
      fontSize: 14,
      formatter: (value) => (value < 0 ? "" : value.toFixed(4)),
    },
    splitLine: { show: false },
  },
  series: [
    {
      name: "KDE density",
      type: "line",
      data: densityPoints,
      smooth: true,
      symbol: "none",
      lineStyle: { color: t.palette[0], width: 3.5 },
      areaStyle: { color: t.palette[0], opacity: 0.25 },
      z: 2,
      markLine: {
        silent: true,
        symbol: "none",
        animation: false,
        lineStyle: { color: t.grid, width: 1 },
        label: { show: false },
        data: [{ yAxis: 0 }],
      },
    },
    {
      name: "Individual trials (rug)",
      type: "custom",
      coordinateSystem: "cartesian2d",
      data: reactionTimesMs,
      itemStyle: { color: t.palette[0], opacity: 0.3 },
      z: 3,
      renderItem: (params, api) => {
        const value = api.value(0);
        const top = rugTickTops[params.dataIndex];
        const bottom = top - rugTickHeight;
        const topPx = api.coord([value, top]);
        const bottomPx = api.coord([value, bottom]);
        return {
          type: "line",
          shape: {
            x1: topPx[0],
            y1: topPx[1],
            x2: bottomPx[0],
            y2: bottomPx[1],
          },
          style: {
            stroke: t.palette[0],
            lineWidth: 2,
            opacity: 0.3,
          },
        };
      },
    },
  ],
});

Retrieve this implementation

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

Part of Density Plot with Rug Marks on anyplot.ai.

Other implementations