Drafter.Widget.Sparkline (drafter v0.3.2)

Copy Markdown View Source

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

Summary

Functions

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

The component tag this widget registers under.

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

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

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

Builds the widget state from props.

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

Draws the sparkline into rect.

Turns data into {bar_characters, normalized_values}.

Callback implementation for Drafter.Widget.unmount/1.

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

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

Types

rgb()

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

t()

@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()
}

Functions

apply_data_buffer(state, buffer, rect)

@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()

@spec component_tag() :: :sparkline

The component tag this widget registers under.

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

focused(state)

from_component_opts(data, opts)

@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(event, state)

@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/0. The sparkline is not focusable.

interpolate_color(arg1, arg2, factor)

@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(props)

@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(args, opts)

@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(state, rect)

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(data, min_val, max_val, width)

@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(state)

Callback implementation for Drafter.Widget.unmount/1.

update(props, state)

@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(mount_props, existing_state, opts)

@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