Basic Icicle Chart — Highcharts

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 Highcharts

Renders

JavaScript source (Highcharts)

// anyplot.ai
// icicle-basic: Basic Icicle Chart
// Library: highcharts 12.6.0 | JavaScript 22.23.2
// Quality: 87/100 | Created: 2026-09-05

//# anyplot-orientation: landscape

// Only the core Highcharts bundle is loaded (no `treemap`/`icicle` module — the
// icicle series is itself built on top of the treemap module, neither of which
// ships here), so the layered rectangles are computed with a partition layout
// (root spans the full width; each child's width is its value's share of the
// parent's width; a childless node stretches down to the last row so no gaps
// appear) and drawn by hand with `chart.renderer.rect()` — the same hand-drawn
// technique treemap-basic / sunburst-basic / sankey-basic use for series types
// the core bundle doesn't ship. Hover uses a native SVG `<title>` per
// rectangle, covering the whole tile.

const t = window.ANYPLOT_TOKENS;

// --- Data: repository file-system breakdown (KB) — name/parent/value --------
const RECORDS = [
  { name: "repo", parent: null },
  { name: "src", parent: "repo" },
  { name: "node_modules", parent: "repo" },
  { name: "docs", parent: "repo" },
  { name: "tests", parent: "repo" },
  { name: "assets", parent: "repo" },
  { name: "components", parent: "src" },
  { name: "utils", parent: "src" },
  { name: "api", parent: "src" },
  { name: "styles", parent: "src" },
  { name: "react", parent: "node_modules", value: 890 },
  { name: "webpack", parent: "node_modules", value: 540 },
  { name: "lodash", parent: "node_modules", value: 310 },
  { name: "guides", parent: "docs", value: 85 },
  { name: "api-reference", parent: "docs", value: 45 },
  { name: "unit", parent: "tests", value: 95 },
  { name: "integration", parent: "tests", value: 70 },
  { name: "images", parent: "assets", value: 210 },
  { name: "fonts", parent: "assets", value: 60 },
  { name: "Button.jsx", parent: "components", value: 95 },
  { name: "Modal.jsx", parent: "components", value: 80 },
  { name: "Chart.jsx", parent: "components", value: 90 },
  { name: "Table.jsx", parent: "components", value: 55 },
  { name: "format.js", parent: "utils", value: 55 },
  { name: "validate.js", parent: "utils", value: 45 },
  { name: "api-client.js", parent: "utils", value: 40 },
  { name: "routes.js", parent: "api", value: 65 },
  { name: "middleware.js", parent: "api", value: 45 },
  { name: "theme.css", parent: "styles", value: 35 },
  { name: "globals.css", parent: "styles", value: 25 },
];

// --- Build tree + bottom-up value rollup ------------------------------------
const nodesByName = new Map(RECORDS.map((r) => [r.name, { ...r, children: [] }]));
nodesByName.forEach((node) => {
  if (node.parent && nodesByName.has(node.parent)) {
    nodesByName.get(node.parent).children.push(node);
  }
});
function rollUp(node) {
  if (node.children.length) {
    node.value = node.children.reduce((s, c) => s + rollUp(c), 0);
  }
  return node.value;
}
const ROOT = nodesByName.get("repo");
rollUp(ROOT);

// --- Color + text helpers ----------------------------------------------------
function hexToRgb(hex) {
  const c = parseInt(hex.slice(1), 16);
  return [(c >> 16) & 255, (c >> 8) & 255, c & 255];
}
function mix(hexA, hexB, f) {
  const [r1, g1, b1] = hexToRgb(hexA);
  const [r2, g2, b2] = hexToRgb(hexB);
  const r = Math.round(r1 + (r2 - r1) * f);
  const g = Math.round(g1 + (g2 - g1) * f);
  const b = Math.round(b1 + (b2 - b1) * f);
  return `#${[r, g, b].map((v) => v.toString(16).padStart(2, "0")).join("")}`;
}
function luma(hex) {
  const [r, g, b] = hexToRgb(hex);
  return (0.299 * r + 0.587 * g + 0.114 * b) / 255;
}
const labelColorFor = (bgHex) => (luma(bgHex) > 0.55 ? "#1A1A17" : "#FFFDF6");
const kb = (v) => `${v.toLocaleString("en-US")} KB`;

