# `Rover.Heatmap`
[🔗](https://github.com/nseaSeb/rover/blob/v0.4.0/lib/rover/heatmap.ex#L1)

Density, as a heat field rather than as pins.

Where five hundred markers are a wall of overlapping icons, a heatmap answers a
different question: *where is there a lot of this?* Deliveries per neighbourhood,
clients per area, incidents over a season.

    <.map id="deliveries" heatmap={@deliveries} />

A point needs only a coordinate:

    %{lat: 45.75, lon: 4.85}
    %{lat: 45.75, lon: 4.85, weight: 0.4}

## No identity, and why

`Rover.Marker` and `Rover.Shape` both insist on a stable `:id`, because both are
reconciled one feature at a time. A heatmap is not: it is an aggregate, and no
individual point is visible in the result. Requiring an id for every row of a
density query would be ceremony that buys nothing.

So heatmaps are diffed the way shapes are — by a revision computed once per
render on the server. Same list, same `rev`, no work on the client. A changed
list rebuilds the field, which is what changing a density field means anyway.

## Weights

`:weight` is **relative, from 0 to 1**, and defaults to `1`. OpenLayers saturates
anything above 1, so raw counts do not work as-is — divide by your maximum:

    max = Enum.max_by(rows, & &1.orders).orders

    <.map
      id="deliveries"
      heatmap={rows}
      heatmap_fields={[weight: fn row -> row.orders / max end]}
    />

# `point`

```elixir
@type point() :: %{lat: float(), lon: float(), weight: float()}
```

# `new_all!`

```elixir
@spec new_all!(
  Enumerable.t(),
  keyword()
) :: [point()]
```

Normalises a list of points. Nil entries, and entries without a usable
coordinate, are dropped.

Unlike markers, an unusable point is skipped rather than raised on: a density
query returning one row with a null coordinate should thin the map, not take the
page down.

## Examples

    iex> Rover.Heatmap.new_all!([%{lat: 45.75, lon: 4.85}])
    [%{lat: 45.75, lon: 4.85, weight: 1.0}]

    iex> Rover.Heatmap.new_all!([%{lat: 45.75, lon: 4.85, weight: 0.25}])
    [%{lat: 45.75, lon: 4.85, weight: 0.25}]

    iex> Rover.Heatmap.new_all!([%{lat: nil, lon: nil}, %{lat: 45.75, lon: 4.85}])
    [%{lat: 45.75, lon: 4.85, weight: 1.0}]

# `rev`

```elixir
@spec rev([point()]) :: integer()
```

The revision of a normalised point list: what the client compares to decide
whether to rebuild the field.

## Examples

    iex> points = Rover.Heatmap.new_all!([%{lat: 45.75, lon: 4.85}])
    iex> Rover.Heatmap.rev(points) == Rover.Heatmap.rev(points)
    true

# `style!`

```elixir
@spec style!(keyword()) :: map()
```

Normalises the style options into the map the JavaScript runtime reads.

## Options

  * `:radius` — point radius in pixels. Defaults to `8`.
  * `:blur` — blur radius in pixels. Defaults to `15`.
  * `:opacity` — layer opacity, 0 to 1. Defaults to `1`.
  * `:gradient` — a list of CSS colours, cold to hot. Defaults to OpenLayers'.

## Examples

    iex> Rover.Heatmap.style!([])
    %{radius: 8, blur: 15, opacity: 1}

    iex> Rover.Heatmap.style!(radius: 12, gradient: ["#fff", "#f00"])
    %{radius: 12, blur: 15, opacity: 1, gradient: ["#fff", "#f00"]}

---

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