Wordle-style spiral layout for word-cloud rendering.
Takes a list of weighted terms (the output of Text.WordCloud.terms/2)
and produces a list of placements — (x, y, width, height, font_size, rotation) tuples — that can be handed to any rendering surface (SVG,
Canvas, PDF, …). The layout itself is renderer-agnostic: this module
produces no markup.
Algorithm
Higher-weight terms are placed first, starting at the canvas centre. Each subsequent term sweeps outward along an Archimedean spiral; at each sample point, the candidate bounding box is tested against every previously-placed box. The first non-colliding position wins. Terms that find no fit within the canvas bounds are dropped from the output.
This is the same recipe Jonathan Feinberg used in the original Wordle implementation (and that d3-cloud follows in the browser). Pure-BEAM bounding-box collision detection scales fine for the typical 100–300-term clouds; for larger inputs an occupancy-grid refinement would be the natural follow-up.
Font metrics
Word width and height depend on the rendering font. We can't measure
that from inside Elixir without a font-rendering library, so the
default metric callback uses a coarse monospace approximation
(character width ≈ 0.6 × font size; height ≈ 1.2 × font size). For
pixel-accurate layout, pass a :font_metrics callback that consults
your real font (e.g. via Cairo, FreeType, or the browser's Canvas
TextMetrics API in the consumer's runtime).
Summary
Types
Width/height pixel metrics for a term at a given font size.
A placement map ready for rendering.
Term entries as returned by Text.WordCloud.terms/2.
Functions
Lays out a list of weighted terms onto a width × height canvas.
Types
Width/height pixel metrics for a term at a given font size.
@type placement() :: %{ term: String.t(), weight: float(), count: pos_integer(), kind: :word | :phrase, x: float(), y: float(), width: float(), height: float(), font_size: float(), rotation: number() }
A placement map ready for rendering.
@type term_entry() :: %{ :term => String.t(), :weight => float(), :count => pos_integer(), :kind => :word | :phrase, optional(any()) => any() }
Term entries as returned by Text.WordCloud.terms/2.
Functions
@spec layout( [term_entry()], keyword() ) :: [placement()]
Lays out a list of weighted terms onto a width × height canvas.
Arguments
terms— the output ofText.WordCloud.terms/2(or any list of%{term, weight, count, kind}maps).
Options
:width— canvas width in pixels. Default800.:height— canvas height in pixels. Default600.:font_size_range—{min, max}font size, in pixels. The top weight (1.0) maps to the max; weight0.0maps to the min. Default{12, 96}.:rotations— list of rotation angles (degrees) to choose from per term, or one of the atoms:radialor:spiral. Default[0](horizontal only). Common list alternatives are[0, 90]or[-30, 0, 30].:radialorients each word along the angle from canvas centre to its placement (sunburst style — words point outward like spokes).:spiralorients each word tangent to the radial spoke (vortex style — words flow around concentric arcs, the way the eye reads a logarithmic-spiral logo).
Both modes clamp final rotations to
[-90°, 90°]so words always read left-to-right rather than upside-down.:padding— pixel padding added to every bounding box for collision testing. Default2.:font_metrics—(term, font_size) -> {width, height}callback. Default uses a monospace approximation (character width =0.6 × font_size, height =1.2 × font_size).:spiral_step— angular increment of the Archimedean spiral sampling. Smaller = more thorough but slower. Default0.1.:max_spiral_radius— cap on spiral radius (pixels) before declaring "no fit". Defaultmax(width, height).
Returns
- A list of placement maps with
x,y,width,height,font_size, androtationfilled in. Sorted in placement order (highest weight first). Terms that did not fit are omitted.
Examples
iex> terms = [%{term: "hello", weight: 1.0, count: 5, kind: :word}]
iex> [p] = Text.WordCloud.Layout.layout(terms, width: 800, height: 600)
iex> p.term
"hello"
iex> p.font_size > 0
true