// Shrink-to-fit: largest fontSize in [min, nominal] whose estimated text width
// fits maxWidth, or null if even `min` overflows — callers omit the label then.
function fitFontSize(text, maxWidth, nominal, min) {
  for (let size = nominal; size >= min; size--) {
    if (text.length * size * 0.56 + 6 <= maxWidth) return size;
  }
  return null;
}

function ancestorsOf(node) {
  const chain = [node];
  let cur = node;
  while (cur.parent && nodesByName.has(cur.parent)) {
    cur = nodesByName.get(cur.parent);
    chain.push(cur);
  }
  return chain;
}

// --- Title (fontsize scaled off the 67-char baseline) -----------------------
const TITLE_TEXT = "Repository File Sizes · icicle-basic · javascript · highcharts · anyplot.ai";
const TITLE_FS = Math.max(Math.round(22 * Math.min(1, 67 / TITLE_TEXT.length)), 14);

// --- Chart shell (no series/axes — the icicle is drawn by hand) ------------
const chart = Highcharts.chart("container", {
  chart: {
    backgroundColor: "transparent",
    animation: false,
    style: { fontFamily: "inherit" },
    marginTop: 112,
    marginBottom: 30,
    marginLeft: 30,
    marginRight: 30,
  },
  credits: { enabled: false },
  title: {
    text: TITLE_TEXT,
    style: { color: t.ink, fontSize: TITLE_FS + "px", fontWeight: "600" },
  },
  subtitle: {
    text: "Simulated project repository — tile width ∝ folder/file size (KB), rows ∝ directory depth",
    style: { color: t.inkSoft, fontSize: "14px" },
  },
  xAxis: { visible: false },
  yAxis: { visible: false },
  legend: { enabled: false },
  plotOptions: { series: { animation: false } },
  series: [],
});

const PLOT_W = chart.plotWidth;
const PLOT_H = chart.plotHeight;
const GAP = 3;

// --- Partition layout (values -> layered rectangles) ------------------------
// Root spans the full row; each child's width is its value's share of the
// parent's width. A node without children stretches down to the last row
// (classic icicle behaviour) instead of leaving blank space beneath it.
const DEPTH_COUNT = (() => {
  let max = 0;
  (function walk(node, depth) {
    max = Math.max(max, depth);
    node.children.forEach((c) => walk(c, depth + 1));
  })(ROOT, 0);
  return max + 1;
})();
// The root row only ever shows a single label, so giving it a full 1/DEPTH_COUNT
// share leaves a near-empty band at the top. Shrink it to ~55% of a normal row
// and redistribute the freed height across the deeper, information-dense rows.
const ROW_WEIGHTS = Array.from({ length: DEPTH_COUNT }, (_, d) => (d === 0 ? 0.55 : 1));
const ROW_UNIT = PLOT_H / ROW_WEIGHTS.reduce((a, b) => a + b, 0);
const ROW_Y = [0];
ROW_WEIGHTS.forEach((w) => ROW_Y.push(ROW_Y[ROW_Y.length - 1] + w * ROW_UNIT));

const TILES = [];
(function partition(node, x0, x1, depth, branchColor) {
  const y0 = ROW_Y[depth];
  const y1 = node.children.length ? ROW_Y[depth + 1] : PLOT_H;
  TILES.push({ node, x0, x1, y0, y1, depth, color: branchColor });
  if (node.children.length) {
    let cx = x0;
    node.children.forEach((child, i) => {
      const w = (x1 - x0) * (child.value / node.value);
      // Depth 1 (top-level folders) pick the next Imprint hue — abstract
      // categories, canonical order. Deeper descendants inherit their
      // branch's hue and get progressively tinted toward the page background.
      const childColor = depth === 0 ? t.palette[i % t.palette.length] : branchColor;
      partition(child, cx, cx + w, depth + 1, childColor);
      cx += w;
    });
  }
})(ROOT, 0, PLOT_W, 0, null);

