Density Plot with Rug Marks — Chart.js

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 Chart.js

Renders

JavaScript source (Chart.js)

// anyplot.ai
// density-rug: Density Plot with Rug Marks
// Library: chartjs 4.4.7 | JavaScript 22.23.2
// Quality: 94/100 | Created: 2026-09-02

const t = window.ANYPLOT_TOKENS;

// --- Data (in-memory, deterministic) ----------------------------------------
// Support-ticket first-response times (minutes): a fast "auto-triaged" cohort
// and a slower "needs a human" cohort — a realistic bimodal shape.
function lcg(seed) {
  let state = seed;
  return () => {
    state = (state * 1103515245 + 12345) & 0x7fffffff;
    return state / 0x7fffffff;
  };
}
const rand = lcg(42);
const jitterRand = lcg(7);
function gaussian() {
  const u1 = Math.max(rand(), 1e-9);
  const u2 = rand();
  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
}

const responseTimes = [];
for (let i = 0; i < 110; i++) {
  responseTimes.push(Math.max(0.2, 2.6 + gaussian() * 0.55));
}
for (let i = 0; i < 70; i++) {
  responseTimes.push(Math.max(0.2, 6.3 + gaussian() * 1.15));
}

// --- Kernel density estimate (Gaussian kernel, Silverman bandwidth) --------
const n = responseTimes.length;
const mean = responseTimes.reduce((a, b) => a + b, 0) / n;
const variance =
  responseTimes.reduce((a, b) => a + (b - mean) ** 2, 0) / (n - 1);
const std = Math.sqrt(variance);
const bandwidth = 1.06 * std * n ** (-1 / 5);

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

const dataMin = Math.min(...responseTimes);
const dataMax = Math.max(...responseTimes);
const gridMin = Math.max(0, dataMin - 3 * bandwidth);
const gridMax = dataMax + 3 * bandwidth;
const gridSteps = 200;
const curve = Array.from({ length: gridSteps + 1 }, (_, i) => {
  const x = gridMin + ((gridMax - gridMin) * i) / gridSteps;
  return { x, y: density(x) };
});
const peakDensity = Math.max(...curve.map((p) => p.y));

// Two tallest local maxima, ordered by x, call out the fast/slow cohorts.
function localMaxima(points) {
  const maxima = [];
  for (let i = 1; i < points.length - 1; i++) {
    if (points[i].y > points[i - 1].y && points[i].y > points[i + 1].y) {
      maxima.push(points[i]);
    }
  }
  return maxima;
}
const modes = localMaxima(curve)
  .sort((a, b) => b.y - a.y)
  .slice(0, 2)
  .sort((a, b) => a.x - b.x);
const modeLabels = ["Auto-triaged", "Needs a human"];

// --- Color helpers -----------------------------------------------------------
function hexToRgba(hex, alpha) {
  const r = parseInt(hex.slice(1, 3), 16);
  const g = parseInt(hex.slice(3, 5), 16);
  const b = parseInt(hex.slice(5, 7), 16);
  return `rgba(${r}, ${g}, ${b}, ${alpha})`;
}
const brand = t.palette[0];

// --- Mount --------------------------------------------------------------------
const canvas = document.createElement("canvas");
document.getElementById("container").appendChild(canvas);

// --- Rug marks plugin (core Chart.js plugin API — no external dependency) ---
const rugTickHeight = 22;
const rugPlugin = {
  id: "rugMarks",
  afterDatasetsDraw(chart) {
    const { ctx, chartArea, scales } = chart;
    const xScale = scales.x;
    ctx.save();
    ctx.strokeStyle = hexToRgba(brand, 0.3);
    ctx.lineWidth = 1.5;
    responseTimes.forEach((value) => {
      const xPixel = xScale.getPixelForValue(value);
      // Stagger tick height so overlapping ticks in the two dense clusters
      // don't merge into a solid block.
      const tickHeight = rugTickHeight + (jitterRand() - 0.5) * 10;
      ctx.beginPath();
      ctx.moveTo(xPixel, chartArea.bottom);
      ctx.lineTo(xPixel, chartArea.bottom - tickHeight);
      ctx.stroke();
    });
    ctx.restore();
  },
};

// --- Mode guides plugin (dashed callouts at the two KDE peaks) --------------
const modeGuidesPlugin = {
  id: "modeGuides",
  afterDatasetsDraw(chart) {
    if (modes.length < 2) return;
    const { ctx, chartArea, scales } = chart;
    const xScale = scales.x;
    const yScale = scales.y;
    ctx.save();
    ctx.strokeStyle = hexToRgba(t.inkSoft, 0.5);
    ctx.lineWidth = 1;
    ctx.setLineDash([4, 4]);
    ctx.font = "12px sans-serif";
    ctx.fillStyle = t.inkSoft;
    ctx.textAlign = "center";
    ctx.textBaseline = "top";
    const lineTop = chartArea.top + 16;
    modes.forEach((mode, i) => {
      const xPixel = xScale.getPixelForValue(mode.x);
      const yPixel = yScale.getPixelForValue(mode.y);
      ctx.beginPath();
      ctx.moveTo(xPixel, lineTop);
      ctx.lineTo(xPixel, yPixel);
      ctx.stroke();
      ctx.fillText(modeLabels[i], xPixel, chartArea.top);
    });
    ctx.restore();
  },
};

// --- Chart ---------------------------------------------------------------------
new Chart(canvas, {
  type: "line",
  data: {
    datasets: [
      {
        label: "Density estimate",
        data: curve,
        parsing: false,
        borderColor: brand,
        backgroundColor: hexToRgba(brand, 0.2),
        borderWidth: 3,
        fill: "origin",
        tension: 0.3,
        pointRadius: 0,
      },
    ],
  },
  options: {
    responsive: true,
    maintainAspectRatio: false,
    animation: false,
    layout: { padding: { top: 4, bottom: 4 } },
    plugins: {
      title: {
        display: true,
        text: "density-rug · javascript · chartjs · anyplot.ai",
        color: t.ink,
        font: { size: 22 },
        padding: { bottom: 20 },
      },
      legend: { display: false },
      tooltip: { enabled: false },
    },
    scales: {
      x: {
        type: "linear",
        min: gridMin,
        max: gridMax,
        ticks: { color: t.inkSoft, font: { size: 14 } },
        grid: { display: false },
        border: { color: t.inkSoft },
        title: {
          display: true,
          text: "First Response Time (minutes)",
          color: t.ink,
          font: { size: 16 },
        },
      },
      y: {
        beginAtZero: true,
        suggestedMax: peakDensity * 1.2,
        ticks: { color: t.inkSoft, font: { size: 14 } },
        grid: { color: t.grid },
        border: { display: false },
        title: {
          display: true,
          text: "Density",
          color: t.ink,
          font: { size: 16 },
        },
      },
    },
  },
  plugins: [rugPlugin, modeGuidesPlugin],
});

Retrieve this implementation

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

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

Other implementations