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,:closekeys
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 to0- 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. Defaultnil, auto-detected from the data with 5% padding on each side:max_value— explicit Y maximum. Defaultnil, auto-detected from the data with 5% padding on each side. When the resolved minimum equals the maximum the range is widened by0.001either way:color—{r, g, b}primary colour for single-series charts. Defaultnil, 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:colorfor single-series bar/scatter/area. Default[]:show_axes—boolean/0, draw axis lines and the zero-line when the range spans zero. Defaultfalse. Reserves two rows and a labelled left column:show_labels—boolean/0, draw axis tick labels. Defaultfalse:title—String.t/0displayed on a row above the chart. Defaultnil; an empty string is treated the same asnil: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_barand:range_baronly, and every other chart type falls back to the vertical renderer:bar_labels— list of strings labelling each bar or group, shown whenshow_labels: true. Default[]:show_values—boolean/0, show the numeric value beside each bar. Defaultfalse:fill_opacity— brightness of the area fill body relative to the series colour,0.0(invisible) to1.0(same as edge). Default0.6:animated—boolean/0, animate new data points. Defaultfalse:animation_speed— milliseconds per animation frame. Default100:width— chart width in columns. The element defaults it to the rect the parent allocated, but rendering always usesrect.width, so this only reaches the state:height— chart height in rows. The element defaults it to the rect the parent allocated;mount/1on its own defaults it to1:max_data_points— cap on retained points; older points are dropped when exceeded. Defaultnil, no cap:bar_gap— blank columns between bars. Default0:area_fill— which side of the line the area body fills::below(default) or:invertedto fill from the line up to the top edge:show_baseline—boolean/0, draw the zero baseline row on stacked braille areas. Defaultfalse:zero_center— how a stacked range spanning zero is scaled::symmetric(default, equal extent either side of zero) or:independentto let the positive and negative extents differ. Ignored when the data does not cross zero:smooth—boolean/0, interpolate between points on line charts. Defaultfalse:line_thickness— line width in pixels for line and area charts. Default1:connect_lines—boolean/0, join consecutive points with line segments. Defaultfalse:raw_data—boolean/0, plot every point instead of downsampling to the viewport width with LTTB. Defaultfalse:precision— decimal places in the Y-axis tick labels. Default3. Read bymount/1andupdate/2only; thechart/2element does not forward it:style—map/0of style properties. Default%{}:class— theme class atom or list of them, reachingmount/1as: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/:sixelto force a protocol. Overrides the runtimemode:config, but theDRAFTER_MODEenv var still forces over it. When unset, the global mode applies:DRAFTER_MODE, then themode:run option (Drafter.run(App, mode: :pixel)/Drafter.render_mode/1), then:text.:image_throttle— for:pixelcharts, the minimum gap between image regenerations, in Drafter's timing units:{n, :fps},{n, :ms},{n, :tick}(everynrender 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:pixelcharts, pixels generated per terminal cell column (rows use2×); default4. 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)
Summary
Functions
Replaces :data with everything the widget's data channel has buffered.
The registry tag for this widget.
Turns the {:chart, data, opts} element into a props map for mount/1.
Pans both axes as the pointer moves with a button held.
Callback implementation for Drafter.Widget.handle_event/2.
Pans the chart.
Ends the current drag gesture, clearing :drag_last_x and setting
:dragging_scrollbar. Always returns {:ok, state} and consumes the event.
Whether this chart is drawing a transmitted image rather than cells.
Builds the chart state from props.
opts[:height], or 5 when it is absent.
Draws the chart into rect, padded or truncated to exactly rect.height strips.
Draws several series into one set of strips. See the multi-series chart renderer.
Draws a multi-row bar chart from a flat value list. See the bar chart renderer.
Callback implementation for Drafter.Widget.unmount/1.
Folds fresh props into state.
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.
Types
@type chart_type() ::
:line
| :step
| :bar
| :clustered_bar
| :stacked_bar
| :range_bar
| :candlestick
| :area
| :scatter
| :histogram
| :heatmap
| :bubble
| :braille
| :braille_area
@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 }
Functions
@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.
@spec component_tag() :: :chart
The registry tag for this widget.
iex> Drafter.Widget.Chart.component_tag()
:chart
@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}
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}.
Callback implementation for Drafter.Widget.handle_event/2.
@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
Ends the current drag gesture, clearing :drag_last_x and setting
:dragging_scrollbar. Always returns {:ok, state} and consumes the event.
Whether this chart is drawing a transmitted image rather than cells.
@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}
@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
@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/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.
Draws several series into one set of strips. See the multi-series chart renderer.
Draws a multi-row bar chart from a flat value list. See the bar chart renderer.
Callback implementation for Drafter.Widget.unmount/1.
@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
@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.