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 are uint64. Pass :type<uint>.

  • double — wraps ncdplot_*. Y values are num64. 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

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 Mu

Hook 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 Mu

Push 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 Mu

Set (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 Mu

Park 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 Mu

Tear down both the native plot handle and the widget plane. Called by Selkie::App.shutdown.

method has-handle

method has-handle() returns Bool

Returns True iff the native plot handle is currently allocated. Mostly useful in tests verifying lifecycle behavior.

Selkie v0.10.0

High-level TUI framework built on Notcurses

Authors

  • Matt Doughty

License

Artistic-2.0

Dependencies

Notcurses::Native:ver<0.4.0+>:auth<zef:apogee>

Test Dependencies

Provides

  • Selkie
  • Selkie::App
  • Selkie::App::Internal::Dispatch
  • Selkie::App::Internal::ErrorLog
  • Selkie::App::Internal::FocusTree
  • Selkie::App::Internal::HitTest
  • Selkie::App::Internal::IdleBudget
  • Selkie::App::Internal::OverlayTree
  • Selkie::App::Internal::RenderLoop
  • Selkie::App::Internal::ScreenModalLifecycle
  • Selkie::App::Internal::Terminal
  • Selkie::App::Internal::TerminalSequences
  • Selkie::Container
  • Selkie::EffectiveBounds
  • Selkie::Event
  • Selkie::Layout::Allocate
  • Selkie::Layout::HBox
  • Selkie::Layout::Split
  • Selkie::Layout::VBox
  • Selkie::Plot::Palette
  • Selkie::Plot::Scaler
  • Selkie::Plot::Ticks
  • Selkie::ScreenManager
  • Selkie::Sizing
  • Selkie::Store
  • Selkie::Style
  • Selkie::Test::Focus
  • Selkie::Test::Keys
  • Selkie::Test::Snapshot
  • Selkie::Test::Snapshot::Harness
  • Selkie::Test::Store
  • Selkie::Test::Supply
  • Selkie::Test::Tree
  • Selkie::Theme
  • Selkie::Trace
  • Selkie::Tree
  • Selkie::Widget
  • Selkie::Widget::Axis
  • Selkie::Widget::BarChart
  • Selkie::Widget::Border
  • Selkie::Widget::Button
  • Selkie::Widget::CardList
  • Selkie::Widget::Checkbox
  • Selkie::Widget::CommandPalette
  • Selkie::Widget::ConfirmModal
  • Selkie::Widget::FileBrowser
  • Selkie::Widget::FocusableByDefault
  • Selkie::Widget::Heatmap
  • Selkie::Widget::HelpOverlay
  • Selkie::Widget::Histogram
  • Selkie::Widget::Image
  • Selkie::Widget::Legend
  • Selkie::Widget::LineChart
  • Selkie::Widget::ListView
  • Selkie::Widget::Modal
  • Selkie::Widget::MultiLineInput
  • Selkie::Widget::PasswordStrength
  • Selkie::Widget::Plot
  • Selkie::Widget::ProgressBar
  • Selkie::Widget::RadioGroup
  • Selkie::Widget::RichText
  • Selkie::Widget::RichText::Span
  • Selkie::Widget::ScatterPlot
  • Selkie::Widget::ScrollView
  • Selkie::Widget::Select
  • Selkie::Widget::Sparkline
  • Selkie::Widget::Spinner
  • Selkie::Widget::TabBar
  • Selkie::Widget::Table
  • Selkie::Widget::Text
  • Selkie::Widget::TextInput
  • Selkie::Widget::TextInput::HighlightSpan
  • Selkie::Widget::TextStream
  • Selkie::Widget::Toast
  • Selkie::Widget::ViewportedCardList

Documentation

The Camelia image is copyright 2009 by Larry Wall. "Raku" is trademark of the Yet Another Society. All rights reserved.