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

Renders time-series and financial data as interactive charts with multiple styles.

Supported chart types: `:line`, `:braille`, `:step`, `:area`, `:braille_area`,
`:bar`, `:clustered_bar`, `:stacked_bar`, `:range_bar`, `:scatter`, `:bubble`,
`:histogram`, `:heatmap`, and `:candlestick`. Any unrecognised value renders as
`:line`. Braille-dot rendering provides the highest resolution (two data points
per column, four per row). Quadrant-block rendering provides 2×2 pixel
resolution per cell (coarser but larger dots). Bar charts use half-block
characters for 2× vertical resolution.

Each chart type reads `:data` in its own shape:

  * `:line`, `:braille`, `:step`, `:area`, `:braille_area`, `:bar` — a list of
    numbers, or a list of such lists for multiple series
  * `:clustered_bar`, `:stacked_bar` — a list of series, each a list of numbers
  * `:range_bar` — a list of `[low, high]` pairs, one per bar
  * `:scatter` — `[x, y]` lists, `{x, y}` tuples, or weighted variants
    `[x, y, weight]` / `{x, y, weight}` where weight is a float between 0.0 and
    1.0. Higher weights produce denser braille dot clusters and brighter colors.
    A list of point-lists renders multiple series
  * `:bubble` — points carrying a magnitude, rendered as sized dots; a list of
    point-lists renders multiple series
  * `:histogram` — a flat list of raw values, binned automatically
  * `:heatmap` — a list of rows, each a list of numbers, forming a matrix
  * `:candlestick` — `[open, high, low, close]` lists or maps with `:open`,
    `:high`, `:low`, `:close` keys

## Component tag

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

    chart(data, opts)

The positional argument becomes `:data` when it is a list; otherwise `:data` is
read from `opts`. Because a bare keyword list in the first position is treated
as `opts`, both `chart(values, chart_type: :line)` and
`chart(data: values, chart_type: :line)` are valid. `:width` and `:height`
default to the rect the parent allocated.

## Negative Values

All chart types (except candlestick) support negative values natively. The Y-axis
range is derived from the data including any negative values. For charts that span
both positive and negative territory a zero-line is drawn automatically when
`show_axes: true`. Set `min_value` and `max_value` explicitly to pin a symmetric
range:

    chart(io_data, chart_type: :line, min_value: -150, max_value: 150, show_axes: true)

## Multi-Series Charts

Pass a list of series (each a list of values) to `:data` for `:line`,
`:clustered_bar`, `:stacked_bar`, and `:scatter`. Each series is rendered in its
own colour cycling through `:colors`. If `:colors` is empty a built-in palette of
six hues is used.

    chart([series_a, series_b, series_c],
      chart_type: :line,
      height: 8,
      colors: [{100, 200, 255}, {255, 150, 80}, {80, 255, 150}]
    )

For `:scatter`, multi-series data is a list of point-lists where each point-list
contains `[x, y]` pairs:

    chart([series_a_points, series_b_points], chart_type: :scatter, height: 8)

For `:range_bar`, data is a list of `[low, high]` pairs — one pair per bar:

    chart([[10, 40], [25, 65], [5, 55]], chart_type: :range_bar, height: 8)

## Bar Chart Types

  * `:bar` — classic single-row sparkline; one block-char per data point
  * `:clustered_bar` — multi-row grouped bars; each group shows one bar per
    series side by side with half-block vertical resolution
  * `:stacked_bar` — multi-row stacked bars; series accumulate from the
    baseline (supports negatives — positive series stack upward, negative
    series stack downward)
  * `:range_bar` — one bar per data point spanning a low→high range

## Keyboard Controls (when focused)

  * `←` / `→` — scroll the X-axis by 5 data points, clamped at 0 on the left
  * `↑` / `↓` — pan the Y-axis up/down by 1 unit
  * `?c` — reset the Y-axis pan offset to `0`
  * Click and drag — pan both axes simultaneously

Every other key bubbles.

