Basic Treemap — MUI X Charts

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 MUI X Charts

Renders

JavaScript source (MUI X Charts)

// anyplot.ai
// treemap-basic: Basic Treemap
// Library: muix 7.29.1 | JavaScript 22.23.2
// Quality: 93/100 | Created: 2026-08-24
import { ChartContainer } from "@mui/x-charts/ChartContainer";
import { useDrawingArea } from "@mui/x-charts/hooks";

const t = window.ANYPLOT_TOKENS;

const title = "treemap-basic · javascript · muix · anyplot.ai";
const titleFontSize = Math.max(18, Math.min(34, Math.round(34 * Math.min(1, 67 / title.length))));

// --- Data: a company expense breakdown by department (category) and cost
// center / project (subcategory), in $ thousands -- one of the spec's listed
// applications. 6 departments, 20 cost centers (26 rectangles total). -------
const DEPARTMENTS = [
  {
    name: "Engineering",
    items: [
      { name: "Platform Infra", value: 820 },
      { name: "Mobile Apps", value: 540 },
      { name: "DevOps", value: 310 },
      { name: "QA & Testing", value: 210 },
    ],
  },
  {
    name: "Sales",
    items: [
      { name: "Enterprise", value: 610 },
      { name: "SMB", value: 380 },
      { name: "Partnerships", value: 190 },
    ],
  },
  {
    name: "Marketing",
    items: [
      { name: "Digital Ads", value: 430 },
      { name: "Content", value: 260 },
      { name: "Events", value: 150 },
      { name: "Brand", value: 120 },
    ],
  },
  {
    name: "R&D",
    items: [
      { name: "Applied Research", value: 480 },
      { name: "Prototyping", value: 260 },
      { name: "Patents & Compliance", value: 140 },
    ],
  },
  {
    name: "Operations",
    items: [
      { name: "Facilities", value: 340 },
      { name: "IT Support", value: 220 },
      { name: "Logistics", value: 180 },
    ],
  },
  {
    name: "Customer Success",
    items: [
      { name: "Support", value: 300 },
      { name: "Onboarding", value: 180 },
      { name: "Training", value: 110 },
    ],
  },
];
const departments = DEPARTMENTS.map((d) => ({
  ...d,
  value: d.items.reduce((sum, i) => sum + i.value, 0),
}));
const grandTotal = departments.reduce((sum, d) => sum + d.value, 0);

// --- Squarified treemap layout (Bruls, Huizing & van Wijk, 1999) -----------
// Lays out `items` (each with a numeric `.value`) inside `container`
// ({x, y, w, h}), returning each item with added `.x/.y/.w/.h`, minimizing
// aspect ratio so rectangles stay close to square rather than thin slivers.
function worstRatio(row, length) {
  if (row.length === 0) return Infinity;
  const sum = row.reduce((a, r) => a + r.area, 0);
  const rmax = Math.max(...row.map((r) => r.area));
  const rmin = Math.min(...row.map((r) => r.area));
  return Math.max((length * length * rmax) / (sum * sum), (sum * sum) / (length * length * rmin));
}

function layoutRow(row, rect) {
  const sum = row.reduce((a, r) => a + r.area, 0);
  const { x, y, w, h } = rect;
  const placed = [];
  if (w >= h) {
    const stripW = sum / h;
    let cy = y;
    row.forEach((item) => {
      const itemH = item.area / stripW;
      placed.push({ ...item, x, y: cy, w: stripW, h: itemH });
      cy += itemH;
    });
    return { placed, rest: { x: x + stripW, y, w: w - stripW, h } };
  }
  const stripH = sum / w;
  let cx = x;
  row.forEach((item) => {
    const itemW = item.area / stripH;
    placed.push({ ...item, x: cx, y, w: itemW, h: stripH });
    cx += itemW;
  });
  return { placed, rest: { x, y: y + stripH, w, h: h - stripH } };
}

function squarify(items, container) {
  const total = items.reduce((s, i) => s + i.value, 0);
  const area = Math.max(container.w, 0) * Math.max(container.h, 0);
  const sorted = items.map((i) => ({ ...i, area: total > 0 ? (i.value / total) * area : 0 })).sort((a, b) => b.area - a.area);

  const results = [];
  let rect = container;
  let row = [];
  let remaining = sorted;
  while (remaining.length > 0) {
    const candidate = remaining[0];
    const shortSide = Math.min(rect.w, rect.h);
    const nextRow = row.concat(candidate);
    if (row.length === 0 || worstRatio(nextRow, shortSide) <= worstRatio(row, shortSide)) {
      row = nextRow;
      remaining = remaining.slice(1);
    } else {
      const { placed, rest } = layoutRow(row, rect);
      results.push(...placed);
      rect = rest;
      row = [];
    }
  }
  if (row.length > 0) results.push(...layoutRow(row, rect).placed);
  return results;
}

