Basic Icicle Chart — Chart.js

An icicle chart displaying hierarchical data as adjacent rectangles in a layered structure, where each rectangle's size represents its value in the hierarchy. Unlike treemaps that nest rectangles, icicle charts stack them in rows (horizontal) or columns (vertical), making parent-child relationships explicitly visible through spatial adjacency. This layout excels at showing both the hierarchy levels and the proportional values simultaneously.

Basic Icicle Chart rendered with Chart.js

Renders

JavaScript source (Chart.js)

// anyplot.ai
// icicle-basic: Basic Icicle Chart
// Library: chartjs 4.4.7 | JavaScript 22.23.2
// Quality: 94/100 | Created: 2026-09-05

const t = window.ANYPLOT_TOKENS;

// --- Data (in-memory, deterministic) ---------------------------------------
// File system hierarchy: project -> folders -> files, value = size (KB).
const nodes = [
  { name: "project", parent: null, value: 100, level: 0 },
  { name: "src", parent: "project", value: 55, level: 1 },
  { name: "tests", parent: "project", value: 25, level: 1 },
  { name: "docs", parent: "project", value: 12, level: 1 },
  { name: "config", parent: "project", value: 8, level: 1 },
  { name: "components", parent: "src", value: 20, level: 2 },
  { name: "utils", parent: "src", value: 15, level: 2 },
  { name: "api", parent: "src", value: 12, level: 2 },
  { name: "styles", parent: "src", value: 8, level: 2 },
  { name: "unit", parent: "tests", value: 15, level: 2 },
  { name: "integration", parent: "tests", value: 10, level: 2 },
  { name: "guides", parent: "docs", value: 7, level: 2 },
  { name: "reference", parent: "docs", value: 5, level: 2 },
  { name: "webpack", parent: "config", value: 5, level: 2 },
  { name: "eslint", parent: "config", value: 3, level: 2 },
];

// Partition layout: give each node a horizontal span [x0, x1] within its
// parent's span, proportional to value (same idea as d3's partition layout).
const byParent = new Map();
for (const node of nodes) {
  if (!byParent.has(node.parent)) byParent.set(node.parent, []);
  byParent.get(node.parent).push(node);
}
const root = nodes.find((node) => node.parent === null);
root.x0 = 0;
root.x1 = 100;
const queue = [root];
while (queue.length) {
  const parent = queue.shift();
  const children = byParent.get(parent.name) || [];
  const total = children.reduce((sum, child) => sum + child.value, 0);
  let cursor = parent.x0;
  for (const child of children) {
    const width = (child.value / total) * (parent.x1 - parent.x0);
    child.x0 = cursor;
    child.x1 = cursor + width;
    cursor += width;
    queue.push(child);
  }
}

// Color by top-level branch (root = neutral, each folder its own hue, files
// inherit their folder's hue) so the hierarchy groups visually.
const branchColor = new Map([
  ["src", t.palette[0]],
  ["tests", t.palette[1]],
  ["docs", t.palette[2]],
  ["config", t.palette[3]],
]);
const colorByName = new Map(nodes.map((node) => [node.name, null]));
for (const node of nodes) {
  if (node.level === 0) colorByName.set(node.name, t.ink);
  else if (node.level === 1) colorByName.set(node.name, branchColor.get(node.name));
  else colorByName.set(node.name, colorByName.get(node.parent));
}

const rowLabels = ["Project", "Folder", "File"];

// --- Label contrast: pick ink-dark or ink-light text per rectangle fill -----
// (root uses the theme-adaptive ink/pageBg swap directly; branch/file rows use
// this since their fill colors are fixed across themes, so the readable text
// color must be picked per-color rather than swapped by theme.)
function relativeLuminance(r, g, b) {
  const lin = (c) => {
    const s = c / 255;
    return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
  };
  return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
}
function contrastRatio(l1, l2) {
  const lighter = Math.max(l1, l2);
  const darker = Math.min(l1, l2);
  return (lighter + 0.05) / (darker + 0.05);
}
function labelColorFor(hex) {
  const n = parseInt(hex.slice(1), 16);
  const luminance = relativeLuminance((n >> 16) & 255, (n >> 8) & 255, n & 255);
  const darkContrast = contrastRatio(luminance, relativeLuminance(0x1a, 0x1a, 0x17));
  const lightContrast = contrastRatio(luminance, relativeLuminance(0xf0, 0xef, 0xe8));
  return darkContrast >= lightContrast ? "#1A1A17" : "#F0EFE8";
}
const labelColorByName = new Map(
  nodes.map((node) => [
    node.name,
    node.level === 0 ? t.pageBg : labelColorFor(colorByName.get(node.name)),
  ]),
);

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

// --- Label plugin (draws node names onto rectangles with enough room) -------
const rectLabelPlugin = {
  id: "icicleRectLabels",
  afterDatasetsDraw(chart) {
    const { ctx } = chart;
    ctx.save();
    ctx.font = `600 15px ${Chart.defaults.font.family}`;
    ctx.textAlign = "center";
    ctx.textBaseline = "middle";
    nodes.forEach((node, i) => {
      const meta = chart.getDatasetMeta(i);
      const el = meta.data[node.level];
      if (!el) return;
      const { x, base, y } = el.getProps(["x", "base", "y"], true);
      const rectWidth = Math.abs(x - base);
      const textWidth = ctx.measureText(node.name).width;
      if (rectWidth < textWidth + 16) return;
      ctx.fillStyle = labelColorByName.get(node.name);
      ctx.fillText(node.name, (x + base) / 2, y);
    });
    ctx.restore();
  },
};

// --- Chart -------------------------------------------------------------------
new Chart(canvas, {
  type: "bar",
  data: {
    labels: rowLabels,
    datasets: nodes.map((node) => ({
      label: node.name,
      data: rowLabels.map((_, i) => (i === node.level ? [node.x0, node.x1] : null)),
      backgroundColor: colorByName.get(node.name),
      borderColor: t.pageBg,
      borderWidth: 2,
      borderSkipped: false,
      grouped: false,
      barPercentage: 1,
      categoryPercentage: 0.9,
    })),
  },
  options: {
    indexAxis: "y",
    responsive: true,
    maintainAspectRatio: false,
    animation: false,
    plugins: {
      title: {
        display: true,
        text: "icicle-basic · javascript · chartjs · anyplot.ai",
        color: t.ink,
        font: { size: 22 },
      },
      legend: { display: false },
      tooltip: { enabled: false },
    },
    scales: {
      x: {
        min: 0,
        max: 100,
        ticks: { color: t.inkSoft, font: { size: 14 }, callback: (v) => `${v}%` },
        grid: { color: t.grid },
        title: { display: true, text: "Share of Project Size", color: t.ink, font: { size: 16 } },
      },
      y: {
        ticks: { color: t.inkSoft, font: { size: 14 } },
        grid: { display: false },
        title: { display: true, text: "Hierarchy Level", color: t.ink, font: { size: 16 } },
      },
    },
  },
  plugins: [rectLabelPlugin],
});

Retrieve this implementation

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

Part of Basic Icicle Chart on anyplot.ai.

Other implementations