Selkie--Widget--Plot
NAME
Selkie::Widget::Plot - Streaming chart wrapping notcurses' native ncuplot/ncdplot
SYNOPSIS
use Selkie::Widget::Plot;
use Selkie::Sizing;
# Streaming uint plot ā push samples as they arrive
my $cpu = Selkie::Widget::Plot.new(
type => 'uint',
min-y => 0,
max-y => 100,
title => 'CPU %',
sizing => Sizing.flex,
);
$cpu-supply.tap: -> $sample-pct {
state $tick = 0;
$cpu.push-sample($tick++, $sample-pct);
};
# Streaming double plot for fractional / non-integer measurements
my $temp = Selkie::Widget::Plot.new(
type => 'double',
min-y => -10.0,
max-y => 40.0,
sizing => Sizing.flex,
);
# Reactive ā bind to a store array of (x, y) pairs and the widget
# will pick up new samples from store updates.
my $bound = Selkie::Widget::Plot.new(
store-path => <metrics throughput>,
type => 'uint',
min-y => 0,
max-y => 1000,
sizing => Sizing.flex,
);DESCRIPTION
Selkie::Widget::Plot wraps notcurses' built-in plot widgets: ncuplot (uint64 samples) and ncdplot (num64 samples). The native code handles scaling, tick marks, blitter selection, and incremental rendering ā this widget's job is lifecycle management (creating and destroying the native handle, surviving plane resizes), the Selkie sample-push API, and optional store binding.
The plot is streaming-oriented: you push samples one at a time and it maintains a ring buffer of recent samples internally. For fixed/static data plotted with full chart machinery (axes, legends, multi-series), use Selkie::Widget::LineChart instead. For an inline single-row chart, use Selkie::Widget::Sparkline.
The two type variants
uint (default) ā wraps
ncuplot_*. Y values areuint64. Pass:type<uint>.double ā wraps
ncdplot_*. Y values arenum64. Pass:type<double>for fractional measurements.
X values are always uint64 in both variants ā they're slot indices, not arbitrary numeric values. If you need a non-monotonic or floating-point x-axis, use Selkie::Widget::LineChart or Selkie::Widget::ScatterPlot.
Native handle lifecycle
The native ncuplot / ncdplot handle is created lazily on the first render() after the widget gets a plane. It survives until one of:
resize ā the handle is destroyed and a new one is created at the new dimensions. Existing samples are lost. notcurses' plot API exposes no way to transfer sample state across resize. If you need history that survives terminal resize, keep the sample buffer outside the widget (e.g. in the store) and use Selkie::Widget::LineChart with reactive binding instead.
park ā when scrolled off-screen by a container swap, the handle is destroyed proactively. Recreated on the next render.
destroy ā final cleanup at widget shutdown.
Samples pushed before the handle exists (e.g. between widget construction and first plane attach) are buffered and flushed to the handle when it's created.
Reactive binding
:store-path binds the widget to a store path holding a list of ($x, $y) pairs. On dirty (when the store updates), the widget diffs the new list against its last-pushed-index and forwards new samples to the native handle. Truncating the array (or replacing it with a shorter one) causes the widget to recreate the handle and re-push from scratch.
EXAMPLES
Plotting an interval-driven sine wave
use Selkie::Widget::Plot;
my $plot = Selkie::Widget::Plot.new(
type => 'double',
min-y => -1.0,
max-y => 1.0,
sizing => Sizing.flex,
);
# Drive samples from an interval Supply
react {
whenever Supply.interval(0.05) -> $i {
my $value = sin($i * 0.1);
$plot.push-sample($i, $value);
}
}Lifecycle and resize behavior
The handle automatically recreates on resize. Sample history is lost, which is fine for a streaming dashboard but might surprise you in testing:
my $plot = Selkie::Widget::Plot.new(:type<uint>, :min-y(0), :max-y(100));
$plot.push-sample(0, 50);
$plot.push-sample(1, 75);
# Simulate a terminal resize:
$plot.handle-resize(20, 80);
# At this point the previous samples are gone ā the handle was
# recreated at the new dimensions. To preserve history, keep the
# sample buffer in your app code (or the store) and re-push after
# resize. For a chart that survives resize without manual
# bookkeeping, use Selkie::Widget::LineChart instead.Disabling spesh in test code
The shared notcurses + native plot interaction trips the MoarVM specializer in some pathological cases. The snapshot harness sets MVM_SPESH_DISABLE=1 globally; in your own test code that exercises the Plot widget's lifecycle, set the env var before running. See xt/snapshots/25-plot-streaming.raku for a working example.
SEE ALSO
Selkie::Widget::Sparkline ā single-row inline chart, no native handle
Selkie::Widget::LineChart ā full multi-series chart for static data
has Str $.type
Sample type variant: uint (default) wraps ncuplot, double wraps ncdplot.
has Real $.min-y
Lower bound of the Y range. Below this, samples saturate to the bottom of the plot.
has Real $.max-y
Upper bound of the Y range. Above this, samples saturate to the top of the plot.
has Str $.title
Optional title written above the plot by notcurses.
has Int $.gridtype
Notcurses blitter for the plot rendering. Defaults to braille (2Ć4 sub-cell resolution); see NCBLIT_* in Notcurses::Native::Types.
has UInt $.rangex
Number of x-axis slots in the ring buffer. Defaults to widget width Ć 2 so braille's sub-cell density is fully used. Set explicitly when pushing more than one sample per cell-column.
has Positional[Str] @.store-path
Optional reactive binding ā store path to a list of ($x, $y) pairs. The widget pushes new samples to the native handle when the store path updates.
has Str $.empty-message
Message rendered when no samples have been received yet. The default is the expected startup state for monitoring dashboards. Set to the empty string to suppress (the plot will show a blank pane until the first sample arrives).
method on-store-attached
method on-store-attached(
$store
) returns MuHook called when the widget is attached to a store. Subscribes against :store-path so new samples appended to the path are pushed into the native ring buffer on each tick. No-op when no :store-path was given (caller drives via push-sample).
method push-sample
method push-sample(
Int(Cool) $x,
Real $y
) returns MuPush a sample. $x is the slot index (always integer); $y is the value (UInt for type=uint, Num for type=double). If the native handle doesn't exist yet (before plane attach or immediately after resize), the sample is buffered and flushed on next render.
method set-sample
method set-sample(
Int(Cool) $x,
Real $y
) returns MuSet (overwrite) the sample at slot $x. Same semantics as notcurses' ncuplot_set_sample / ncdplot_set_sample: replaces rather than accumulating. No-op if the handle hasn't been created yet ā use push-sample for buffer-aware writes.
method park
method park() returns MuPark the plot. Tears down the native plot handle (which retains its own pixel resources outside the standard plane) before parking the widget plane. Sample buffer and the :store-path subscription survive ā on the next render after unpark, the handle is recreated and the buffered samples are flushed in.
method destroy
method destroy() returns MuTear down both the native plot handle and the widget plane. Called by Selkie::App.shutdown.
method has-handle
method has-handle() returns BoolReturns True iff the native plot handle is currently allocated. Mostly useful in tests verifying lifecycle behavior.