function inset(r, px) {
  const w = Math.max(0, r.w - 2 * px);
  const h = Math.max(0, r.h - 2 * px);
  return { ...r, x: r.x + px, y: r.y + px, w, h };
}

// --- Color: distinct Imprint hue per department; cost centers within a
// department get progressively lighter tints of that hue (largest = base
// hue, smallest = lightest), so shading intensity communicates both the
// category identity and the nesting depth, per the spec's Notes. -----------
function hexToRgb(hex) {
  const n = parseInt(hex.slice(1), 16);
  return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
}
function rgbToHsl(r, g, b) {
  r /= 255;
  g /= 255;
  b /= 255;
  const max = Math.max(r, g, b);
  const min = Math.min(r, g, b);
  const l = (max + min) / 2;
  if (max === min) return [0, 0, l];
  const d = max - min;
  const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
  let h;
  if (max === r) h = (g - b) / d + (g < b ? 6 : 0);
  else if (max === g) h = (b - r) / d + 2;
  else h = (r - g) / d + 4;
  return [h / 6, s, l];
}
function hueToRgb(p, q, tIn) {
  let tt = tIn;
  if (tt < 0) tt += 1;
  if (tt > 1) tt -= 1;
  if (tt < 1 / 6) return p + (q - p) * 6 * tt;
  if (tt < 1 / 2) return q;
  if (tt < 2 / 3) return p + (q - p) * (2 / 3 - tt) * 6;
  return p;
}
function hslToRgb(h, s, l) {
  if (s === 0) {
    const v = Math.round(l * 255);
    return [v, v, v];
  }
  const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
  const p = 2 * l - q;
  return [Math.round(hueToRgb(p, q, h + 1 / 3) * 255), Math.round(hueToRgb(p, q, h) * 255), Math.round(hueToRgb(p, q, h - 1 / 3) * 255)];
}
function tintRgb(hex, factor) {
  const [r, g, b] = hexToRgb(hex);
  const [h, s, l] = rgbToHsl(r, g, b);
  return hslToRgb(h, s, l + (0.94 - l) * factor);
}
function relLuminanceRgb([r, g, b]) {
  const srgb = [r, g, b].map((v) => v / 255).map((v) => (v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4)));
  return 0.2126 * srgb[0] + 0.7152 * srgb[1] + 0.0722 * srgb[2];
}
function textColorForRgb(rgb) {
  return relLuminanceRgb(rgb) > 0.45 ? "#1A1A17" : "#FAF8F1";
}
function textColorFor(hex) {
  return textColorForRgb(hexToRgb(hex));
}

// Keyed by department name (not array position) so color assignment stays
// correct even if DEPARTMENTS is reordered or rebalanced -- squarify() sorts
// its output by value, so indexing colors by position would silently
// misassign hues once input order no longer matches value order.
const DEPT_COLOR_BY_NAME = new Map(DEPARTMENTS.map((d, i) => [d.name, t.palette[i % t.palette.length]]));

// --- Layout: departments squarified over the chart area, then each
// department's cost centers squarified again inside a header-inset rect --
const MARGIN = { top: 130, right: 32, bottom: 24, left: 32 };
const GUTTER = 3;
const HEADER_H = 30;
const LABEL_FONT_SIZE = 15;
const CHAR_WIDTH_RATIO = 0.56;

function fmtK(v) {
  return `$${v.toLocaleString()}K`;
}
function fitsLabel(w, h, text, fontSize) {
  return w - 12 >= text.length * fontSize * CHAR_WIDTH_RATIO && h >= fontSize + 6;
}

function TreemapRect({ x, y, w, h, fill, stroke, strokeWidth, label, sublabel, fontSize, textFill, tooltip }) {
  const showLabel = label && fitsLabel(w, h, label, fontSize);
  const showSub = showLabel && sublabel && h >= fontSize * 2 + 10;
  return (
    <g>
      <rect x={x} y={y} width={w} height={h} fill={fill} stroke={stroke} strokeWidth={strokeWidth}>
        <title>{tooltip}</title>
      </rect>
      {showLabel && (
        <text x={x + 8} y={y + fontSize + 4} fontSize={fontSize} fontWeight={500} fill={textFill} pointerEvents="none">
          {label}
        </text>
      )}
      {showSub && (
        <text x={x + 8} y={y + fontSize * 2 + 6} fontSize={fontSize - 2} fill={textFill} opacity={0.85} pointerEvents="none">
          {sublabel}
        </text>
      )}
    </g>
  );
}

