# `Text.WordCloud.Layout`
[🔗](https://github.com/kipcole9/text/blob/v0.5.0/lib/word_cloud/layout.ex#L1)

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

# `metrics`

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

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

# `placement`

```elixir
@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`

```elixir
@type term_entry() :: %{
  :term =&gt; String.t(),
  :weight =&gt; float(),
  :count =&gt; pos_integer(),
  :kind =&gt; :word | :phrase,
  optional(any()) =&gt; any()
}
```

Term entries as returned by `Text.WordCloud.terms/2`.

# `layout`

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

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

### Arguments

* `terms` — the output of `Text.WordCloud.terms/2` (or any list of
  `%{term, weight, count, kind}` maps).

### 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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
