Basic Treemap — Highcharts

A treemap displaying hierarchical data as nested rectangles, where each rectangle's area is proportional to its value. This visualization excels at showing part-to-whole relationships in hierarchical structures, making it easy to spot large and small items at a glance. Treemaps efficiently use screen space to display large amounts of hierarchical data in a compact form.

Basic Treemap rendered with Highcharts

Renders

JavaScript source (Highcharts)

// anyplot.ai
// treemap-basic: Basic Treemap
// Library: highcharts 12.6.0 | JavaScript 22.23.2
// Quality: 90/100 | Created: 2026-08-24

//# anyplot-orientation: landscape

// Only the core Highcharts bundle is loaded (no `treemap` module), so the
// rectangles are computed with a squarified-treemap algorithm (Bruls,
// Huizing & van Wijk) and drawn by hand with `chart.renderer.rect()` — the
// same hand-drawn technique sunburst-basic / sankey-basic / heatmap-basic
// use for series types the core bundle doesn't ship. Hover uses a native
// SVG `<title>` per rectangle (same trick as sunburst-basic), which covers
// the whole tile — not just an inscribed circle.

const t = window.ANYPLOT_TOKENS;

// --- Data: annual department budget by cost center (USD thousands) --------
const DEPARTMENTS = [
  {
    name: "Engineering",
    items: [
      { name: "Cloud Infrastructure", value: 850 },
      { name: "Contractor Dev", value: 420 },
      { name: "R&D Prototypes", value: 260 },
      { name: "Tooling & Licenses", value: 180 },
    ],
  },
  {
    name: "Sales & Marketing",
    items: [
      { name: "Digital Advertising", value: 610 },
      { name: "Sales Commissions", value: 480 },
      { name: "Trade Shows", value: 240 },
      { name: "Content & Creative", value: 190 },
    ],
  },
  {
    name: "Operations",
    items: [
      { name: "Customer Support", value: 340 },
      { name: "Facilities", value: 320 },
      { name: "Logistics", value: 275 },
    ],
  },
  {
    name: "G&A",
    items: [
      { name: "HR & Recruiting", value: 210 },
      { name: "Legal & Compliance", value: 150 },
      { name: "Finance & Accounting", value: 130 },
    ],
  },
];

DEPARTMENTS.forEach((dept, i) => {
  dept.color = t.palette[i]; // canonical order — abstract departments, no semantic hue
  dept.total = dept.items.reduce((s, d) => s + d.value, 0);
});
const GRAND_TOTAL = DEPARTMENTS.reduce((s, d) => s + d.total, 0);
// Largest single cost center overall — gets a bolder border below as the
// chart's one focal-point emphasis beyond the standard size/color/tint encoding.
const TOP_LEAF_VALUE = Math.max(...DEPARTMENTS.flatMap((d) => d.items.map((i) => i.value)));

// --- Squarified treemap layout (values -> rectangles) -----------------------
function worstRatio(row, side) {
  const sum = row.reduce((s, d) => s + d.area, 0);
  const maxA = Math.max(...row.map((d) => d.area));
  const minA = Math.min(...row.map((d) => d.area));
  return Math.max((side * side * maxA) / (sum * sum), (sum * sum) / (side * side * minA));
}

function squarify(items, x0, y0, x1, y1, out) {
  if (!items.length) return;
  const w = x1 - x0;
  const h = y1 - y0;
  const side = Math.min(w, h);
  let row = [items[0]];
  let i = 1;
  while (i < items.length) {
    const nextRow = row.concat(items[i]);
    if (worstRatio(nextRow, side) <= worstRatio(row, side)) {
      row = nextRow;
      i++;
    } else {
      break;
    }
  }
  const rowSum = row.reduce((s, d) => s + d.area, 0);
  if (w >= h) {
    const stripW = rowSum / h;
    let cy = y0;
    row.forEach((d) => {
      const rectH = d.area / stripW;
      out.push({ item: d, x: x0, y: cy, w: stripW, h: rectH });
      cy += rectH;
    });
    squarify(items.slice(row.length), x0 + stripW, y0, x1, y1, out);
  } else {
    const stripH = rowSum / w;
    let cx = x0;
    row.forEach((d) => {
      const rectW = d.area / stripH;
      out.push({ item: d, x: cx, y: y0, w: rectW, h: stripH });
      cx += rectW;
    });
    squarify(items.slice(row.length), x0, y0 + stripH, x1, y1, out);
  }
}

function layout(entries, x0, y0, x1, y1) {
  const total = entries.reduce((s, d) => s + d.value, 0);
  const scale = ((x1 - x0) * (y1 - y0)) / total;
  const sized = entries
    .slice()
    .sort((a, b) => b.value - a.value)
    .map((d) => ({ ...d, area: d.value * scale }));
  const out = [];
  squarify(sized, x0, y0, x1, y1, out);
  return out;
}

// --- 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 money = (v) => `$${v.toLocaleString("en-US")}K`;

// Shrink-to-fit: returns the largest fontSize in [min, nominal] whose
// estimated text width fits maxWidth, or null if even `min` overflows —
// callers treat null as "omit the label" (small tiles stay unlabeled).
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;
}

