Treemaps — when rectangles beat words

Not every weighted visualization needs scattered text. Treemaps tile the canvas into rectangles whose areas reflect weight — no overlaps, no gaps, every category visible at once. The same { text, weight } input that feeds a word cloud feeds treemap() too; only the output shape changes.

Coordinate model differs from word clouds. Treemap entries use (x, y) as the top-left corner of each tile, plus width and height. Word-cloud placements use the bbox center. Mixing the two render paths without adjusting anchors will misalign drawings.

Squarified treemap algorithm (high level):

  1. Sort items by descending weight so large tiles are decided first.
  2. Greedily pack items into horizontal or vertical strips along the remaining free rectangle, picking the strip orientation that keeps child rectangles closest to square.
  3. Each strip's thickness is chosen so the strip's total area matches the sum of its items' weights (scaled to canvas area).
  4. Recurse into the leftover L-shaped region until every item has a rectangle.

The result is readable at a glance: big categories get big tiles, small ones nest in corners. Unlike word clouds, every input item receives a tile (nothing is "dropped" for lack of space) as long as weights are positive and the canvas area is non-zero.

Step by step

Step 1

Call treemap()

Import treemap from the same entry point as word-cloud layout. Options are simpler here — only width and height matter. Font sizes are not computed for you; pick label typography when you render.

treemap-layout.js
import { treemap } from "./js/index.js";

const items = [
  { text: "WebAssembly", weight: 100 },
  { text: "Rust",        weight: 78  },
  { text: "layout",      weight: 60  },
  { text: "treemap",     weight: 44  },
  // … more items
];

const width  = 960;
const height = 540;

const rects = await treemap(items, { width, height });

// rects → [{ text, value, x, y, width, height }, ...]
// `value` echoes the input weight; areas ≈ (value / sum) × width × height
console.log(rects[0]);
// { text: "WebAssembly", value: 100, x: 0, y: 0, width: 412, height: 298 }
Step 2

Render tiles with fillRect

Use ctx.fillRect(x, y, width, height) directly — no translate/rotate dance. A 1px inset on each side makes adjacent tile borders visible. Labels are optional UI sugar: only draw text when the cell is large enough to read.

render-treemap.js
/** Minimum cell size (px) before we attempt a label */
const MIN_LABEL_W = 34;
const MIN_LABEL_H = 16;
const INSET       = 1;   // gap between tile fill and grid line
const PAD         = 6;   // label padding inside tile

export function drawTreemap(ctx, rects, palette) {
  ctx.textBaseline = "top";

  for (let i = 0; i < rects.length; i++) {
    const r = rects[i];
    const iw = Math.max(0, r.width  - INSET * 2);
    const ih = Math.max(0, r.height - INSET * 2);

    // Tile fill
    ctx.fillStyle = palette[i % palette.length];
    ctx.fillRect(r.x + INSET, r.y + INSET, iw, ih);

    // Label — skip tiny cells
    if (r.width > MIN_LABEL_W && r.height > MIN_LABEL_H) {
      ctx.fillStyle = "rgba(255,255,255,0.92)";
      ctx.font = "13px system-ui, sans-serif";
      ctx.fillText(r.text, r.x + PAD, r.y + PAD);
    }
  }
}
Step 3

Word cloud vs. treemap — output comparison

layout() (word cloud)treemap()
Output shape { text, x, y, rotation, fontSize } { text, value, x, y, width, height }
x, y mean Center of word bbox Top-left corner of tile
Items that don't fit Omitted from result array All items tiled (weights > 0)
Overlap None (bitmap collision mask) None (partition of canvas)
Typical draw call fillText after translate + rotate fillRect (+ optional fillText)
Step 4

Complete page

main.js
import { treemap }      from "./js/index.js";
import { drawTreemap }  from "./render-treemap.js";

const canvas = document.getElementById("map");
const ctx    = canvas.getContext("2d");

const items = [
  ["WebAssembly", 100], ["Rust", 78], ["layout", 60],
  ["engine", 52], ["treemap", 44], ["bitmap", 38],
].map(([text, weight]) => ({ text, weight }));

async function render() {
  const w = Math.floor(canvas.getBoundingClientRect().width) || 800;
  const h = Math.round(w * 0.55);
  canvas.width = w;
  canvas.height = h;

  const rects = await treemap(items, { width: w, height: h });
  drawTreemap(ctx, rects, ["#1f6feb", "#238636", "#a371f7", "#db6d28"]);
}

render();
window.addEventListener("resize", render);
Hover / drill-down. Treemap rects are plain geometry — hit-testing is a simple point-in-rectangle check on mouse coordinates. For nested hierarchies you'd pass tree-structured input in your app layer; this engine flattens one level of siblings per call.

Try it yourself

Twenty technology terms, fixed weights. Colors cycle through a palette; labels only appear when a tile is large enough to read comfortably.

live treemap