# `Scales.Transform`
[🔗](https://github.com/giggleton/scales/blob/v0.1.0/lib/scales/transform.ex#L1)

The transformation a continuous scale puts its values through before it trains on them, named
by a value (`t: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]

# `break`

```elixir
@type break() :: float() | :infinity | :neg_infinity | :nan
```

A break in transformed space: a number, or an infinity or NaN where R's lands one.

# `name`

```elixir
@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: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.

# `t`

```elixir
@type t() :: %Scales.Transform{
  breaks:
    ({float(), float()} -&gt; [number()])
    | ({float(), float()}, keyword() -&gt; [number()]),
  derivative: (term() -&gt; term()) | nil,
  domain: {number() | :neg_infinity, number() | :infinity},
  inverse: (term() -&gt; term()),
  inverse_derivative: (term() -&gt; term()) | nil,
  labels: ([term()] -&gt; [String.t()]),
  minor_breaks: ([float()], {float(), float()} -&gt; [float()]),
  name: String.t(),
  transform: (term() -&gt; term())
}
```

A transformation of your own, made by `new/2`: its name, its functions and its domain.

# `breaks`

```elixir
@spec breaks(name(), {float(), float()}, keyword()) :: [break()]
```

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` (`t:non_neg_integer/0`) - The number of breaks wanted, five when not given, for every transformation but `:time` and 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]

# `derivative`

```elixir
@spec derivative(name(), [Scales.double()]) :: [Scales.double()]
```

The derivative of the transformation `name` at each of `values`, R's `d_transform`, each a
double (`t: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]

# `format`

```elixir
@spec format(name(), [float()], {float(), float()} | :given, keyword()) :: [
  String.t()
]
```

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"]

# `inverse`

```elixir
@spec inverse(name(), [number()]) :: list()
```

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]

# `inverse_derivative`

```elixir
@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]

# `minor_breaks`

```elixir
@spec minor_breaks(name(), [float()], {float(), float()}) :: [float()]
```

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]

# `new`

```elixir
@spec new(String.t(), keyword()) :: t()
```

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`, `:nan` or `nil`.

* `: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, for `derivative/2`; without one, `derivative/2` raises.

* `:inverse_derivative` (function of arity 1) - Its inverse's derivative, of one position, for `inverse_derivative/2`; without one, `inverse_derivative/2` raises.

* `:domain` - The values it takes, `{low, high}` in data units, each a number or an infinity, `low` below `high`. 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 handed `breaks/3`'s options, so `n:` 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 as `Scales.Labels.plain/1` writes 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]

# `squish`

```elixir
@spec squish(name(), {float(), float()}) :: {float(), float()}
```

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}

# `transform`

```elixir
@spec transform(name(), list()) :: list()
```

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]

# `trim_to_domain`

```elixir
@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

# `width`

```elixir
@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]

---

*Consult [api-reference.md](api-reference.md) for complete listing*
