Selkie--Plot--Scaler
NAME
Selkie::Plot::Scaler - Map a numeric domain onto a discrete cell range
SYNOPSIS
use Selkie::Plot::Scaler;
# A linear scaler that maps the domain [0, 100] onto a 20-cell axis.
my $s = Selkie::Plot::Scaler.linear(min => 0, max => 100, cells => 20);
$s.value-to-cell(0); # β 0
$s.value-to-cell(50); # β 10
$s.value-to-cell(100); # β 19
$s.value-to-cell(150); # β 19 (clamped)
$s.value-to-cell(-10); # β 0 (clamped)
$s.cell-to-value(10); # β 52.6315... (midpoint of cell 10)
# Inverted axis β cell 0 holds the maximum value, useful for y-axes
# where the top of the screen is row 0 but the largest value should
# render highest on the chart.
my $y = Selkie::Plot::Scaler.linear(
min => 0, max => 100, cells => 20, :invert,
);
$y.value-to-cell(100); # β 0 (top row)
$y.value-to-cell(0); # β 19 (bottom row)DESCRIPTION
A Selkie::Plot::Scaler maps a numeric value in the domain [min, max] to an integer cell index in [0, cells-1]. It's the shared coordinate-mapping primitive used by every chart widget β Sparkline, BarChart, LineChart, ScatterPlot, Heatmap, and the axes that label them.
The scaler is pure data: no notcurses, no widget, no I/O. It's deterministic and exhaustively unit-testable, which matters because a miscomputed mapping silently corrupts every chart that uses it.
The linear formula
For cells E<gt> 1:
cell = round( (value - min) / (max - min) * (cells - 1) )then clamped to [0, cells-1]. With :invert the result is flipped: cell = (cells - 1) - cell.
The inverse cell-to-value returns the value at the midpoint of the target cell:
value = (cell / (cells - 1)) * (max - min) + minA round-trip cell-to-value(value-to-cell(v)) recovers v within Β± (max - min) / (2 * (cells - 1)) β the half-cell precision floor imposed by the integer cell grid.
Edge cases
cellsmust be > 0 β zero-cell scalers are nonsensical and throw.min == maxdegenerate range β every value maps to the middle cell.cell-to-valuereturnsmin(which equalsmax).min E<gt> maxβ throws. Inverted axes are expressed via:invert, not via reversed bounds.NaNinput βvalue-to-cellreturnsUInt(the typed undef). NaN is propagated, not clamped, so callers can detect missing samples.+Inf/-Infinput β clamped to the corresponding edge cell (cells - 1for+Inf,0for-Inf; flipped under:invert).Out-of-domain value β clamped to the nearest edge cell. No exception.
EXAMPLES
Composing scalers for a 2D plot
A scatter plot needs two scalers β one per axis. The y-scaler is typically inverted because terminal row 0 is the top of the screen but charts conventionally place the maximum value at the top.
my $x-scaler = Selkie::Plot::Scaler.linear(
min => 0, max => $duration, cells => $width,
);
my $y-scaler = Selkie::Plot::Scaler.linear(
min => $min-y, max => $max-y, cells => $height, :invert,
);
for @samples -> %point {
my $col = $x-scaler.value-to-cell(%point<t>);
my $row = $y-scaler.value-to-cell(%point<v>);
plot-dot($row, $col);
}Recovering tick values for axis labels
When generating axis tick labels (see Selkie::Plot::Ticks) you want the value at a given cell. cell-to-value gives the cell midpoint:
my $axis-scaler = Selkie::Plot::Scaler.linear(
min => 0, max => 1000, cells => 80,
);
say $axis-scaler.cell-to-value(0); # β 0
say $axis-scaler.cell-to-value(40); # β 506.32...
say $axis-scaler.cell-to-value(79); # β 1000SEE ALSO
Selkie::Plot::Ticks β generates "nice" tick values for a domain
Selkie::Plot::Palette β colorblind-safe series colors and ramps
Selkie::Widget::Axis β renders ticks + labels along a chart edge
has Real $.min
Domain lower bound (inclusive).
has Real $.max
Domain upper bound (inclusive).
has UInt $.cells
Number of discrete cells in the target range. Must be > 0.
has Bool $.invert
Whether to flip the mapping (cell 0 holds the maximum value).
method linear
method linear(
Real :$min!,
Real :$max!,
Int :$cells! where { ... },
Bool :$invert = Bool::False
) returns Selkie::Plot::ScalerLinear scaler constructor. Throws if cells is 0 or if C<min E max>. min == max is permitted (degenerate range β every value maps to the middle cell). Use :invert for axes where cell 0 should hold the maximum value (typically y-axes β terminal row 0 is the top of the screen, and charts conventionally render the largest value highest).
method value-to-cell
method value-to-cell(
Real $value
) returns UIntMap a value in the domain to a cell index in [0, cells-1]. Out-of-domain values are clamped to the nearest edge. NaN propagates as UInt (the typed undef). Β±Inf clamps to the corresponding edge.
method cell-to-value
method cell-to-value(
Int $cell-in
) returns RealMap a cell index back to its midpoint value in the domain. Out-of-range cell indices are clamped.