Rust + WebAssembly
layout engine

Places word clouds and other visualizations off the main thread, scaling from 50 to 50 000+ words. Framework-agnostic — your code does the drawing.

~1 ms for 100 words (Fast mode) ~88 ms for 50 000 words (Fast mode) ~120× faster than d3-cloud Web Worker ready No bundler needed

Live demo

Runs in-browser via WebAssembly — no server-side computation. Read the Canvas guide →

Canvas

Press Esc or click Exit fullscreen to return to the demo card.

The card limits canvas size, so fewer words may fit than the engine can place. This demo sets minFontSize / maxFontSize per preset (50 words: 16–88px, 500: 10–52px, 5 000: 6–16px) — word weight maps within that range; you can pass any values you want. Open Fullscreen to lay out on the full viewport and see the maximum number of words placed.

Switch the font. Measuring is on by default and needs nothing from you — the wrapper asks the browser to measure each word in whatever font you are rendering with, using the same measureText the browser will use to draw it. Untick Measure the font to fall back to the engine's built-in guess at a generic sans-serif: the further your font is from that guess, the more the drawn glyphs overrun their reserved space, and because words are center-anchored you see it as slight overlap on a word's first and last letters. Try it with Georgia or Impact, where the guess is furthest off.

your-app.js
// One async call. No bundler needed.
import { layout } from "./js/index.js";

const items = [
  { text: "WebAssembly", weight: 100 },
  { text: "Rust",        weight: 80  },
  { text: "layout",      weight: 40  },
];

const placements = await layout(items, {
  mode:        "pretty",  // "pretty" | "balanced" | "fast"
  width:       960,
  height:      600,
  minFontSize: 14,
  maxFontSize: 88,
  padding:     2,
  // The font you render with. Words get measured in it via canvas,
  // so footprints match the drawn glyphs. Defaults to sans-serif.
  font:        "Inter, sans-serif",
});
// → [{ text, x, y, rotation, fontSize, baselineOffset }, ...]
// x/y are bbox center — render with center anchoring.

const ctx = canvas.getContext("2d");
ctx.textAlign    = "center";
ctx.textBaseline = "middle";
for (const p of placements) {
  ctx.save();
  ctx.translate(p.x, p.y);
  if (p.rotation) ctx.rotate(p.rotation * Math.PI / 180);
  ctx.font = `${p.fontSize}px Inter, sans-serif`;
  ctx.fillText(p.text, 0, 0);
  ctx.restore();
}

Off the main thread

Layout runs in a Web Worker — the spinner below never stops, proving the UI stays responsive. Read the Web Worker guide →

Web Worker

Press Esc or click Exit fullscreen to return to the demo card.

idle

The card limits canvas size, so fewer words may fit than the engine can place. This demo sets minFontSize / maxFontSize per preset (50 words: 16–88px, 500: 10–52px, 5 000: 6–16px) — word weight maps within that range; you can pass any values you want. Open Fullscreen to lay out on the full viewport and see the maximum number of words placed.

worker-app.js
// Layout runs on a dedicated Worker.
// Concurrent calls are safe — request IDs are matched.
import { createLayoutWorker } from "./js/layout-worker.js";

const layoutWorker = createLayoutWorker();

const placements = await layoutWorker.layout(items, {
  mode:   "pretty",
  width:  960,
  height: 600,
});

// Render exactly as before — x/y are still bbox centers.
// Call layoutWorker.terminate() when you're done with the worker.
layoutWorker.terminate();

Treemap — same engine, different layout

The LayoutEngine trait is generic. Treemap is a fully-implemented strategy with its own output type — areas proportional to weight, no overlaps. Read the Treemap guide →

Treemap

Press Esc or click Exit fullscreen to return to the demo card.

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

const rects = await treemap(
  [
    { text: "Rust",   weight: 78 },
    { text: "wasm",   weight: 40 },
    { text: "layout", weight: 30 },
    /* … */
  ],
  { width: 960, height: 600 },
);
// → [{ text, value, x, y, width, height }, ...]
// x/y are top-left — render with fillRect.

const ctx = canvas.getContext("2d");
for (const r of rects) {
  ctx.fillRect(r.x, r.y, r.width, r.height);
}

Performance

Host CPU, scalar kernels, release build. Identical deterministic inputs for both engines (Zipf weights, seed 1). Full methodology in BENCHMARKS.md.

ModeWords Median (ms)d3-cloud (ms)Speedup
Fast100 0.97112~120×
Pretty100 11112~10×
Fast500 2.6384~150×
Pretty500 173384~2.2×
Fast5 000 142 551~180×
Fast50 000 88——
Fairness caveat: d3-cloud measures real glyph outlines; this engine uses approximate advance-width boxes — part of d3-cloud's cost is work this engine currently skips. Both single-threaded. Parallel rasterization (~2× for Fast at 50k) is on the roadmap.

How it works

You hand the engine a list of words; it hands back coordinates. The calculation (steps 1–3) runs entirely in WebAssembly; the drawing happens outside it, in whatever renderer you choose — Canvas, SVG, D3, React, or any charting tool that can place text.

1

Give it words and weights

The input is a list of { text, weight }. A word's weight decides how big it should be — the heaviest word becomes the largest, the rest scale down from there.

2

Turn each word into a shape

Every word becomes a compact black-and-white stencil of the space it needs, built in parallel across CPU cores. The size of that stencil comes from the browser: the wrapper asks measureText how wide each word is in the font you render with, which is the same code path that will draw it, so the two cannot disagree. Where no canvas is available the engine falls back to a built-in estimate of a generic sans-serif.

3

Pack the shapes without overlap

Each word spirals outward from the center looking for a gap. To check a spot, its stencil is compared against a single map of pixels already taken — a fast bitwise test that stays quick even at tens of thousands of words. A mode tunes this: Pretty for a tight spiral look, Fast for huge counts, Balanced in between.

4

Get back coordinates to draw

The result is { x, y, rotation, fontSize, baselineOffset } for each placed word — position only, no pixels. You draw it however you like (Canvas, SVG, D3, React…). Run the whole thing in a Web Worker and the page never freezes, even for 50 000 words.

Examples

In-depth walkthroughs with annotated code, layout options explained, and a playground on each page — from Canvas and SVG rendering to text tokenization, interactivity, workers, and treemaps.

Drawing a word cloud with Canvas Weighted input → WASM layout → center-anchored fillText rendering. Layout modes, font sizing, and a live demo. Rendering with SVG Crisp scalable text, per-word DOM nodes, and CSS styling. Same placements, different renderer. Round or boxy: choosing a spiral The spiral option side by side — Archimedean's disk vs. rectangular's box-filled cloud, on identical words. Turning words sideways The rotate option side by side — all-upright vs. a mix of 0° and 90°, and how many more words fit. Interactive word clouds Hover highlights, click-to-select, and tooltips. Attach pointer events to each placed word. From raw text to word cloud Tokenize prose, count frequencies, filter stop words, and lay out the top terms. Responsive word clouds Re-run layout when a dashboard panel or viewport changes size — with a draggable demo and width presets. Running layout in a Web Worker Keep the UI responsive during large clouds. Client/worker protocol, concurrent requests, and when to move off-thread. Building a treemap layout Same weighted input, rectangle output. Squarified tiling, top-left coordinates, and label sizing.