Selkie.git | docs/api/ | Selkie--Plot--Ticks.md


NAME
====

Selkie::Plot::Ticks - Heckbert "nice-number" tick generation for axes

SYNOPSIS
========

```raku
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.2
```

DESCRIPTION
===========

`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 - min` and 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 in `rough-step` shouldn't bump the leading digit if either side is reasonable.

  * Compute `nice-min = floor(min / step) * step` and `nice-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 at `min`; `step` is `0`.

EXAMPLES
========

Driving an axis widget
----------------------

```raku
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:

```raku
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](Selkie--Plot--Scaler.md) — maps tick values to cell positions

  * [Selkie::Widget::Axis](Selkie--Widget--Axis.md) — 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

```raku
method nice(
    Real :$min!,
    Real :$max!,
    Int :$count where { ... } = 5
) returns Selkie::Plot::Ticks
```

Generate 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<lt> 2> or if C<min E<gt> max>. `min == max` is permitted (returns a single-tick set).

### method values

```raku
method values() returns List
```

Return the tick values as a list. Same data as the `values` accessor; this method exists for API symmetry with `labels`.

### method labels

```raku
method labels() returns List
```

Return 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.