Text.WordCloud.Layout (Text v0.5.0)

Copy Markdown View Source

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

metrics()

@type metrics() :: {width :: number(), height :: number()}

Width/height pixel metrics for a term at a given font size.

placement()

@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.

term_entry()

@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

layout(terms, options \\ [])

@spec layout(
  [term_entry()],
  keyword()
) :: [placement()]

Lays out a list of weighted terms onto a width × height canvas.

Arguments

Options

  • :width — canvas width in pixels. Default 800.

  • :height — canvas height in pixels. Default 600.

  • :font_size_range{min, max} font size, in pixels. The top weight (1.0) maps to the max; weight 0.0 maps to the min. Default {12, 96}.

  • :rotations — list of rotation angles (degrees) to choose from per term, or one of the atoms :radial or :spiral. Default [0] (horizontal only). Common list alternatives are [0, 90] or [-30, 0, 30].

    • :radial orients each word along the angle from canvas centre to its placement (sunburst style — words point outward like spokes).

    • :spiral orients 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. Default 2.

  • :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. Default 0.1.

  • :max_spiral_radius — cap on spiral radius (pixels) before declaring "no fit". Default max(width, height).

Returns

  • A list of placement maps with x, y, width, height, font_size, and rotation filled 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