function Treemap() {
  // The drawing area comes from MUI X's own DrawingProvider (via
  // ChartContainer's `margin` prop) rather than a hand-rolled offset -- the
  // same layout primitive MUI X's own axis/legend components rely on.
  const { left, top, width, height } = useDrawingArea();
  const chartArea = { x: left, y: top, w: width, h: height };
  const deptRects = squarify(departments, chartArea).map((d) => ({ ...d, color: DEPT_COLOR_BY_NAME.get(d.name) }));

  return (
    <g>
      {deptRects.map((dept) => {
        const outer = inset(dept, GUTTER);
        const pct = ((dept.value / grandTotal) * 100).toFixed(1);
        const canShowHeader = outer.h >= HEADER_H + 40 && outer.w >= 70;
        const headerText = textColorFor(dept.color);

        if (!canShowHeader) {
          // Too small for a nested breakdown -- fill flat with the base hue.
          return (
            <TreemapRect
              key={dept.name}
              x={outer.x}
              y={outer.y}
              w={outer.w}
              h={outer.h}
              fill={dept.color}
              stroke={t.pageBg}
              strokeWidth={1.5}
              label={dept.name}
              fontSize={LABEL_FONT_SIZE - 1}
              textFill={headerText}
              tooltip={`${dept.name}: ${fmtK(dept.value)} (${pct}%)`}
            />
          );
        }

        const innerArea = { x: outer.x, y: outer.y + HEADER_H, w: outer.w, h: outer.h - HEADER_H };
        const itemRects = squarify(dept.items, innerArea);
        const maxItemArea = Math.max(...itemRects.map((x) => x.area));

        return (
          <g key={dept.name}>
            <rect x={outer.x} y={outer.y} width={outer.w} height={HEADER_H} fill={dept.color}>
              <title>{`${dept.name}: ${fmtK(dept.value)} (${pct}% of total)`}</title>
            </rect>
            <text x={outer.x + 8} y={outer.y + HEADER_H / 2 + 5} fontSize={LABEL_FONT_SIZE} fontWeight={700} fill={headerText}>
              {dept.name} · {fmtK(dept.value)}
            </text>
            <rect x={outer.x} y={outer.y} width={outer.w} height={outer.h} fill="none" stroke={t.pageBg} strokeWidth={GUTTER} />
            {itemRects.map((item) => {
              const r = inset(item, 2);
              // Largest cost center keeps the department's base hue; smaller
              // ones tint progressively lighter, so shade intensity echoes
              // relative size within the department (nesting-depth cue).
              const tintFactor = itemRects.length <= 1 ? 0 : (1 - item.area / maxItemArea) * 0.75;
              const rgb = tintRgb(dept.color, tintFactor);
              const fill = `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`;
              const itemPct = ((item.value / dept.value) * 100).toFixed(0);
              return (
                <TreemapRect
                  key={item.name}
                  x={r.x}
                  y={r.y}
                  w={r.w}
                  h={r.h}
                  fill={fill}
                  stroke={t.pageBg}
                  strokeWidth={1.5}
                  label={item.name}
                  sublabel={fmtK(item.value)}
                  fontSize={LABEL_FONT_SIZE - 2}
                  textFill={textColorForRgb(rgb)}
                  tooltip={`${dept.name} / ${item.name}: ${fmtK(item.value)} (${itemPct}% of ${dept.name})`}
                />
              );
            })}
          </g>
        );
      })}
    </g>
  );
}

const topDept = departments.reduce((a, b) => (b.value > a.value ? b : a));
const topDeptPct = ((topDept.value / grandTotal) * 100).toFixed(0);
const insight = `${topDept.name} leads at ${fmtK(topDept.value)} — ${topDeptPct}% of total budget`;

// --- Chart (default-exported component -- the harness mounts it) ----------
// ChartContainer supplies the <ChartsSurface> SVG root and theme context; its
// `margin` prop drives the DrawingProvider that Treemap() reads back via
// useDrawingArea(), so the top-chrome/plot split is expressed through MUI X's
// own layout system rather than a parallel hand-rolled offset. The treemap
// body itself is laid out in absolute pixel space via squarify() above, so no
// axis/scale is needed -- xAxis/yAxis are omitted entirely.
export default function Chart() {
  const { width, height } = window.ANYPLOT_SIZE;

  return (
    <ChartContainer width={width} height={height} series={[]} margin={MARGIN} skipAnimation>
      <text x={width / 2} y={48} textAnchor="middle" fontSize={titleFontSize} fontWeight={600} fill={t.ink}>
        {title}
      </text>
      <text x={width / 2} y={78} textAnchor="middle" fontSize={14} fill={t.inkSoft}>
        Company expense breakdown by department and cost center · values in $K, area ∝ budget
      </text>
      <text x={width / 2} y={102} textAnchor="middle" fontSize={13} fontStyle="italic" fill={DEPT_COLOR_BY_NAME.get(topDept.name)}>
        {insight}
      </text>
      <Treemap />
    </ChartContainer>
  );
}

Part of Basic Treemap on anyplot.ai.

Other implementations