Selkie.git | t/ | 44-plot-scaler.rakutest
use Test;
use lib 'lib';
use Selkie::Plot::Scaler;
plan 14;
subtest "linear constructor accepts valid args" => {
plan 4;
my $s = Selkie::Plot::Scaler.linear(min => 0, max => 100, cells => 20);
isa-ok $s, Selkie::Plot::Scaler;
is $s.min, 0, "min stored";
is $s.max, 100, "max stored";
is $s.cells, 20, "cells stored";
};
subtest "linear constructor rejects cells == 0" => {
plan 1;
dies-ok { Selkie::Plot::Scaler.linear(min => 0, max => 1, cells => 0) },
"zero-cell scaler is rejected";
};
subtest "linear constructor rejects min > max" => {
plan 1;
dies-ok { Selkie::Plot::Scaler.linear(min => 10, max => 5, cells => 20) },
"min > max is rejected (use :invert instead)";
};
subtest "linear constructor accepts min == max (degenerate)" => {
plan 1;
lives-ok { Selkie::Plot::Scaler.linear(min => 7, max => 7, cells => 20) },
"min == max is permitted (degenerate range)";
};
subtest "value-to-cell at endpoints, midpoint, and inside" => {
plan 5;
my $s = Selkie::Plot::Scaler.linear(min => 0, max => 100, cells => 20);
is $s.value-to-cell(0), 0, "min maps to cell 0";
is $s.value-to-cell(100), 19, "max maps to cell cells-1";
is $s.value-to-cell(50), 10, "midpoint maps to cell 10 (round of 9.5)";
is $s.value-to-cell(25), 5, "25% maps to cell 5 (round of 4.75)";
is $s.value-to-cell(75), 14, "75% maps to cell 14 (round of 14.25)";
};
subtest "value-to-cell clamps below min and above max" => {
plan 4;
my $s = Selkie::Plot::Scaler.linear(min => 0, max => 100, cells => 20);
is $s.value-to-cell(-50), 0, "below-min clamps to 0";
is $s.value-to-cell(-1), 0, "just-below-min clamps to 0";
is $s.value-to-cell(101), 19, "just-above-max clamps to cells-1";
is $s.value-to-cell(1e9), 19, "extreme above-max clamps to cells-1";
};
subtest "value-to-cell handles NaN and infinities" => {
plan 3;
my $s = Selkie::Plot::Scaler.linear(min => 0, max => 100, cells => 20);
nok $s.value-to-cell(NaN).defined, "NaN propagates as undef";
is $s.value-to-cell(Inf), 19, "+Inf clamps to cells-1";
is $s.value-to-cell(-Inf), 0, "-Inf clamps to 0";
};
subtest "inverted axis flips cell direction" => {
plan 5;
my $s = Selkie::Plot::Scaler.linear(
min => 0, max => 100, cells => 20, :invert,
);
is $s.value-to-cell(100), 0, "max maps to cell 0 under :invert";
is $s.value-to-cell(0), 19, "min maps to cell cells-1 under :invert";
is $s.value-to-cell(50), 9, "midpoint flips around midpoint";
is $s.value-to-cell(Inf), 0, "+Inf clamps to 0 under :invert";
is $s.value-to-cell(-Inf), 19, "-Inf clamps to cells-1 under :invert";
};
subtest "single-cell axis maps everything to 0" => {
plan 4;
my $s = Selkie::Plot::Scaler.linear(min => 0, max => 100, cells => 1);
is $s.value-to-cell(0), 0, "min";
is $s.value-to-cell(50), 0, "mid";
is $s.value-to-cell(100), 0, "max";
is $s.value-to-cell(-1), 0, "out-of-range";
};
subtest "degenerate domain (min == max) maps to middle cell" => {
plan 4;
my $s = Selkie::Plot::Scaler.linear(min => 7, max => 7, cells => 21);
# cells - 1 = 20; (cells - 1) div 2 = 10
is $s.value-to-cell(7), 10, "value at the degenerate point";
is $s.value-to-cell(0), 10, "any other value also maps to middle";
is $s.value-to-cell(1e9), 10, "extreme value too";
# cell-to-value of degenerate domain returns min (== max)
is $s.cell-to-value(0), 7, "cell-to-value returns the degenerate value";
};
subtest "cell-to-value at endpoints and midpoint" => {
plan 3;
my $s = Selkie::Plot::Scaler.linear(min => 0, max => 100, cells => 21);
# (cells - 1) = 20
is-approx $s.cell-to-value(0), 0, "cell 0 → min";
is-approx $s.cell-to-value(20), 100, "cell cells-1 → max";
is-approx $s.cell-to-value(10), 50, "cell 10 → midpoint";
};
subtest "cell-to-value under :invert" => {
plan 3;
my $s = Selkie::Plot::Scaler.linear(
min => 0, max => 100, cells => 21, :invert,
);
is-approx $s.cell-to-value(0), 100, "cell 0 → max under :invert";
is-approx $s.cell-to-value(20), 0, "cell cells-1 → min under :invert";
is-approx $s.cell-to-value(10), 50, "midpoint cell → midpoint value";
};
subtest "round-trip cell-to-value(value-to-cell(v)) within tolerance" => {
plan 7;
my $cells = 41;
my $s = Selkie::Plot::Scaler.linear(min => 0, max => 100, cells => $cells);
# Tolerance per the documented half-cell precision floor:
# ± (max - min) / (2 * (cells - 1))
my $tol = (100 - 0) / (2 * ($cells - 1));
for 0, 1, 13.7, 50, 73.2, 99, 100 -> $v {
my $rt = $s.cell-to-value($s.value-to-cell($v));
ok ($rt - $v).abs <= $tol,
"round-trip $v → $rt within ±{$tol.fmt('%.4f')}";
}
};
subtest "negative domain works correctly" => {
plan 5;
my $s = Selkie::Plot::Scaler.linear(min => -50, max => 50, cells => 21);
# (cells - 1) = 20
is $s.value-to-cell(-50), 0, "min (-50) → 0";
is $s.value-to-cell(50), 20, "max (+50) → 20";
is $s.value-to-cell(0), 10, "zero → middle cell";
is-approx $s.cell-to-value(0), -50, "cell 0 → -50";
is-approx $s.cell-to-value(20), 50, "cell 20 → +50";
};