# `Drafter.Widget.Sparkline`
[🔗](https://github.com/jaman/drafter/blob/main/lib/drafter/widget/sparkline.ex#L1)

Renders a compact sparkline chart using Unicode block characters.

Each data point maps to one of the nine bar heights `▁▂▃▄▅▆▇█` (or a blank
for the minimum). When `:min_color` and `:max_color` differ, individual bars
are coloured by linear interpolation between those two colours based on their
normalised value. An optional summary appends `min:X max:Y avg:Z` text to the
right of the bars.

When `orientation: :horizontal` is set, each data point becomes one row and
bars grow left-to-right using left-aligned eighth-block characters.

## Component tag

Tag `:sparkline`, built by `Drafter.App` as `{:sparkline, data, opts}`:

    sparkline(data, opts)

The positional argument becomes `:data` when it is a non-empty list that is not
a keyword list; otherwise `:data` is read from `opts`. Both
`sparkline(values, summary: true)` and `sparkline(data: values, summary: true)`
are therefore valid.

## Options

  * `:data` - `[number()]` to plot. Default `[]`. Only the first `width` values
    of a vertical sparkline and the first `rect.height` values of a horizontal
    one are drawn.
  * `:min_value` - `t:number/0` explicit minimum for scaling. Default:
    `Enum.min(data)`, or `0` when the data is empty. A `nil` value falls back to
    the same default.
  * `:max_value` - `t:number/0` explicit maximum for scaling. Default:
    `Enum.max(data)`, or `0` when the data is empty. A `nil` value falls back to
    the same default.
  * `:min_color` - `{r, g, b}` colour for the lowest bars. Default `nil`, which
    uses the sparkline's computed theme colour, itself falling back to
    `{100, 200, 100}`.
  * `:max_color` - `{r, g, b}` colour for the highest bars. Default `nil`, with
    the same fallback as `:min_color`. Equal min and max colours make every bar
    that colour.
  * `:color` - `{r, g, b}`. Default `nil`. Held on the state and never read by
    `render/2`, which takes its base colour from the theme.
  * `:summary` - `t:boolean/0`, append `min:X max:Y avg:Z` at the right edge.
    Default `false`. Reserves 20 columns of the rect for the text. Only drawn for
    a vertical sparkline, though `apply_data_buffer/3` reserves the same 20
    columns either way.
  * `:orientation` - `:vertical | :horizontal`. Default `:vertical`; any value
    other than `:horizontal` renders vertically.
  * `:style` - `t:map/0` of style overrides passed to the theme computation.
    Default `%{}`.
  * `:class` - theme class atom or list of them, normalised by
    `Drafter.Style.normalize_classes/1` and reaching `mount/1` as `:classes`.
    Default `[]`.
  * `:height` - `t:pos_integer/0` read only by `preferred_height/2`, never by
    `mount/1`. Default `3`.

Every option except `:height` is live-updatable: `update_props_from_mount/3`
passes the full mount props through. Supplying `:data` without `:min_value` or
`:max_value` rescales the sparkline to the new data.

## Usage

    sparkline(data: [1, 3, 2, 8, 5, 9, 4], summary: true)
    sparkline(data: readings, min_color: {100, 200, 100}, max_color: {255, 50, 50})
    sparkline(data: readings, orientation: :horizontal)

# `rgb`

```elixir
@type rgb() :: {0..255, 0..255, 0..255}
```

# `t`

```elixir
@type t() :: %Drafter.Widget.Sparkline{
  app_module: module() | nil,
  classes: [atom()],
  color: rgb() | nil,
  data: [number()],
  max_color: rgb() | nil,
  max_value: number(),
  min_color: rgb() | nil,
  min_value: number(),
  orientation: :vertical | :horizontal,
  style: map(),
  summary: boolean()
}
```

# `apply_data_buffer`

```elixir
@spec apply_data_buffer(t(), Drafter.RingBuffer.t(), Drafter.Widget.rect()) :: t()
```

Replaces `:data` with the newest entries of a `Drafter.RingBuffer` and rescales
`:min_value` and `:max_value` to them.

Takes the last `rect.width` values, or `rect.width - 20` when `:summary` is set,
with a floor of one value. Returns `state` unchanged for an empty buffer, which
is the only case where an explicit `:min_value` or `:max_value` survives.

# `component_tag`

```elixir
@spec component_tag() :: :sparkline
```

The component tag this widget registers under.

    iex> Drafter.Widget.Sparkline.component_tag()
    :sparkline

# `focused`

# `from_component_opts`

```elixir
@spec from_component_opts(
  term(),
  keyword()
) :: Drafter.Widget.props()
```

Builds the props map for a `{:sparkline, data, opts}` element.

`data` becomes `:data` when it is a non-empty list that is not a keyword list;
otherwise `opts[:data]` is used, defaulting to `[]`. `:class` is normalised into
`:classes` and `:__app_module__` becomes `:app_module`.

    iex> props = Drafter.Widget.Sparkline.from_component_opts([1, 3, 2], summary: true)
    iex> {props.data, props.summary, props.min_value}
    {[1, 3, 2], true, nil}

    iex> props = Drafter.Widget.Sparkline.from_component_opts([data: [4, 5]], [])
    iex> props.data
    []

    iex> Drafter.Widget.Sparkline.from_component_opts(nil, data: [4, 5]).data
    [4, 5]

# `handle_event`

```elixir
@spec handle_event(Drafter.Event.t(), t() | Drafter.Widget.props()) :: {:noreply, t()}
```

Ignores every event and returns `{:noreply, state}`.

A plain props map is passed through `mount/1` first, so the returned state is
always a `t:t/0`. The sparkline is not focusable.

# `interpolate_color`

```elixir
@spec interpolate_color(rgb(), rgb(), float()) :: rgb()
```

Blends two `{r, g, b}` colours, rounding each channel.

`factor` must be a float — an integer raises `FunctionClauseError`. `0.0` returns
the first colour and `1.0` the second; values outside `0.0..1.0` extrapolate.

    iex> Drafter.Widget.Sparkline.interpolate_color({0, 0, 0}, {200, 100, 50}, 0.5)
    {100, 50, 25}

    iex> Drafter.Widget.Sparkline.interpolate_color({10, 20, 30}, {200, 100, 50}, 0.0)
    {10, 20, 30}

# `mount`

```elixir
@spec mount(Drafter.Widget.props()) :: t()
```

Builds the widget state from `props`.

`:min_value` and `:max_value` are taken from `props` when present and not `nil`,
and otherwise from the data — `Enum.min/1` and `Enum.max/1`, or `0` and `0` for
empty data.

    iex> state = Drafter.Widget.Sparkline.mount(%{data: [1, 3, 2, 8]})
    iex> {state.min_value, state.max_value, state.summary, state.orientation}
    {1, 8, false, :vertical}

    iex> state = Drafter.Widget.Sparkline.mount(%{})
    iex> {state.data, state.min_value, state.max_value}
    {[], 0, 0}

    iex> Drafter.Widget.Sparkline.mount(%{data: [1, 2], max_value: 100}).max_value
    100

# `preferred_height`

```elixir
@spec preferred_height(
  term(),
  keyword()
) :: pos_integer()
```

The number of rows the element asks for: `opts[:height]`, default `3`.

    iex> Drafter.Widget.Sparkline.preferred_height(nil, [])
    3

    iex> Drafter.Widget.Sparkline.preferred_height([1, 2, 3], height: 8)
    8

# `render`

```elixir
@spec render(t() | Drafter.Widget.props(), Drafter.Widget.rect()) :: [
  Drafter.Draw.Strip.t()
]
```

Draws the sparkline into `rect`.

`state` may be a plain props map, in which case it is passed through `mount/1`
first. A vertical sparkline returns a single strip. A horizontal one returns one
strip per data point, capped at `rect.height`, so it returns `[]` for empty data.

# `render_sparkline_with_values`

```elixir
@spec render_sparkline_with_values([number()], number(), number(), non_neg_integer()) ::
  {String.t(), [float()]}
```

Turns `data` into `{bar_characters, normalized_values}`.

Takes at most `width` values, normalises each into `0.0..1.0` against `min_val`
and `max_val`, and picks the matching character from the current skin's vertical
sparkline levels. Empty data returns `width` spaces and `width` values of `0.5`.
The two elements of the result always have the same length, which is
`min(length(data), width)` for non-empty data.

    iex> Drafter.Widget.Sparkline.render_sparkline_with_values([], 0, 0, 3)
    {"   ", [0.5, 0.5, 0.5]}

    iex> {chars, values} = Drafter.Widget.Sparkline.render_sparkline_with_values([1, 5, 10], 1, 10, 2)
    iex> {String.length(chars), values}
    {2, [0.0, 0.4444444444444444]}

# `unmount`

# `update`

```elixir
@spec update(Drafter.Widget.props(), t()) :: t()
```

Replaces the state fields named in `props`, keeping the current value for any key
that is absent.

New `:data` rescales `:min_value` and `:max_value` to it unless `props` carries a
non-`nil` `:min_value` or `:max_value` of its own. Empty new data keeps the
existing scale.

    iex> state = Drafter.Widget.Sparkline.mount(%{data: [1, 2, 3]})
    iex> updated = Drafter.Widget.Sparkline.update(%{data: [10, 20]}, state)
    iex> {updated.data, updated.min_value, updated.max_value}
    {[10, 20], 10, 20}

    iex> state = Drafter.Widget.Sparkline.mount(%{data: [1, 2, 3]})
    iex> updated = Drafter.Widget.Sparkline.update(%{data: [10, 20], max_value: 50}, state)
    iex> {updated.min_value, updated.max_value}
    {10, 50}

# `update_props_from_mount`

```elixir
@spec update_props_from_mount(Drafter.Widget.props(), term(), keyword()) ::
  Drafter.Widget.props()
```

Passes the mount props through unchanged, so every option is live-updatable
through the component tree.

    iex> props = Drafter.Widget.Sparkline.from_component_opts([1, 2], [])
    iex> Drafter.Widget.Sparkline.update_props_from_mount(props, %{}, []) == props
    true

---

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