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

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

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

method values

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

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.

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.