// --- Draw ---------------------------------------------------------------
const g = chart.renderer.g("icicle").add();
// name -> { rect, fill, stroke, strokeWidth } — lets hover highlight the
// hovered tile and brighten its full ancestry chain back to the root.
const nodeElements = new Map();

function addTooltip(el, text) {
  const titleEl = document.createElementNS("http://www.w3.org/2000/svg", "title");
  titleEl.textContent = text;
  el.element.appendChild(titleEl);
}

TILES.forEach(({ node, x0, x1, y0, y1, depth, color }) => {
  const bx = chart.plotLeft + x0 + GAP / 2;
  const by = chart.plotTop + y0 + GAP / 2;
  const bw = Math.max(x1 - x0 - GAP, 0);
  const bh = Math.max(y1 - y0 - GAP, 0);
  if (bw <= 0 || bh <= 0) return;

  const isRoot = depth === 0;
  const fill = isRoot ? t.elevatedBg : mix(color, t.pageBg, Math.min(0.12 * (depth - 1), 0.45));
  const stroke = isRoot ? t.inkSoft : t.pageBg;
  // Depth-graded stroke weight (root heaviest, leaves lightest) instead of a
  // uniform 2px everywhere, plus a subtle corner radius for polish.
  const strokeWidth = Math.max(2.5 - depth * 0.4, 1.2);
  const pct = ((node.value / ROOT.value) * 100).toFixed(1);
  const path = (function ancestry(n) {
    return n.parent ? `${ancestry(nodesByName.get(n.parent))} → ${n.name}` : n.name;
  })(node);

  const rect = chart.renderer
    .rect(bx, by, bw, bh, 3)
    .attr({
      fill,
      stroke,
      "stroke-width": strokeWidth,
      zIndex: 2 + depth,
    })
    .add(g);
  addTooltip(rect, `${path}\n${kb(node.value)} (${pct}% of repo)`);
  nodeElements.set(node.name, { rect, fill, stroke, strokeWidth });

  // Highcharts-specific interactivity: hovering a tile brightens it and
  // thickens+recolors the stroke of every ancestor up to the root, tracing
  // the lineage chain — a native mouseover/mouseout touch on top of the
  // hand-drawn rects, most useful in the interactive HTML view.
  const chain = ancestorsOf(node);
  rect.element.addEventListener("mouseenter", () => {
    chain.forEach((n, i) => {
      const entry = nodeElements.get(n.name);
      if (!entry) return;
      entry.rect.attr({
        fill: i === 0 ? mix(entry.fill, "#FFFFFF", 0.15) : entry.fill,
        stroke: t.amber,
        "stroke-width": entry.strokeWidth + (i === 0 ? 1.5 : 1),
      });
    });
  });
  rect.element.addEventListener("mouseleave", () => {
    chain.forEach((n) => {
      const entry = nodeElements.get(n.name);
      if (!entry) return;
      entry.rect.attr({ fill: entry.fill, stroke: entry.stroke, "stroke-width": entry.strokeWidth });
    });
  });

  const label = node.name;
  const nameSize = fitFontSize(label, bw - 10, 16, 11);
  if (nameSize && bh >= 24) {
    const showValue = bh >= 46 && bw >= 56;
    const textColor = labelColorFor(fill);
    chart.renderer
      .text(label, bx + 8, showValue ? by + 20 : by + bh / 2 + nameSize * 0.35)
      .attr({ align: "left", zIndex: 6 + depth })
      .css({ color: textColor, fontSize: `${nameSize}px`, fontWeight: "600", pointerEvents: "none" })
      .add(g);
    if (showValue) {
      chart.renderer
        .text(kb(node.value), bx + 8, by + 20 + 16)
        .attr({ align: "left", zIndex: 6 + depth })
        .css({ color: textColor, fontSize: "12px", pointerEvents: "none" })
        .add(g);
    }
  }
});

Retrieve this implementation

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

Part of Basic Icicle Chart on anyplot.ai.

Other implementations