Selkie--Plot--Ticks
NAME
Selkie::Plot::Ticks - Heckbert "nice-number" tick generation for axes
SYNOPSIS
use Selkie::Plot::Ticks;
# Roughly five ticks across [0, 100]. Heckbert lands on step=20 (the
# nearest "nice" multiplier in {1, 2, 5}); five ticks would have
# wanted step=25, which isn't in the set, so we get six instead.
my $t = Selkie::Plot::Ticks.nice(min => 0, max => 100, count => 5);
$t.values; # β (0, 20, 40, 60, 80, 100)
$t.labels; # β ("0", "20", "40", "60", "80", "100")
$t.step; # β 20
# Awkward endpoints β Heckbert pads to nice numbers
my $u = Selkie::Plot::Ticks.nice(min => 7, max => 93, count => 5);
$u.values; # β (0, 20, 40, 60, 80, 100) β extends past min/max
$u.step; # β 20
# Sub-unit ranges produce sub-unit steps
my $v = Selkie::Plot::Ticks.nice(min => 0, max => 1, count => 5);
$v.values; # β (0, 0.2, 0.4, 0.6, 0.8, 1.0)
$v.labels; # β ("0.0", "0.2", "0.4", "0.6", "0.8", "1.0")
$v.step; # β 0.2DESCRIPTION
Selkie::Plot::Ticks picks "nice" tick values for an axis covering the domain [min, max]. Nice means each tick is a multiple of step, and step is chosen from {1, 2, 5} Γ 10^n for some integer n β the values that humans naturally read on a graph.
The algorithm is Paul Heckbert's classic, described in Graphics Gems (1990): pick a "nice" range, divide it into roughly count intervals, snap the interval to a nice number, then enumerate ticks. The output count is approximately count, not exactly β typical deviation is Β±1 tick.
The algorithm
Given min, max, and a target count:
Compute
range = max - minand snap it up to a nice number (floored to a 1, 2, 5, or 10 leading digit).Compute
rough-step = range / (count - 1)and snap it rounded to a nice number β small differences inrough-stepshouldn't bump the leading digit if either side is reasonable.Compute
nice-min = floor(min / step) * stepandnice-max = ceil(max / step) * stepβ round the data range outward to the nearest tick.Enumerate ticks at
nice-min, nice-min + step, nice-min + 2Β·step, ..., nice-max.
The result is a tick set whose endpoints may extend slightly beyond the data range. This is intentional β chart axes look better when the labels are round numbers like 0 and 100 rather than the precise data extent of 7 and 93.
A worked example
For min = 0.001, max = 0.009, count = 4:
range = nice(0.008, :!round).0.008 / 10^-3 = 8β leading digit 10 β range =0.01.rough-step = 0.01 / 3 β 0.00333.nice(0.00333, :round):3.33 / 10^-3 = 3.33β leading digit 5 β step =0.005.nice-min = floor(0.001 / 0.005) * 0.005 = 0.nice-max = ceil(0.009 / 0.005) * 0.005 = 0.01.Ticks:
0, 0.005, 0.01β three ticks, requested four. Heckbert prefers nice spacing over exact count.
Edge cases
count E<lt> 2β nonsensical (a single tick has no spacing). Throws.min E<gt> maxβ throws. Pass arguments in order.min == maxβ degenerate. Returns a single-element tick set atmin;stepis0.
EXAMPLES
Driving an axis widget
use Selkie::Plot::Scaler;
use Selkie::Plot::Ticks;
use Selkie::Widget::Axis;
my $scaler = Selkie::Plot::Scaler.linear(min => 0, max => 1000, cells => 80);
my $ticks = Selkie::Plot::Ticks.nice(min => 0, max => 1000, count => 5);
my $axis = Selkie::Widget::Axis.new(
edge => 'bottom',
:$scaler,
:$ticks,
);Picking labels for a sub-unit range
When the step is fractional, labels are zero-padded to the step's precision so they align visually:
my $t = Selkie::Plot::Ticks.nice(min => 0.0, max => 0.1, count => 5);
$t.step; # β 0.02
$t.labels; # β ("0.00", "0.02", "0.04", "0.06", "0.08", "0.10")SEE ALSO
Selkie::Plot::Scaler β maps tick values to cell positions
Selkie::Widget::Axis β renders ticks + labels along an edge
has Real $.min
The data-range lower bound passed in.
has Real $.max
The data-range upper bound passed in.
has UInt $.count
The target tick count (approximate; actual may differ by Β±1-2).
has Real $.step
The chosen tick step. Always a member of {1, 2, 5} Γ 10^n. Zero in the degenerate min == max case.
has Positional[Real] @.values
The generated tick values, in ascending order.
method nice
method nice(
Real :$min!,
Real :$max!,
Int :$count where { ... } = 5
) returns Selkie::Plot::TicksGenerate a nice tick set covering [min, max] with approximately count ticks. The actual count may differ from count by Β±1-2 β Heckbert prefers round numbers over an exact count. Throws if C<count E 2> or if C<min E max>. min == max is permitted (returns a single-tick set).
method values
method values() returns ListReturn the tick values as a list. Same data as the values accessor; this method exists for API symmetry with labels.
method labels
method labels() returns ListReturn formatted labels for each tick. Labels use a fixed decimal precision derived from step so they align visually: =item Integer step (e.g. 25) β no decimals: ("0", "25", "50") =item Sub-unit step (e.g. 0.005) β decimals matching the step: ("0.000", "0.005", "0.010") Negative ticks render with a leading minus sign.