Where a continuous axis's breaks go, given its limits: evenly spaced numbers, nice to read, that
cover them (extended/2), round steps that cover them (pretty/2), or every multiple of a
width (fullseq/3), moved by an offset (width/3), between numbers, dates, date-times or
times of day; powers of a base and their
multiples for a logarithmic axis (log/2), whole numbers for an axis of exponentials
(exp/2), and whole seconds, minutes, hours, days or weeks for spans of time (timespan/2).
The breaks may fall outside the limits; which to keep is the caller's.
Summary
Types
A step on a calendar or a clock, as Date.shift/2 writes one: units and their counts
([month: 1]).
A unit a span of time is counted in.
A value broken: a number, or a date, date-time or time of day.
Functions
Breaks for an axis of exponentials over the limits {low, high}, in either order, R's
breaks_exp().
The breaks of the extended Wilkinson sequence that scores best over the limits {low, high},
low below high, in ascending order, R's breaks_extended(), n: breaks wanted.
Every multiple of size from the low limit rounded down to one to the high limit rounded up,
R's fullseq(), the limits in either order.
Breaks for a logarithmic axis over the limits {low, high}, positive numbers in either order,
R's breaks_log().
About n: intervals of round values covering the limits {low, high}, in either order, R's
breaks_pretty(), its pretty().
Breaks for an axis of spans of time over the limits {low, high}, in either order, counted in
unit:, in seconds, R's breaks_timespan().
Breaks width apart, R's breaks_width(): fullseq/3's sequence over the limits, each moved
by offset:.
Types
A step on a calendar or a clock, as Date.shift/2 writes one: units and their counts
([month: 1]).
@type timespan_unit() :: :second | :minute | :hour | :day | :week
A unit a span of time is counted in.
@type value() :: number() | Date.t() | NaiveDateTime.t() | DateTime.t() | Time.t()
A value broken: a number, or a date, date-time or time of day.
Functions
Breaks for an axis of exponentials over the limits {low, high}, in either order, R's
breaks_exp().
Each limit below 0 is taken as 0. Where the high limit reaches 3 and the limits are 3 or more
apart, the breaks are the high limit rounded down and the whole numbers below it, n: less one
of them at most, then 0, in that order, high to low, as R gives them; otherwise they are
extended/2's breaks on the limits, which every option here reaches.
Options
:n- The number of breaks wanted, 2 or more,labeling::extended()'sm; the breaks found may number a few more or fewer. The default value is5.:nice_numbers- The steps a sequence may take, each times a power of ten, in order of preference,labeling::extended()'sQ. The default value is[1, 5, 2, 2.5, 4, 3].:only_loose(boolean/0) - Whether the breaks must reach the limits, the first at or below the low one and the last at or above the high one,labeling::extended()'sonly.loose. The default value isfalse.:weights- The weights of simplicity, coverage, density and legibility, four numbers, the first three positive,labeling::extended()'sw. The default value is[0.25, 0.2, 0.5, 0.05].
Examples
iex> Scales.Breaks.exp({0.0, 100.0}, n: 4)
[100.0, 99.0, 98.0, 0.0]
iex> Scales.Breaks.exp({100.0, 102.0})
[100.0, 100.5, 101.0, 101.5, 102.0]
The breaks of the extended Wilkinson sequence that scores best over the limits {low, high},
low below high, in ascending order, R's breaks_extended(), n: breaks wanted.
It searches for the evenly spaced sequence that scores best on simplicity (a step from the nice numbers times a power of ten, early in their list, and zero among the breaks), coverage (the sequence spans the limits without overshooting them far), density (close to the number of breaks wanted) and legibility, weighted as R weights them. Limits narrower than 100 times the machine epsilon, or wider than the square root of the largest float, get the number wanted spaced evenly from end to end.
Options
:n- The number of breaks wanted, 2 or more,labeling::extended()'sm; the breaks found may number a few more or fewer. The default value is5.:nice_numbers- The steps a sequence may take, each times a power of ten, in order of preference,labeling::extended()'sQ. The default value is[1, 5, 2, 2.5, 4, 3].:only_loose(boolean/0) - Whether the breaks must reach the limits, the first at or below the low one and the last at or above the high one,labeling::extended()'sonly.loose. The default value isfalse.:weights- The weights of simplicity, coverage, density and legibility, four numbers, the first three positive,labeling::extended()'sw. The default value is[0.25, 0.2, 0.5, 0.05].
Examples
iex> Scales.Breaks.extended({9.225, 35.075})
[10.0, 15.0, 20.0, 25.0, 30.0, 35.0]
iex> Scales.Breaks.extended({0.0, 10.0})
[0.0, 2.5, 5.0, 7.5, 10.0]
iex> Scales.Breaks.extended({0.0, 10.0}, nice_numbers: [1, 2, 1.5, 4, 3])
[0.0, 2.0, 4.0, 6.0, 8.0, 10.0]
iex> Scales.Breaks.extended({52.0, 335.0}, n: 2)
[0.0, 300.0]
Every multiple of size from the low limit rounded down to one to the high limit rounded up,
R's fullseq(), the limits in either order.
On numbers size is a positive number; limits that are one number, as R's zero_range() judges
them, give that number half a size either side. On dates, date-times and times it is a step of
one unit, as Date.shift/2 writes one ([month: 1]), where R takes "1 month", and the
sequence is Dates, date-times or Times laid as R lays them: from the low limit rounded down
to the step's unit (a week to its Monday, a month to its 1st, seconds and minutes to the nearest
multiple of the step since 1970-01-01 00:00 UTC, or since midnight for a time) to the high one
rounded up, a date's units a day, a week, a month or a year, a time's a second, a minute or an
hour, a date-time's any of the seven. A DateTime is broken on its zone's clock, read by
time_zone_database:; months and years step on the calendar, the rest by elapsed time, as R's
seq() steps them.
Options
:pad(boolean/0) - Whether a sequence of numbers gains a step before it and one after, R'spad; refused for dates, date-times and times. The default value isfalse.:time_zone_database(atom/0) - The time zone database that reads aDateTime's zone, such asTz.TimeZoneDatabase; Elixir's own, which knows UTC alone, when not given. The default value isCalendar.UTCOnlyTimeZoneDatabase.
Examples
iex> Scales.Breaks.fullseq({1.6, 7.0}, 2)
[0.0, 2.0, 4.0, 6.0, 8.0]
iex> Scales.Breaks.fullseq({1.6, 7.0}, 2, pad: true)
[-2.0, 0.0, 2.0, 4.0, 6.0, 8.0, 10.0]
iex> Scales.Breaks.fullseq({~D[2026-01-10], ~D[2026-04-20]}, month: 1)
[~D[2026-01-01], ~D[2026-02-01], ~D[2026-03-01], ~D[2026-04-01], ~D[2026-05-01]]
Breaks for a logarithmic axis over the limits {low, high}, positive numbers in either order,
R's breaks_log().
It takes every power of base: from the low limit's logarithm rounded down to the high one's
rounded up, or every second, third and so on, the widest step that leaves n: breaks less two
inside the limits. Where every power leaves too few, it adds multiples of each power, one
multiple at a time, each the whole number below the base that keeps the steps evenest on the
logarithm (3 first for base 10), until enough fall inside, and keeps those inside with one more
either side; and where even every whole multiple leaves too few, extended/2's breaks on the
limits, n: wanted. A base of 2 or less adds no multiples: its powers are the breaks, however
few fall inside. Limits whose logarithms lie between the same two whole numbers but round to
one give that power alone.
Options
:n(pos_integer/0) - The number of breaks wanted, R'sn: a step is enough when it leaves that many less two inside the limits. The default value is5.:base- The logarithm's base, a number above 1, R'sbase. The default value is10.
Examples
iex> Scales.Breaks.log({1.0, 1.0e5})
[1.0, 100.0, 1.0e4]
iex> Scales.Breaks.log({2000.0, 9000.0})
[2000.0, 3000.0, 5000.0, 7000.0, 10000.0]
iex> Scales.Breaks.log({3.0, 5.0}, base: 2)
[2.0, 4.0, 8.0]
@spec pretty({value(), value()}, keyword()) :: [ number() | Time.t() | {Date.t() | NaiveDateTime.t() | DateTime.t(), String.t()} ]
About n: intervals of round values covering the limits {low, high}, in either order, R's
breaks_pretty(), its pretty().
On numbers each step is 1, 2, 5 or 10 times a power of ten: the limits' width over the number
wanted, rounded to the nearest of those, and the breaks every multiple of it from the low limit
rounded down to one to the high limit rounded up. On dates and date-times they are R's calendar
and clock steps, from a second to a thousand years, the one that cuts the limits closest to
n: times, a DateTime broken on its zone's clock, read by time_zone_database:, each break
with its label, {break, label}, as R names its breaks: written in the format of the step, as
Scales.Transform.format/4 writes them; on times of
day the numbers' breaks on their seconds since midnight, as R breaks an hms, each a Time,
wrapped past midnight. Limits that are one value, as R's zero_range() judges them, give the
first alone, a date's or date-time's labelled in full, as R writes a break it has not named.
R's documentation describes its numbers' steps but for the rule that rounds the width over the number wanted, which it leaves out; this rounds it to the nearest nice number as Heckbert's nice numbers do (Graphics Gems, 1990), but up from R's own switches, measured from its output: above 1.4, 2.8 and 7 times its power of ten.
Options
:n(non_neg_integer/0) - The number of intervals wanted, R'sn; the breaks found may cut the limits a few more or fewer times. The default value is5.:time_zone_database(atom/0) - The time zone database that reads aDateTime's zone, such asTz.TimeZoneDatabase; Elixir's own, which knows UTC alone, when not given. The default value isCalendar.UTCOnlyTimeZoneDatabase.
Examples
iex> Scales.Breaks.pretty({1.0, 15.0})
[0.0, 2.0, 4.0, 6.0, 8.0, 10.0, 12.0, 14.0, 16.0]
iex> Scales.Breaks.pretty({1.0, 20.0}, n: 2)
[0.0, 10.0, 20.0]
iex> Scales.Breaks.pretty({~D[2020-01-01], ~D[2022-01-01]})
[
{~D[2020-01-01], "2020-01"},
{~D[2020-07-01], "2020-07"},
{~D[2021-01-01], "2021-01"},
{~D[2021-07-01], "2021-07"},
{~D[2022-01-01], "2022-01"}
]
iex> Scales.Breaks.pretty({~T[00:00:00], ~T[01:00:00]})
[~T[00:00:00], ~T[00:16:40], ~T[00:33:20], ~T[00:50:00], ~T[01:06:40]]
Breaks for an axis of spans of time over the limits {low, high}, in either order, counted in
unit:, in seconds, R's breaks_timespan().
The limits are measured in the unit that suits their span: seconds over two minutes or less,
minutes over two hours or less, hours over two days or less, days over two weeks or less, else
weeks; there they take extended/2's breaks with the nice numbers 1, 2, 1.5, 4 and 3, n:
wanted, which are then measured in seconds, so they fall on whole seconds, minutes, hours, days
or weeks. R gives them as a difftime of seconds; here they are numbers.
Options
:unit- The unit the limits are counted in, one of:second,:minute,:hour,:dayand:week, R'sunit; the breaks are seconds. The default value is:second.:n- The number of breaks wanted, 2 or more, R'sn. The default value is5.
Examples
iex> Scales.Breaks.timespan({0.0, 100.0}, unit: :minute)
[0.0, 1200.0, 2400.0, 3600.0, 4800.0, 6000.0]
iex> Scales.Breaks.timespan({0.0, 100.0}, n: 3)
[0.0, 100.0]
Breaks width apart, R's breaks_width(): fullseq/3's sequence over the limits, each moved
by offset:.
On numbers the offset is a number added to each. On dates, date-times and times it is a list of
steps ([month: 1, day: 8]), each moving every break in turn, as R moves them by each of a
vector of offsets: seconds, minutes, hours, days and weeks by elapsed time, months and years on
the calendar, a day past the end of a month carried into the next, as R carries it, so 31
January and a month is 3 March. The breaks stay in the order the offset leaves them, unsorted
where it moves two onto one day, as R's are. A time of day wraps past midnight, where R's hms
runs on past 24 hours.
Options
:offset- How far each break is moved: a number for numbers, 0 when not given; a list of steps for dates, date-times and times, each a unit and a whole count, negative to move back ([month: 1, day: 8]).:time_zone_database(atom/0) - The time zone database that reads aDateTime's zone, such asTz.TimeZoneDatabase; Elixir's own, which knows UTC alone, when not given. The default value isCalendar.UTCOnlyTimeZoneDatabase.
Examples
iex> Scales.Breaks.width({1.6, 7.0}, 1, offset: 0.5)
[1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5]
iex> Scales.Breaks.width({~D[2026-01-10], ~D[2026-04-20]}, [month: 1], offset: [day: 8])
[~D[2026-01-09], ~D[2026-02-09], ~D[2026-03-09], ~D[2026-04-09], ~D[2026-05-09]]