## Options

  * `:data` — numeric list; list of series for multi-series types; `[low, high]`
    pairs for `:range_bar`. Default `[]`
  * `:chart_type` — `:line` (default), `:step`, `:area`, `:braille`,
    `:braille_area`, `:bar`, `:clustered_bar`, `:stacked_bar`, `:range_bar`,
    `:scatter`, `:bubble`, `:histogram`, `:heatmap`, `:candlestick`. Any other
    value renders as `:line`
  * `:pixel_style` — pixel rendering style for line, step and scatter:
    `:braille` (default) or `:quadrant`
  * `:min_value` — explicit Y minimum. Default `nil`, auto-detected from the data
    with 5% padding on each side
  * `:max_value` — explicit Y maximum. Default `nil`, auto-detected from the data
    with 5% padding on each side. When the resolved minimum equals the maximum the
    range is widened by `0.001` either way
  * `:color` — `{r, g, b}` primary colour for single-series charts. Default `nil`,
    falling back to the theme and then to `{100, 200, 255}`
  * `:colors` — list of `{r, g, b}` tuples, one per series for multi-series types;
    the first entry overrides `:color` for single-series bar/scatter/area.
    Default `[]`
  * `:show_axes` — `t:boolean/0`, draw axis lines and the zero-line when the range
    spans zero. Default `false`. Reserves two rows and a labelled left column
  * `:show_labels` — `t:boolean/0`, draw axis tick labels. Default `false`
  * `:title` — `t:String.t/0` displayed on a row above the chart. Default `nil`;
    an empty string is treated the same as `nil`
  * `:x_labels` — list of strings for X-axis tick labels. Default `[]`
  * `:y_labels` — list of strings for Y-axis tick labels. Default `[]`
  * `:orientation` — `:vertical` (default) or `:horizontal`; applies to
    `:bar`, `:clustered_bar`, `:stacked_bar` and `:range_bar` only, and every
    other chart type falls back to the vertical renderer
  * `:bar_labels` — list of strings labelling each bar or group, shown when
    `show_labels: true`. Default `[]`
  * `:show_values` — `t:boolean/0`, show the numeric value beside each bar.
    Default `false`
  * `:fill_opacity` — brightness of the area fill body relative to the series
    colour, `0.0` (invisible) to `1.0` (same as edge). Default `0.6`
  * `:animated` — `t:boolean/0`, animate new data points. Default `false`
  * `:animation_speed` — milliseconds per animation frame. Default `100`
  * `:width` — chart width in columns. The element defaults it to the rect the
    parent allocated, but rendering always uses `rect.width`, so this only reaches
    the state
  * `:height` — chart height in rows. The element defaults it to the rect the
    parent allocated; `mount/1` on its own defaults it to `1`
  * `:max_data_points` — cap on retained points; older points are dropped when
    exceeded. Default `nil`, no cap
  * `:bar_gap` — blank columns between bars. Default `0`
  * `:area_fill` — which side of the line the area body fills: `:below`
    (default) or `:inverted` to fill from the line up to the top edge
  * `:show_baseline` — `t:boolean/0`, draw the zero baseline row on stacked
    braille areas. Default `false`
  * `:zero_center` — how a stacked range spanning zero is scaled: `:symmetric`
    (default, equal extent either side of zero) or `:independent` to let the
    positive and negative extents differ. Ignored when the data does not cross
    zero
  * `:smooth` — `t:boolean/0`, interpolate between points on line charts.
    Default `false`
  * `:line_thickness` — line width in pixels for line and area charts. Default `1`
  * `:connect_lines` — `t:boolean/0`, join consecutive points with line segments.
    Default `false`
  * `:raw_data` — `t:boolean/0`, plot every point instead of downsampling to the
    viewport width with LTTB. Default `false`
  * `:precision` — decimal places in the Y-axis tick labels. Default `3`. Read by
    `mount/1` and `update/2` only; the `chart/2` element does not forward it
  * `:style` — `t:map/0` of style properties. Default `%{}`
  * `:class` — theme class atom or list of them, reaching `mount/1` as
    `:classes`. Default `[]`
  * `:renderer` — rendering backend for this chart: `:text` (ASCII/block), `:braille`
    (anti-aliased braille), `:pixel` / `:auto` (best terminal graphics available —
    kitty/iTerm2/sixel image, falling back to braille), or `:iterm2` / `:kitty` /
    `:sixel` to force a protocol. Overrides the runtime `mode:` config, but the
    `DRAFTER_MODE` env var still forces over it. When unset, the global mode applies:
    `DRAFTER_MODE`, then the `mode:` run option (`Drafter.run(App, mode: :pixel)` /
    `Drafter.render_mode/1`), then `:text`.
  * `:image_throttle` — for `:pixel` charts, the minimum gap between image regenerations,
    in Drafter's timing units: `{n, :fps}`, `{n, :ms}`, `{n, :tick}` (every `n` render
    frames — the default, frame-aligned so it can't beat against the render clock), or a
    bare integer (milliseconds). Default `{2, :tick}`. Lower means smoother animation but
    more terminal load.
  * `:image_scale` — for `:pixel` charts, pixels generated per terminal cell column
    (rows use `2×`); default `4`. Higher is sharper but produces larger images.

