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.
(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):
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.
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.
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 }
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.
/** 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); } } }
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) |
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);
Twenty technology terms, fixed weights. Colors cycle through a palette; labels only appear when a tile is large enough to read comfortably.