The transformation a continuous scale puts its values through before it trains on them, named
by a value (name/0): values into transformed space (transform/2) and back
(inverse/2), and an axis's default breaks (breaks/3), minor breaks (minor_breaks/3) and
labels (format/3) there, as R's transformation objects carry them; for a transformation of
numbers, also the derivatives both ways (derivative/2, inverse_derivative/2) and the range
data takes in transformed space (trim_to_domain/2).
Breaks are found as on an axis showing an extent in transformed space: the extent is squished
into the transformation's domain (squish/2), turned back into data units and broken there,
and the breaks transformed back, in the order that leaves them, so a reversing transformation's
descend. Some may fall outside the extent; which to keep is the caller's. Labels are written
from the breaks turned back, 10^x under log10; a date's depend on the step chosen for the
whole axis, so format/3 is handed the extent too. A date, date-time or time transformation
also lays breaks a calendar or clock step apart (width/3).
A value the transformation cannot place, below a logarithm's or the square root's domain,
becomes :nan, R's NaN; a value placed at an infinity is :infinity or :neg_infinity, and a
missing one stays nil.
Compositions
{:compose, [first, second, ...]} puts values through each transformation in turn, R's
transform_compose(): {:compose, [:log10, :reverse]} is a log axis drawn high to low. Its
breaks and labels are its first part's, found on the extent turned back into that part's space,
and n: reaches them as it reaches that part; its minor breaks are
Scales.MinorBreaks.regular/3's, unreversed whichever way it runs, as R's; it has derivatives
where every part has them, by the chain rule. Its domain is what every part takes: the first
part's domain transformed, cut to the next part's, transformed again, and so on, turned back
into data units, so {:compose, [:sqrt, :log10]} takes values from 1e-200 up, and a
composition no value passes raises ArgumentError. A date, date-time or time transformation
may come first, and the composition then takes and gives its values, its breaks, labels and
steps (width/3) its first part's, so {:compose, [:date, :reverse]} is a date axis drawn
late to early; anywhere else it raises ArgumentError.
iex> Scales.Transform.transform({:compose, [:log10, :reverse]}, [1, 10, 100])
[-0.0, -1.0, -2.0]
iex> Scales.Transform.format({:compose, [:log10, :reverse]}, [-0.0, -1.0, -2.0], {-2.1, 0.1})
["1", "10", "100"]A transformation of your own
new/2 makes one from its functions, R's new_transform(), and it is given wherever a name is,
a composition's parts too. Each function takes one value and returns a number, :infinity,
:neg_infinity, :nan or nil. The transformation is handed every value in its domain, an
integer as a float, and the domain's own ends among them, since an axis is bounded by them;
nil and :nan are never handed over, and a value outside the domain becomes :nan without
it. So a transformation on the whole line, the default domain, handles both infinities, as R's
functions do through their arithmetic; its inverse is handed the transformation's values at
the domain's ends, and handles them too.
iex> cube =
...> Scales.Transform.new("cube",
...> transform: fn
...> :infinity -> :infinity
...> :neg_infinity -> :neg_infinity
...> value -> value * value * value
...> end,
...> inverse: fn
...> :infinity -> :infinity
...> :neg_infinity -> :neg_infinity
...> position when position < 0 -> -:math.pow(-position, 1 / 3)
...> position -> :math.pow(position, 1 / 3)
...> end
...> )
iex> Scales.Transform.transform(cube, [2, -0.5, nil])
[8.0, -0.125, nil]
iex> Scales.Transform.breaks(cube, {-1100.0, 1100.0})
[-1000.0, -125.0, 0.0, 125.0, 1000.0]
iex> Scales.Transform.format(cube, [-1000.0, -125.0, 0.0, 125.0, 1000.0], {-1100.0, 1100.0})
["-10", "-5", "0", "5", "10"]Any distribution's quantile is one, as :probit is the normal's: the Cauchy distribution's on
the probabilities from 0 to 1, its ends the infinities.
iex> cauchy =
...> Scales.Transform.new("cauchy",
...> transform: fn
...> probability when probability == 0 -> :neg_infinity
...> probability when probability == 1 -> :infinity
...> probability -> :math.tan(:math.pi() * (probability - 0.5))
...> end,
...> inverse: fn
...> :neg_infinity -> 0.0
...> :infinity -> 1.0
...> quantile -> 0.5 + :math.atan(quantile) / :math.pi()
...> end,
...> domain: {0, 1}
...> )
iex> Scales.Transform.transform(cauchy, [0, 0.5, 1, 1.5])
[:neg_infinity, 0.0, :infinity, :nan]
Summary
Types
A break in transformed space: a number, or an infinity or NaN where R's lands one.
A transformation, by name: :identity, R's transform_identity(); :log (base e), :log2,
:log10 and {:log, base}, R's transform_log(), transform_log2(), transform_log10() and
transform_log(base); :log1p, R's transform_log1p(); :exp (base e) and {:exp, base},
R's transform_exp(); :asinh, R's transform_asinh(); :pseudo_log and
{:pseudo_log, sigma: sigma, base: base}, either or both, R's
transform_pseudo_log(sigma = 1, base = exp(1)); :sqrt, :reverse (each value negated) and
:reciprocal, R's transform_sqrt() and its kin; :asn, the arcsine square root, and
:atanh, R's transform_asn() and transform_atanh(); :logit and :probit, R's
transform_logit() and transform_probit(); {:probability, :normal, mean: mean, sd: sd} and
{:probability, :logistic, location: location, scale: scale}, either or both, R's
transform_probability("norm", mean = 0, sd = 1) and transform_probability("logis", location = 0, scale = 1); {:boxcox, exponent} and {:boxcox, exponent, offset: offset},
R's transform_boxcox(p, offset = 0); {:modulus, exponent} and {:modulus, exponent, offset: offset}, R's transform_modulus(p, offset = 1); {:yj, exponent}, the Yeo-Johnson
transformation, R's transform_yj(p); :timespan and {:timespan, unit}, a number counted in
unit, :second to :week, as its seconds, R's transform_timespan(unit = "secs"); :date,
a Date as its days since 1970-01-01, R's transform_date(); {:datetime, zone, database}, a
DateTime or NaiveDateTime as its seconds since 1970-01-01 00:00 UTC, its breaks laid on
the clock of zone, read by the time zone database database, R's transform_time(tz), the
zone :default until a column settles it and :naive for naive date-times; :time, a
Time as its seconds since midnight, R's transform_hms(); {:compose, [first, second, ...]}, each in turn, R's transform_compose(); and a transformation of your own, t/0,
made by new/2.
Functions
The default breaks, in transformed space, of an axis showing extent under the transformation
name, in the order the transformation leaves them, some outside the extent, those at an end
of a domain transformed to an infinity there, as logit's 0 and 1, which no extent reaches:
extended breaks (Scales.Breaks) on the extent in data units, powers of the base and their multiples under a logarithm in a base
(Scales.Breaks.log/2, so powers of e under :log), calendar dates under the date
transformation and clock times under the date-time one, as R's pretty() finds them, and
spans of time under the time and span of time ones, as Scales.Breaks.timespan/2 finds them.
Under the Box-Cox, modulus and Yeo-Johnson transformations an end of the extent that turns back
to an infinity, past a bound their values never reach, is set aside and the breaks found on the
other end alone, as R's sit there; none where both do.
The derivative of the transformation name at each of values, R's d_transform, each a
double (Scales.double/0), its infinities and NaN as R's arithmetic gives them. Only a
transformation of numbers has one: a date's, date-time's, time's or span of time's raises
FunctionClauseError.
The default labels of breaks, in transformed space, under the transformation name: its own
breaks on an axis showing extent, found with options, breaks/3's, or, for :given, breaks
it did not choose, the extent squished into the domain as for breaks/3. A number's are
Scales.Labels.plain/1's, written from the breaks turned back, but the Box-Cox, modulus and
Yeo-Johnson transformations write each of their own from the value in data units it was found
from, since the round trip can keep as few as three of its digits, and a span of time's are
Scales.Labels.timespan/2's; a date's in the format of the step its breaks were chosen at, so a date's or date-time's
breaks found with n: are labelled when given the same n:, and a given date written in full.
Positions in transformed space turned back into data units by the transformation name.
The derivative of the inverse of the transformation name at each of positions, in
transformed space, R's d_inverse, as derivative/2 gives the transformation's.
The default minor breaks between breaks on an axis showing extent under the transformation
name, in transformed space: Scales.MinorBreaks.regular/3's, reversed for :reverse.
A transformation of your own named name, R's new_transform(), from its functions, each of
one value: given wherever a transformation's name is, and as a composition's part. Its domain
is the whole line unless domain: says, its breaks Scales.Breaks.extended/2's on an axis's
limits in data units, its minor breaks Scales.MinorBreaks.regular/3's and its labels
Scales.Labels.plain/1's of the breaks turned back, each rounded to 14 significant digits
first, unless breaks:, minor_breaks: or labels: say. A missing function, an unknown option, a function of another arity or a domain
not {low, high} raises NimbleOptions.ValidationError.
The extent {low, high}, in transformed space, squished into the domain of the transformation
name, transformed: each end outside it moved onto it, as R's oob_squish() moves it, so
log10's breaks are not sought below -100, and onto a bound the values of a transformation of
every number never pass, the Yeo-Johnson transformation's 1 for an exponent of -1. A date's,
date-time's or time's is never squished.
The values put through the transformation name.
The range values take in transformed space under the transformation name, R's
trim_to_domain(): their range, nil and :nan left out, squished into the domain and its
ends transformed, as {low, high}; :none with no value to range over, where R warns. Only a
transformation of numbers takes one: a date's, date-time's or time's raises
FunctionClauseError.
The breaks a step lays, written as Date.shift/2 takes one ([week: 2]), on an axis showing
extent under the date, date-time or time transformation name, in transformed space, some
outside it, as R's breaks_width() lays them: from the extent's start rounded down to the
step's unit (a week to its Monday), every step. Only those three transformations have steps.
Types
@type break() :: float() | :infinity | :neg_infinity | :nan
A break in transformed space: a number, or an infinity or NaN where R's lands one.
@type name() :: :identity | :date | {:datetime, String.t() | :default | :naive, module()} | :time | :log | :log2 | :log10 | {:log, number()} | :log1p | :exp | {:exp, number()} | :asinh | :pseudo_log | {:pseudo_log, sigma: number(), base: number()} | :sqrt | :reverse | :reciprocal | :asn | :atanh | :logit | :probit | {:probability, :normal, mean: number(), sd: number()} | {:probability, :logistic, location: number(), scale: number()} | {:boxcox, number()} | {:boxcox, number(), [{:offset, number()}]} | {:modulus, number()} | {:modulus, number(), [{:offset, number()}]} | {:yj, number()} | :timespan | {:timespan, Scales.Breaks.timespan_unit()} | {:compose, [name(), ...]} | t()
A transformation, by name: :identity, R's transform_identity(); :log (base e), :log2,
:log10 and {:log, base}, R's transform_log(), transform_log2(), transform_log10() and
transform_log(base); :log1p, R's transform_log1p(); :exp (base e) and {:exp, base},
R's transform_exp(); :asinh, R's transform_asinh(); :pseudo_log and
{:pseudo_log, sigma: sigma, base: base}, either or both, R's
transform_pseudo_log(sigma = 1, base = exp(1)); :sqrt, :reverse (each value negated) and
:reciprocal, R's transform_sqrt() and its kin; :asn, the arcsine square root, and
:atanh, R's transform_asn() and transform_atanh(); :logit and :probit, R's
transform_logit() and transform_probit(); {:probability, :normal, mean: mean, sd: sd} and
{:probability, :logistic, location: location, scale: scale}, either or both, R's
transform_probability("norm", mean = 0, sd = 1) and transform_probability("logis", location = 0, scale = 1); {:boxcox, exponent} and {:boxcox, exponent, offset: offset},
R's transform_boxcox(p, offset = 0); {:modulus, exponent} and {:modulus, exponent, offset: offset}, R's transform_modulus(p, offset = 1); {:yj, exponent}, the Yeo-Johnson
transformation, R's transform_yj(p); :timespan and {:timespan, unit}, a number counted in
unit, :second to :week, as its seconds, R's transform_timespan(unit = "secs"); :date,
a Date as its days since 1970-01-01, R's transform_date(); {:datetime, zone, database}, a
DateTime or NaiveDateTime as its seconds since 1970-01-01 00:00 UTC, its breaks laid on
the clock of zone, read by the time zone database database, R's transform_time(tz), the
zone :default until a column settles it and :naive for naive date-times; :time, a
Time as its seconds since midnight, R's transform_hms(); {:compose, [first, second, ...]}, each in turn, R's transform_compose(); and a transformation of your own, t/0,
made by new/2.
A base is a number above 1, a sigma, a standard deviation or a scale a number above 0, a
mean, a location or an exponent a number, a Box-Cox offset a number from 0 up and a modulus
offset one from 1 up, and a name holding any other raises FunctionClauseError wherever it is
handed, where R also takes a base below 1, falling, and gives no axis at 1 or a scale of 0, and a
modulus offset below 1, which no longer rises. A falling axis is the transformation in a base
above 1 reversed.
@type t() :: %Scales.Transform{ breaks: ({float(), float()} -> [number()]) | ({float(), float()}, keyword() -> [number()]), derivative: (term() -> term()) | nil, domain: {number() | :neg_infinity, number() | :infinity}, inverse: (term() -> term()), inverse_derivative: (term() -> term()) | nil, labels: ([term()] -> [String.t()]), minor_breaks: ([float()], {float(), float()} -> [float()]), name: String.t(), transform: (term() -> term()) }
A transformation of your own, made by new/2: its name, its functions and its domain.
Functions
The default breaks, in transformed space, of an axis showing extent under the transformation
name, in the order the transformation leaves them, some outside the extent, those at an end
of a domain transformed to an infinity there, as logit's 0 and 1, which no extent reaches:
extended breaks (Scales.Breaks) on the extent in data units, powers of the base and their multiples under a logarithm in a base
(Scales.Breaks.log/2, so powers of e under :log), calendar dates under the date
transformation and clock times under the date-time one, as R's pretty() finds them, and
spans of time under the time and span of time ones, as Scales.Breaks.timespan/2 finds them.
Under the Box-Cox, modulus and Yeo-Johnson transformations an end of the extent that turns back
to an infinity, past a bound their values never reach, is set aside and the breaks found on the
other end alone, as R's sit there; none where both do.
n: asks for that many: of Scales.Breaks.log/2 under a logarithm in a base, of R's
pretty() under the date and date-time transformations, and of Scales.Breaks.extended/2
under the rest. The time and span of time transformations count no breaks, as R's take no
number, and raise ArgumentError for it; Scales.Breaks.timespan/2 takes one.
Options
:n(non_neg_integer/0) - The number of breaks wanted, five when not given, for every transformation but:timeand a span of time's: from 1 for the logarithms in a base (:log,:log2,:log10,{:log, base}), from 0 for dates and date-times, and from 2 for the rest, as R's own breaks take it.
Examples
iex> Scales.Transform.breaks(:identity, {9.225, 35.075})
[10.0, 15.0, 20.0, 25.0, 30.0, 35.0]
iex> Scales.Transform.breaks(:identity, {9.225, 35.075}, n: 10)
[10.0, 12.5, 15.0, 17.5, 20.0, 22.5, 25.0, 27.5, 30.0, 32.5, 35.0]
iex> Scales.Transform.breaks(:log10, {0.0, 3.0})
[0.0, 1.0, 2.0, 3.0]
iex> Scales.Transform.breaks(:log10, {0.0, 6.0}, n: 2)
[0.0, 4.0]
@spec derivative(name(), [Scales.double()]) :: [Scales.double()]
The derivative of the transformation name at each of values, R's d_transform, each a
double (Scales.double/0), its infinities and NaN as R's arithmetic gives them. Only a
transformation of numbers has one: a date's, date-time's, time's or span of time's raises
FunctionClauseError.
Examples
iex> Scales.Transform.derivative(:sqrt, [0.25, 0, -1, nil])
[1.0, :infinity, :nan, nil]
iex> Scales.Transform.derivative(:identity, [3, nil])
[1.0, 1.0]
The default labels of breaks, in transformed space, under the transformation name: its own
breaks on an axis showing extent, found with options, breaks/3's, or, for :given, breaks
it did not choose, the extent squished into the domain as for breaks/3. A number's are
Scales.Labels.plain/1's, written from the breaks turned back, but the Box-Cox, modulus and
Yeo-Johnson transformations write each of their own from the value in data units it was found
from, since the round trip can keep as few as three of its digits, and a span of time's are
Scales.Labels.timespan/2's; a date's in the format of the step its breaks were chosen at, so a date's or date-time's
breaks found with n: are labelled when given the same n:, and a given date written in full.
Options
breaks/3's.
Examples
iex> Scales.Transform.format(:log10, [0.0, 1.0, 2.0], {0.0, 2.0})
["1", "10", "100"]
iex> extent = {19723.0, 19726.0}
iex> Scales.Transform.format(:date, Scales.Transform.breaks(:date, extent), extent)
["Dec 31", "Jan 01", "Jan 02", "Jan 03", "Jan 04", "Jan 05"]
iex> Scales.Transform.format(:date, [19723.0], :given)
["2024-01-01"]
iex> extent = {18262.0, 18993.0}
iex> breaks = Scales.Transform.breaks(:date, extent, n: 20)
iex> :date |> Scales.Transform.format(breaks, extent, n: 20) |> Enum.take(3)
["Jan 2020", "Feb 2020", "Mar 2020"]
Positions in transformed space turned back into data units by the transformation name.
Examples
iex> Scales.Transform.inverse(:log10, [0.0, 1.0, 2.0])
[1.0, 10.0, 100.0]
@spec inverse_derivative(name(), [Scales.double()]) :: [Scales.double()]
The derivative of the inverse of the transformation name at each of positions, in
transformed space, R's d_inverse, as derivative/2 gives the transformation's.
Examples
iex> Scales.Transform.inverse_derivative(:sqrt, [0.5, -2, :infinity])
[1.0, -4.0, :infinity]
The default minor breaks between breaks on an axis showing extent under the transformation
name, in transformed space: Scales.MinorBreaks.regular/3's, reversed for :reverse.
Examples
iex> Scales.Transform.minor_breaks(:identity, [10.0, 20.0, 30.0], {5.0, 35.0})
[0.0, 5.0, 10.0, 15.0, 20.0, 25.0, 30.0, 35.0, 40.0]
A transformation of your own named name, R's new_transform(), from its functions, each of
one value: given wherever a transformation's name is, and as a composition's part. Its domain
is the whole line unless domain: says, its breaks Scales.Breaks.extended/2's on an axis's
limits in data units, its minor breaks Scales.MinorBreaks.regular/3's and its labels
Scales.Labels.plain/1's of the breaks turned back, each rounded to 14 significant digits
first, unless breaks:, minor_breaks: or labels: say. A missing function, an unknown option, a function of another arity or a domain
not {low, high} raises NimbleOptions.ValidationError.
It is handed every value in its domain, the domain's ends among them, as the module
documentation describes, and its inverse every position but nil and :nan: on the whole
line it handles :infinity and :neg_infinity.
Options
:transform(function of arity 1) - Required. The transformation, of one value: a number, an integer as a float, or an infinity in the domain, to a number,:infinity,:neg_infinity,:nanornil.:inverse(function of arity 1) - Required. Its inverse, of one position in transformed space, returning as it does.:derivative(function of arity 1) - Its derivative, of one value, forderivative/2; without one,derivative/2raises.:inverse_derivative(function of arity 1) - Its inverse's derivative, of one position, forinverse_derivative/2; without one,inverse_derivative/2raises.:domain- The values it takes,{low, high}in data units, each a number or an infinity,lowbelowhigh. The default value is{:neg_infinity, :infinity}.:breaks- An axis's breaks, from its limits in data units,{low, high}, to breaks in data units; one of two arguments is also handedbreaks/3's options, son:reaches it, and one of one argument takes none. The default value is&Scales.Breaks.extended/2.:minor_breaks(function of arity 2) - An axis's minor breaks, from its breaks and its extent, both in transformed space, to minor breaks there. The default value is&Scales.MinorBreaks.regular/2.:labels(function of arity 1) - An axis's labels, from its breaks turned back into data units; each rounded to 14 significant digits and written asScales.Labels.plain/1writes it when not given, since an inverse rarely gives a break back exactly.
Examples
iex> square_root =
...> Scales.Transform.new("square root",
...> transform: &:math.sqrt/1,
...> inverse: fn position -> position * position end,
...> domain: {0, 1.0e300}
...> )
iex> Scales.Transform.transform(square_root, [4, -1, nil])
[2.0, :nan, nil]
The extent {low, high}, in transformed space, squished into the domain of the transformation
name, transformed: each end outside it moved onto it, as R's oob_squish() moves it, so
log10's breaks are not sought below -100, and onto a bound the values of a transformation of
every number never pass, the Yeo-Johnson transformation's 1 for an exponent of -1. A date's,
date-time's or time's is never squished.
Examples
iex> Scales.Transform.squish(:log10, {-200.0, 2.0})
{-100.0, 2.0}
The values put through the transformation name.
Examples
iex> Scales.Transform.transform(:log10, [1, 10, 100, 0, -1, nil])
[0.0, 1.0, 2.0, :neg_infinity, :nan, nil]
iex> Scales.Transform.transform(:date, [~D[1970-01-11], nil])
[10.0, nil]
@spec trim_to_domain(name(), [Scales.double()]) :: {Scales.double(), Scales.double()} | :none
The range values take in transformed space under the transformation name, R's
trim_to_domain(): their range, nil and :nan left out, squished into the domain and its
ends transformed, as {low, high}; :none with no value to range over, where R warns. Only a
transformation of numbers takes one: a date's, date-time's or time's raises
FunctionClauseError.
The range is taken before the transformation, so the reciprocal of a range across zero is the
reciprocals of its ends, and an infinity is kept, not squished, so one outside the domain
transforms to :nan, which makes both ends :nan, as R's range() makes them.
Examples
iex> Scales.Transform.trim_to_domain(:log10, [-5, 0, 10, 1000, nil])
{-100.0, 3.0}
iex> Scales.Transform.trim_to_domain(:reciprocal, [-5, 0, 10, 1000])
{-0.2, 0.001}
iex> Scales.Transform.trim_to_domain(:log10, [:neg_infinity, 5])
{:nan, :nan}
iex> Scales.Transform.trim_to_domain(:sqrt, [nil, :nan])
:none
@spec width( :date | {:datetime, term(), module()} | :time | {:compose, [name(), ...]}, keyword(pos_integer()), {float(), float()} ) :: [float()]
The breaks a step lays, written as Date.shift/2 takes one ([week: 2]), on an axis showing
extent under the date, date-time or time transformation name, in transformed space, some
outside it, as R's breaks_width() lays them: from the extent's start rounded down to the
step's unit (a week to its Monday), every step. Only those three transformations have steps.
Examples
iex> Scales.Transform.width(:date, [week: 2], {19723.0, 19750.0})
[19723.0, 19737.0, 19751.0]