`update/2` re-reads every option above except `:width`, `:app_module` and
`:image_throttle`, which are fixed once the widget is mounted.

## Widget value

`Drafter.get_widget_value/1` is not implemented for this widget and returns `nil`.

## Usage

    chart(data: [1, 4, 2, 8, 5, 9, 3], chart_type: :line)
    chart(data: candles, chart_type: :candlestick, show_axes: true)
    chart(data: points, chart_type: :scatter)
    chart(data: [series_a, series_b], chart_type: :clustered_bar, height: 8)
    chart(data: [series_a, series_b], chart_type: :stacked_bar, height: 8)
    chart(data: [[lo, hi] | ...], chart_type: :range_bar, height: 8)
    chart(data: io_data, chart_type: :line, min_value: -150, max_value: 150)

# `chart_type`

```elixir
@type chart_type() ::
  :line
  | :step
  | :bar
  | :clustered_bar
  | :stacked_bar
  | :range_bar
  | :candlestick
  | :area
  | :scatter
  | :histogram
  | :heatmap
  | :bubble
  | :braille
  | :braille_area
```

# `t`

```elixir
@type t() :: %Drafter.Widget.Chart{
  _internal: map(),
  animated: boolean(),
  animation_speed: pos_integer(),
  app_module: module() | nil,
  area_fill: :below | :inverted,
  bar_gap: non_neg_integer(),
  bar_labels: [String.t()],
  chart_type: chart_type(),
  classes: [atom()],
  color: {0..255, 0..255, 0..255} | nil,
  colors: [{0..255, 0..255, 0..255}],
  data: list(),
  fill_opacity: float(),
  focused: boolean(),
  height: pos_integer(),
  max_data_points: pos_integer() | nil,
  max_value: number(),
  min_value: number(),
  orientation: :vertical | :horizontal,
  pixel_style: :braille | :quadrant,
  show_axes: boolean(),
  show_baseline: boolean(),
  show_labels: boolean(),
  show_values: boolean(),
  style: map(),
  title: String.t() | nil,
  width: pos_integer() | nil,
  x_labels: [String.t()],
  y_labels: [String.t()],
  zero_center: :symmetric | :independent
}
```

# `apply_data_buffer`

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

Replaces `:data` with everything the widget's data channel has buffered.

The buffer contents become the whole data set, they are not appended. An empty
buffer leaves the state alone. The Y range is not recomputed here, so it keeps the
bounds from the last `mount/1` or `update/2`.

# `component_tag`

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

The registry tag for this widget.

    iex> Drafter.Widget.Chart.component_tag()
    :chart

# `focused`

# `from_component_opts`

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

Turns the `{:chart, data, opts}` element into a props map for `mount/1`.

`data` is used as `:data` when it is a list, and otherwise `opts[:data]` is used,
which is what lets `chart(chart_type: :line, data: values)` work. `:width` and
`:height` default to the parent rect passed as `:__rect__`, itself defaulting to
`%{width: 80, height: 20}`. `:class` is normalised into `:classes`,
`:__app_module__` becomes `:app_module`, and a `:_render_timestamp` is stamped
from the monotonic clock.

`:precision` is not forwarded; pass it directly to `mount/1` or `update/2`.

    iex> props = Drafter.Widget.Chart.from_component_opts([1, 2, 3], chart_type: :bar)
    iex> {props.data, props.chart_type, props.width, props.height, props.bar_gap}
    {[1, 2, 3], :bar, 80, 20, 0}

    iex> props = Drafter.Widget.Chart.from_component_opts(nil, data: [4, 5], height: 6)
    iex> {props.data, props.height, props.fill_opacity, props.zero_center}
    {[4, 5], 6, 0.6, :symmetric}

# `handle_drag`

```elixir
@spec handle_drag(integer(), integer(), t()) :: {:ok, t()}
```

Pans both axes as the pointer moves with a button held.

The first drag event after a press only records the anchor point and leaves the
offsets alone. Subsequent events move the X scroll offset by the leftward pointer
delta, clamped at `0`, and the Y offset by the downward delta. Always returns
`{:ok, state}`.

# `handle_event`

# `handle_key`

```elixir
@spec handle_key(Drafter.Widget.key() | integer(), t()) :: {:ok, t()} | {:bubble, t()}
```

Pans the chart.