// --- Title (fontsize scaled off the 67-char baseline) -----------------------
const TITLE_TEXT = "Annual Department Budget · treemap-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 treemap is drawn by hand) -----------
const chart = Highcharts.chart("container", {
  chart: {
    backgroundColor: "transparent",
    animation: false,
    style: { fontFamily: "inherit" },
    marginTop: 112,
    marginBottom: 40,
    marginLeft: 40,
    marginRight: 40,
  },
  credits: { enabled: false },
  title: {
    text: TITLE_TEXT,
    style: { color: t.ink, fontSize: TITLE_FS + "px", fontWeight: "600" },
  },
  subtitle: {
    text: "Simulated annual operating budget by department and cost center — tile area ∝ spend (USD thousands)",
    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 DEPT_GAP = 10;
const LEAF_GAP = 4;
const HEADER_H = 42;
const HEADER_MIN_W = 90;
const HEADER_MIN_H = 90;

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

const g = chart.renderer.g("treemap").add();

// --- Level 1: department blocks ---------------------------------------------
const deptItems = DEPARTMENTS.map((d) => ({ name: d.name, value: d.total, dept: d }));
const deptRects = layout(deptItems, 0, 0, PLOT_W, PLOT_H);

deptRects.forEach(({ item, x, y, w, h }) => {
  const dept = item.dept;
  const bx = chart.plotLeft + x + DEPT_GAP / 2;
  const by = chart.plotTop + y + DEPT_GAP / 2;
  const bw = w - DEPT_GAP;
  const bh = h - DEPT_GAP;
  const deptPct = ((dept.total / GRAND_TOTAL) * 100).toFixed(1);

  const showHeader = bw >= HEADER_MIN_W && bh >= HEADER_MIN_H;
  const border = chart.renderer
    .rect(bx, by, bw, bh, 3)
    .attr({ fill: "none", stroke: dept.color, "stroke-width": 3, zIndex: 2 })
    .add(g);
  addTooltip(border, `${dept.name}\n${money(dept.total)} total (${deptPct}% of budget)`);

  let leafY0 = by + LEAF_GAP;
  if (showHeader) {
    const headerEl = chart.renderer
      .rect(bx, by, bw, HEADER_H, 3)
      .attr({ fill: dept.color, zIndex: 3 })
      .add(g);
    addTooltip(headerEl, `${dept.name}\n${money(dept.total)} total (${deptPct}% of budget)`);

    const headerLabel = `${dept.name} · ${money(dept.total)}`;
    const headerSize = fitFontSize(headerLabel, bw - 20, 18, 12);
    const text = headerSize ? headerLabel : dept.name;
    const finalSize = headerSize || fitFontSize(dept.name, bw - 20, 18, 12);
    if (finalSize) {
      chart.renderer
        .text(text, bx + 10, by + HEADER_H / 2 + finalSize * 0.35)
        .attr({ align: "left", zIndex: 4 })
        .css({ color: labelColorFor(dept.color), fontSize: `${finalSize}px`, fontWeight: "600", pointerEvents: "none" })
        .add(g);
    }
    leafY0 = by + HEADER_H + LEAF_GAP;
  }

  const innerX0 = bx + LEAF_GAP;
  const innerY0 = leafY0;
  const innerX1 = bx + bw - LEAF_GAP;
  const innerY1 = by + bh - LEAF_GAP;
  if (innerX1 <= innerX0 || innerY1 <= innerY0) return;

  const leafRects = layout(dept.items, innerX0, innerY0, innerX1, innerY1);
  leafRects.forEach((leaf, rank) => {
    const tint = Math.min(0.15 + rank * 0.09, 0.6);
    const fill = mix(dept.color, "#FFFFFF", tint);
    const pct = ((leaf.item.value / GRAND_TOTAL) * 100).toFixed(1);
    const isTopLeaf = leaf.item.value === TOP_LEAF_VALUE;

    const tileEl = chart.renderer
      .rect(leaf.x, leaf.y, leaf.w, leaf.h)
      .attr({
        fill,
        stroke: isTopLeaf ? t.ink : t.pageBg,
        "stroke-width": isTopLeaf ? 3 : 2,
        zIndex: isTopLeaf ? 4 : 3,
      })
      .add(g);
    addTooltip(
      tileEl,
      `${dept.name} → ${leaf.item.name}\n${money(leaf.item.value)} (${pct}% of budget)${isTopLeaf ? " — largest line item" : ""}`,
    );

    const nameSize = fitFontSize(leaf.item.name, leaf.w - 16, 15, 10);
    if (nameSize && leaf.h >= 30) {
      const showValue = leaf.h >= 52 && leaf.w >= 60;
      const nameY = showValue ? leaf.y + leaf.h / 2 - 6 : leaf.y + leaf.h / 2 + nameSize * 0.35;
      chart.renderer
        .text(leaf.item.name, leaf.x + 8, nameY)
        .attr({ align: "left", zIndex: 5 })
        .css({ color: labelColorFor(fill), fontSize: `${nameSize}px`, fontWeight: "600", pointerEvents: "none" })
        .add(g);
      if (showValue) {
        chart.renderer
          .text(money(leaf.item.value), leaf.x + 8, leaf.y + leaf.h / 2 + 14)
          .attr({ align: "left", zIndex: 5 })
          .css({ color: labelColorFor(fill), fontSize: "12px", pointerEvents: "none" })
          .add(g);
      }
    } else {
      // Fallback for tiles too small to fit the cost-center name: fall back to
      // a value-only label (smaller min font) so no tile ships completely blank.
      const valueText = money(leaf.item.value);
      const valueSize = fitFontSize(valueText, leaf.w - 10, 12, 7);
      if (valueSize && leaf.h >= 16) {
        chart.renderer
          .text(valueText, leaf.x + leaf.w / 2, leaf.y + leaf.h / 2 + valueSize * 0.35)
          .attr({ align: "center", zIndex: 5 })
          .css({ color: labelColorFor(fill), fontSize: `${valueSize}px`, fontWeight: "600", pointerEvents: "none" })
          .add(g);
      }
    }
  });
});

Part of Basic Treemap on anyplot.ai.

Other implementations