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.
Every chart is drawn once as shapes and then painted at the resolution of the
renderer in use (see :renderer): a terminal image at the terminal's own pixels
per cell, braille at two by four dots per cell, or block characters of a
FrenchCurve.Backend.Cells glyph set. Axis, bar and value labels are drawn as
text around and over the plot.
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)On :line, :braille, :step, :area and :braille_area charts a nil in a
series is a gap: nothing is drawn at that position and the series resumes after
it, so series sampled on a shared time grid stay aligned. Bar charts draw a gap
as zero.
chart([[nil, nil, 3, 4, 6], [1, 2, 2, 3, 2]], chart_type: :line, 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— one bar per data point; a list of series draws as:clustered_bar:clustered_bar— grouped bars; each group shows one bar per series side by side: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 a tenth of its span per press?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— how a chart whose renderer resolves to:textdraws::braille(default) or:quadrant, the:quadblock characters: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 after each bar of a horizontal bar chart. Defaultfalse:fill_opacity— how strongly an area's fill shows its series colour over the chart background,0.0(background) to1.0(the series colour). Default0.6:animated—boolean/0. A:line,:areaor:stepchart of one series rotates its points by one every:animation_speedmilliseconds, in every renderer. 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),:independentto let the positive and negative extents differ, each half scaled on its own, or:linear, one scale from the lowest to the highest value with zero where it falls. Ignored when the data does not cross zero:smooth—boolean/0, interpolate between points on line charts. Defaultfalse:series_names— list of names, one per series, shown in the value readout. Default[], which names themseries 1,series 2, …:readout_format— one-arity function turning a value into the text the readout shows. Defaultnil, two decimals:readout_sort—:none(default),:ascor:desc, the order of series in the readout:stacked—boolean/0, on:braille_areacharts stack each series on the ones before it;falsedraws every series from the baseline, later series over earlier ones. Defaulttrue:line_thickness— line width for line and area charts, in braille dots; an image scales it to the terminal's cell height. Default1:connect_lines—boolean/0, join consecutive points of a:lineor:braillechart with line segments;falsedraws the points alone. Defaulttrue: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(braille, or:quadblocks withpixel_style: :quadrant),:braille,:half/:quad/:sextant/:octant(block characters of thatFrenchCurve.Backend.Cellsset, two colours per cell),: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_scale— for:pixelcharts on a terminal that has not reported its cell size, pixels drawn per cell column (rows use2×); default4. When the terminal reports its cell size (see:cell_sizeinDrafter.App), images are drawn at that size and this is unused
update/2 re-reads every option above except :width and :app_module, which are
fixed once the widget is mounted.
A :pixel chart's image is drawn for the frame that shows it and written to the
terminal with that frame's text, so the chart and the cells around it always change
together.
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.
Moves the value readout cursor to the cell x, y as the pointer passes over the
chart.
Pans the chart.
Ends the current drag gesture, clearing :drag_last_x and setting
:dragging_scrollbar. A release that ends no drag is a click, and moves the value
readout cursor to column x. Always returns {:ok, state} and consumes the event.
Whether this chart is drawing a transmitted image rather than cells.
The chart as a Drafter.Render.Picture filling the plot area of rect, or nil
when the chart does not resolve to the pixel renderer or cannot be drawn.
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.
Callback implementation for Drafter.Widget.unmount/1.
Folds fresh props into state.
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 | :linear }
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 :_render_timestamp is stamped
from the monotonic clock for an :animated chart and is 0 otherwise, so a chart
that is not animated keeps the same props while its data does not change.
: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 as handle_key/2 clamps it, and the Y offset by the downward
delta. Always returns {:ok, state}.
Callback implementation for Drafter.Widget.handle_event/2.
Moves the value readout cursor to the cell x, y as the pointer passes over the
chart.
On :line, :braille, :step, :area, :braille_area and :stacked_area
charts the point under the cursor column is read; on :bar, :clustered_bar,
:stacked_bar and :range_bar charts the bar or group under it, or under the
cursor row for horizontal bars. The cursor column is highlighted and a panel
beside it, to the right when it fits and otherwise to the left, lists the label
(the :bar_labels entry for a bar chart, else the :x_labels entry, else
#index) and each series' colour, name (:series_names) and value
(:readout_format; a range bar shows low – high), ordered by :readout_sort. A
chart drawn as an image has the panel drawn into the image. Returns
{:noreply, state} when the cursor cell is unchanged.
@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 and at one less than the longest series on
the right. :up/:ArrowUp and :down/:ArrowDown move the Y offset by one
step, unclamped; each step shifts the drawn value range by a tenth of its span. The codepoint ?c resets the Y offset to 0. All of those
return {:ok, state}. :escape hides the value readout and bubbles; every
other key returns {:bubble, state}.
iex> c = Drafter.Widget.Chart.mount(%{data: Enum.to_list(1..20)})
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(:right, c)
iex> clamped._internal.scroll_offset
2
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. A release that ends no drag is a click, and moves the value
readout cursor to column x. Always returns {:ok, state} and consumes the event.
Whether this chart is drawing a transmitted image rather than cells.
@spec image_raster(t() | Drafter.Widget.props(), Drafter.Widget.rect()) :: Drafter.Render.Picture.t() | nil
The chart as a Drafter.Render.Picture filling the plot area of rect, or nil
when the chart does not resolve to the pixel renderer or cannot be drawn.
The plot area is rect less the y axis column, the x axis row and the title row
that render/2 draws as text. The image has the terminal's pixel size per cell
when the terminal reported it (see :cell_size in Drafter.App), and carries the
value readout panel while the pointer is over the chart.
@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. Every renderer draws the same picture of the
chart at its own resolution: a :pixel chart emits blank strips for the plot
area, because the image itself is emitted separately by the widget server; a
:text or :braille chart draws braille, two by four dots per cell (:text with
pixel_style: :quadrant draws :quad blocks); a cell-set chart draws that set's
blocks. The y axis, the x axis line and the title row are drawn around the plot
area when :show_axes and :title ask for them, labelled with the value range
the chart was drawn against.
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