`:left`/`:ArrowLeft` and `:right`/`:ArrowRight` move the X scroll offset by 5 data
points, clamped at `0` on the left. `:up`/`:ArrowUp` and `:down`/`:ArrowDown` move
the Y offset by one unit, unclamped. The codepoint `?c` resets the Y offset to
`0`. All of those return `{:ok, state}`; every other key returns
`{:bubble, state}`.

    iex> c = Drafter.Widget.Chart.mount(%{data: [1, 2, 3]})
    iex> {:ok, scrolled} = Drafter.Widget.Chart.handle_key(:right, c)
    iex> scrolled._internal.scroll_offset
    5

    iex> c = Drafter.Widget.Chart.mount(%{data: [1, 2, 3]})
    iex> {:ok, clamped} = Drafter.Widget.Chart.handle_key(:left, c)
    iex> clamped._internal.scroll_offset
    0

    iex> c = Drafter.Widget.Chart.mount(%{data: [1, 2, 3]})
    iex> {:ok, panned} = Drafter.Widget.Chart.handle_key(:up, c)
    iex> {:ok, reset} = Drafter.Widget.Chart.handle_key(?c, panned)
    iex> {panned._internal.y_offset, reset._internal.y_offset}
    {1, 0}

    iex> c = Drafter.Widget.Chart.mount(%{data: [1, 2, 3]})
    iex> Drafter.Widget.Chart.handle_key(:enter, c) |> elem(0)
    :bubble

# `handle_mouse_up`

```elixir
@spec handle_mouse_up(integer(), integer(), t()) :: {:ok, t()}
```

Ends the current drag gesture, clearing `:drag_last_x` and setting
`:dragging_scrollbar`. Always returns `{:ok, state}` and consumes the event.

# `image_active?`

```elixir
@spec image_active?(t()) :: boolean()
```

Whether this chart is drawing a transmitted image rather than cells.

# `mount`

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

Builds the chart state from `props`.

Resolves the Y range from `:min_value`/`:max_value` and the data, seeds the
candlestick live candle, and stores the scroll and drag state under `:_internal`.
`:height` defaults to `1` here; the `chart/2` element supplies the parent rect's
height instead.

    iex> c = Drafter.Widget.Chart.mount(%{data: [1, 2, 3], chart_type: :bar})
    iex> {c.chart_type, c.data, c.height, c.bar_gap, c.show_axes}
    {:bar, [1, 2, 3], 1, 0, false}

    iex> c = Drafter.Widget.Chart.mount(%{data: [0, 100]})
    iex> {c.min_value, c.max_value}
    {-5.0, 105.0}

    iex> c = Drafter.Widget.Chart.mount(%{})
    iex> {c.min_value, c.max_value, c.chart_type, c.pixel_style}
    {-5.0, 105.0, :line, :braille}

# `preferred_height`

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

`opts[:height]`, or `5` when it is absent.

    iex> Drafter.Widget.Chart.preferred_height([1, 2], [])
    5

    iex> Drafter.Widget.Chart.preferred_height([1, 2], height: 12)
    12

# `render`

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

Draws the chart into `rect`, padded or truncated to exactly `rect.height` strips.

Returns `[]` when `rect.width` is `0` or less. Accepts either a `t:t/0` or a raw
props map, which is mounted first. The renderer resolved from `:renderer` and the
chart type decides the output: a `:pixel` chart emits blank strips, because the
image itself is emitted separately by the widget server; a `:braille` chart emits
braille strips, falling back to the text renderer when the data cannot be drawn
that way; a `:text` chart draws blocks and box characters, adding the axes and the
title row when those options are on.

# `render_multi_series`

Draws several series into one set of strips. See the multi-series chart renderer.

# `render_tall_bar_chart`

Draws a multi-row bar chart from a flat value list. See the bar chart renderer.

# `unmount`

# `update`

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

Folds fresh props into `state`.

The Y range is recomputed only when the data hash changes or when `props` carries
a numeric `:min_value` or `:max_value`; otherwise the current bounds are kept.
`:width` and `:app_module` are not re-read and keep their mounted values, and
`:area_fill` falls back to the current value on any falsy prop rather than only on
a missing key. Scroll offset, drag anchors and Y pan offset survive untouched.

    iex> c = Drafter.Widget.Chart.mount(%{data: [1, 2, 3]})
    iex> updated = Drafter.Widget.Chart.update(%{data: [10, 20], chart_type: :bar}, c)
    iex> {updated.data, updated.chart_type}
    {[10, 20], :bar}

    iex> c = Drafter.Widget.Chart.mount(%{data: [1, 2, 3], width: 40})
    iex> Drafter.Widget.Chart.update(%{width: 99}, c).width
    40

# `update_props_from_mount`

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

Returns `mount_props`, re-stamping `:_render_timestamp` from the monotonic clock
when the chart resolves to the text renderer so that animation keeps advancing.
Pixel and braille charts get the props through unchanged.

---

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