Scales.Labels (scales v0.1.0)

Copy Markdown View Source

Labels for a scale's breaks: numbers written plainly (plain/1), in decimals (number/2), with a comma between thousands (comma/2), as percentages (percent/2), as amounts of money (currency/2), in scientific notation (scientific/2), in whichever way suits their size (number_auto/1), as powers (log/2, or as text, format_log/2), as bytes (bytes/2), as ordinals (ordinal/2), as p-values (pvalue/2), as spans of time (timespan/2), and dates, date-times and times of day in a format (date/2, time/2) or by what changed from the break before (date_short/2); and text broken into lines (wrap/2) or looked up in a dictionary (dictionary/2).

Each labeller is handed every break an axis or a legend draws and returns a label for each, in order, so the labels can share their decimals: 0.5, 1 and 1.25 are labelled 0.50, 1.00 and 1.25. A label is text, but for log/2's powers (power_label/0). A missing break, nil, has no label, nil, as R's NA has none, but where number_auto/1 writes it "NA". One that takes options is handed on inside a function:

fn breaks -> Scales.Labels.comma(breaks, accuracy: 0.1) end

Labellers are composed by piping one's labels into the next, where R composes them with compose_label():

fn breaks ->
  breaks
  |> Scales.Labels.number(accuracy: 0.1)
  |> Enum.map(fn label -> label && label <> " m" end)
end

plain/1 writes numbers as Elixir shows them, with what an axis read at a glance needs on top; the others write them as R's scales does, each a label to the character.

How a label is written

Each number is multiplied by scale:, then rounded to a multiple of accuracy:, a half going to the even neighbour (2.5 to 2, 3.5 to 4). A number that rounds to zero has no sign. One that scale: takes past the largest float is labelled Inf or -Inf, without prefix: and suffix: where rounding takes it past (and inside them where only the scaling does), and takes no part in the others' accuracy, cents or step. It is written in plain decimals, with as many decimals as the accuracy has (0.01 writes two, 1 or more none) up to twenty, or more if a label needs them to show its first seven significant digits (an accuracy of 0.25 writes 0.25 and 0.50); every label shares the most any needs. Only labels more than 310 characters longer than their scientific form, past about 1e290, are written in scientific notation, as R writes them. big_mark: goes between each three digits of the whole part and decimal_mark: before the decimals; then prefix: and suffix: go around the number, and the sign outside the prefix (-$13.50).

Left out, the accuracy is worked out from the breaks themselves: the power of ten at or below the smallest gap between two of them when every break is a multiple of it, a tenth of it otherwise, and never more than 1. One break, or breaks closer than about 1.5e-8, take an accuracy of 1.

Every formatter takes the same options, listed with its own defaults in its documentation.

Shortening by steps

scale_cut: divides each label by the largest step it reaches and writes the step's suffix after the number, before suffix:: under cut_short_scale/1, 1500 is written 1.5K and two million 2M. A label that is not a whole number of its step, but is a whole number of the step below, is written in the step below when the two steps are not a power of ten apart: under cut_time_scale/1, 90 seconds is 90s, not 1.5m. Labels in each step share their decimals, worked out from that step's labels alone when accuracy: is not given. The steps must include one at threshold 0, whose suffix is written after the labels below every other step, and no threshold may be negative or repeated. cut_short_scale/1, cut_long_scale/1, cut_si/1 and cut_time_scale/1 give the common sets.

Summary

Types

A power log/2 labels a break with, its base and its exponent each written as text: 10^3 is {:power, "10", "3"}.

A label of log/2: a power, a power with a sign before it (-10^3 is {:signed, "-", {:power, "10", "3"}}), the text "0" for a signed zero, or nil for a missing break.

A step for scale_cut:: the suffix written after a label, and the threshold it starts at.

Functions

Labels numbers of bytes, R's label_bytes(): in the unit that suits each, 1500 is 2 kB, or all in one unit.

Labels numbers with a comma between each three digits of the whole part, R's label_comma().

Labels amounts of money, R's label_currency(), in dollars unless prefix: and suffix: say otherwise.

The long scale's steps for scale_cut:, R's cut_long_scale(): thousands K, millions M, billions B (1e12) and trillions T (1e18).

The short scale's steps for scale_cut:, R's cut_short_scale(): thousands K, millions M, billions B (1e9) and trillions T (1e12).

The SI prefixes' steps for scale_cut:, R's cut_si(), from yocto (1e-24) to yotta (1e24), each written with a space and unit after it (" kg"), and unit alone below a yocto and from 1 to 1000.

Steps of time for scale_cut:, R's cut_time_scale(), for labels in seconds: nanoseconds ns, microseconds μs, milliseconds ms, seconds s, minutes m, hours h, days d and weeks w.

Labels dates and date-times in a format, R's label_date(), by Calendar.strftime/3, but for %Y, the year unpadded (the year 900 is 900).

Labels dates and date-times with what changed from the break before, R's label_date_short(): each part, a year, a month, a day and a time of day, written where it or a coarser part changed, finest first, one part to a line.

Labels values by a dictionary, R's label_dictionary(): each value the dictionary holds takes its label, and each it lacks, a missing one included, nomatch:'s; with no nomatch:, a string it lacks is its own label and a missing value stays missing.

Writes numbers as powers of base: in text, R's format_log(): log/2's labels each written base^exponent, its exponent as the exponents are written together, padded on the left to the widest, in up to digits: significant digits.

Labels numbers as powers of base:, R's label_log(): 1000 is {:power, "10", "3"}, drawn as a superscript.

Labels numbers in decimals, R's label_number(), with a space between each three digits of the whole part.

Labels numbers in whichever way suits their size, R's label_number_auto().

Labels numbers as ordinals, R's label_ordinal(): 1st, 2nd, 3rd, 4th.

English ordinals' rules for ordinal/2, R's ordinal_english(): st after a 1, nd after a 2 and rd after a 3, but for 11, 12 and 13, and th after anything else.

Labels proportions as percentages, R's label_percent(): each is multiplied by 100 and written with a %.

Labels numbers plainly, as Elixir shows a float, with what an axis read at a glance needs on top: every label gets as many decimals as the most precise one needs, none when every break is whole, never scientific notation and no thousands separators. Each break is first rounded to 15 significant digits, which drops the rounding error that adding up steps leaves in the last digits (0.3, not 0.30000000000000004), and a negative zero is written 0. R's default labels come from format() instead, which writes 1e+05 where this writes 100000.

Labels p-values, R's label_pvalue(): each rounded to accuracy: as number/2 rounds it, one below the accuracy written as less than it (<0.001) and one above 1 less the accuracy as more than that (>0.999).

Labels numbers in scientific notation, R's label_scientific().

Labels times of day and date-times in a format, R's label_time(), by Calendar.strftime/3.

Labels spans of time, R's label_timespan(): each break, counted in unit:, turned into seconds and written as number/2 writes it under cut_time_scale/1's steps, 90 minutes being 90m and 120 2h.

Breaks text into lines shorter than width: columns, R's label_wrap(), as R's strwrap() breaks it, the lines joined by "\n". A missing label, nil, is written "NA".

Types

power()

@type power() :: {:power, String.t(), String.t()}

A power log/2 labels a break with, its base and its exponent each written as text: 10^3 is {:power, "10", "3"}.

power_label()

@type power_label() :: power() | {:signed, String.t(), power()} | String.t() | nil

A label of log/2: a power, a power with a sign before it (-10^3 is {:signed, "-", {:power, "10", "3"}}), the text "0" for a signed zero, or nil for a missing break.

step()

@type step() :: {String.t(), number()}

A step for scale_cut:: the suffix written after a label, and the threshold it starts at.

Functions

bytes(breaks, options \\ [])

@spec bytes([number() | nil], keyword()) :: [String.t() | nil]

Labels numbers of bytes, R's label_bytes(): in the unit that suits each, 1500 is 2 kB, or all in one unit.

Each label is written as number/2 writes it, the unit its suffix. Under :auto_si and :auto_binary, each break takes the largest unit it reaches, as scale_cut: does, and plain bytes are B, a break that is not a whole number of its unit but is of plain bytes written in bytes (1500 is 1 500 B under :auto_binary); in one unit, scale: is divided by the unit's size. Labels are rounded to whole units unless accuracy: says otherwise.

Options

  • :units - :auto_si, each label in the SI unit, a power of 1000, that suits it; :auto_binary, in the binary unit, a power of 1024; or a unit every label is written in, spelt as R spells it: "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB", "kiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB". The default value is :auto_si.

  • :accuracy - A positive number each label is rounded to a multiple of, in its unit, or nil to work it out from the breaks (see the module's documentation). The default value is 1.

  • :scale - A positive number each break is multiplied by before it is written. The default value is 1.

  • :prefix (String.t/0) - Text written before the number, after a negative's sign. The default value is "".

  • :big_mark (String.t/0) - Text written between each three digits of the whole part. The default value is " ".

  • :decimal_mark - Text written before the decimals, at least one character. The default value is ".".

  • :trim (boolean/0) - false pads each number on the left with spaces to the width of the widest, before its prefix and its sign: "-$ 1" beside "$1,000". The default value is true.

  • :style_negative - How a negative is marked: :hyphen (-1), :minus, a true minus sign (−1), or :parens ((1)). The default value is :hyphen.

  • :style_positive - How a positive is marked: :none (1), :plus (+1) or :space, a figure space as wide as a digit ("\u20071"). The default value is :none.

Examples

iex> Scales.Labels.bytes([0, 1500, 2.0e6, nil])
["0 B", "2 kB", "2 MB", nil]

iex> Scales.Labels.bytes([512, 1024, 1_048_576], units: :auto_binary)
["512 B", "1 kiB", "1 MiB"]

iex> Scales.Labels.bytes([1500, 2500, 1.0e6], accuracy: nil)
["1.5 kB", "2.5 kB", "1 MB"]

iex> Scales.Labels.bytes([1.0e6, 2.5e6], units: "kB", big_mark: ",")
["1,000 kB", "2,500 kB"]

comma(breaks, options \\ [])

@spec comma([number() | nil], keyword()) :: [String.t() | nil]

Labels numbers with a comma between each three digits of the whole part, R's label_comma().

Options

  • :accuracy - A positive number each label is rounded to a multiple of, in the label's own units, after scale:; worked out from the breaks when not given (see the module's documentation).

  • :scale - A positive number each break is multiplied by before it is written. The default value is 1.

  • :prefix (String.t/0) - Text written before the number, after a negative's sign. The default value is "".

  • :suffix (String.t/0) - Text written after the number. The default value is "".

  • :big_mark (String.t/0) - Text written between each three digits of the whole part. The default value is ",".

  • :decimal_mark - Text written before the decimals, at least one character. The default value is ".".

  • :trim (boolean/0) - false pads each number on the left with spaces to the width of the widest, before its prefix and its sign: "-$ 1" beside "$1,000". The default value is true.

  • :style_negative - How a negative is marked: :hyphen (-1), :minus, a true minus sign (−1), or :parens ((1)). The default value is :hyphen.

  • :style_positive - How a positive is marked: :none (1), :plus (+1) or :space, a figure space as wide as a digit ("\u20071"). The default value is :none.

  • :scale_cut - Steps to shorten each label by, a list of {suffix, threshold} pairs (see the module's documentation).

Examples

iex> Scales.Labels.comma([-100_000, 1_234_567.891, 0.125])
["-100,000", "1,234,568", "0"]

iex> Scales.Labels.comma([0.5, 1, 1.25])
["0.50", "1.00", "1.25"]

iex> Scales.Labels.comma([0, 1250, 2500], accuracy: 0.1, scale: 0.001, suffix: "k")
["0.0k", "1.2k", "2.5k"]

iex> Scales.Labels.comma([0, 1500, 1.0e6], scale_cut: Scales.Labels.cut_short_scale())
["0", "2K", "1M"]

currency(breaks, options \\ [])

@spec currency([number() | nil], keyword()) :: [String.t() | nil]

Labels amounts of money, R's label_currency(), in dollars unless prefix: and suffix: say otherwise.

Without accuracy: or scale_cut:, labels show cents when some amount is not whole and none is larger than largest_with_fractional:, and whole amounts otherwise. When both marks would be a comma, the thousands are marked with a space instead.

Options

  • :accuracy - A positive number each label is rounded to a multiple of, in the label's own units, after scale:; worked out from the breaks when not given (see the module's documentation).

  • :scale - A positive number each break is multiplied by before it is written. The default value is 1.

  • :prefix (String.t/0) - Text written before the number, after a negative's sign. The default value is "$".

  • :suffix (String.t/0) - Text written after the number. The default value is "".

  • :big_mark (String.t/0) - Text written between each three digits of the whole part. The default value is ",".

  • :decimal_mark - Text written before the decimals, at least one character. The default value is ".".

  • :trim (boolean/0) - false pads each number on the left with spaces to the width of the widest, before its prefix and its sign: "-$ 1" beside "$1,000". The default value is true.

  • :style_negative - How a negative is marked: :hyphen (-1), :minus, a true minus sign (−1), or :parens ((1)). The default value is :hyphen.

  • :style_positive - How a positive is marked: :none (1), :plus (+1) or :space, a figure space as wide as a digit ("\u20071"). The default value is :none.

  • :scale_cut - Steps to shorten each label by, a list of {suffix, threshold} pairs (see the module's documentation).

  • :largest_with_fractional - The largest amount that shows cents when neither accuracy: nor scale_cut: is given. The default value is 100000.

Examples

iex> Scales.Labels.currency([0, 50, 100])
["$0", "$50", "$100"]

iex> Scales.Labels.currency([-13.5, 0, 13.5])
["-$13.50", "$0.00", "$13.50"]

iex> Scales.Labels.currency([1234.5, 1], big_mark: ".", decimal_mark: ",", prefix: "", suffix: " €")
["1.234,50 €", "1,00 €"]

iex> Scales.Labels.currency([0, 2.5e5, 5.0e5, 1.0e6], scale_cut: Scales.Labels.cut_short_scale())
["$0", "$250K", "$500K", "$1M"]

cut_long_scale(options \\ [])

@spec cut_long_scale(keyword()) :: [step()]

The long scale's steps for scale_cut:, R's cut_long_scale(): thousands K, millions M, billions B (1e12) and trillions T (1e18).

Options

  • :space (boolean/0) - true puts a space before each suffix (2 K). The default value is false.

Examples

iex> Scales.Labels.currency([0, 1.0e9, 2.0e12], scale_cut: Scales.Labels.cut_long_scale())
["$0", "$1,000M", "$2B"]

cut_short_scale(options \\ [])

@spec cut_short_scale(keyword()) :: [step()]

The short scale's steps for scale_cut:, R's cut_short_scale(): thousands K, millions M, billions B (1e9) and trillions T (1e12).

Options

  • :space (boolean/0) - true puts a space before each suffix (2 K). The default value is false.

Examples

iex> Scales.Labels.cut_short_scale()
[{"", 0}, {"K", 1000}, {"M", 1000000}, {"B", 1000000000}, {"T", 1000000000000}]

iex> Scales.Labels.comma([-2000, 0, 2000], scale_cut: Scales.Labels.cut_short_scale(space: true))
["-2 K", "0 ", "2 K"]

cut_si(unit)

@spec cut_si(String.t()) :: [step()]

The SI prefixes' steps for scale_cut:, R's cut_si(), from yocto (1e-24) to yotta (1e24), each written with a space and unit after it (" kg"), and unit alone below a yocto and from 1 to 1000.

Examples

iex> Scales.Labels.comma([0.001, 1, 1500], scale_cut: Scales.Labels.cut_si("g"))
["1 mg", "1 g", "2 kg"]

cut_time_scale(options \\ [])

@spec cut_time_scale(keyword()) :: [step()]

Steps of time for scale_cut:, R's cut_time_scale(), for labels in seconds: nanoseconds ns, microseconds μs, milliseconds ms, seconds s, minutes m, hours h, days d and weeks w.

Options

  • :space (boolean/0) - true puts a space before each suffix (2 K). The default value is false.

Examples

iex> Scales.Labels.comma([0, 45, 90, 120, 5400], scale_cut: Scales.Labels.cut_time_scale())
["0", "45s", "90s", "2m", "90m"]

date(breaks, options \\ [])

@spec date([Date.t() | NaiveDateTime.t() | DateTime.t() | nil], keyword()) :: [
  String.t() | nil
]

Labels dates and date-times in a format, R's label_date(), by Calendar.strftime/3, but for %Y, the year unpadded (the year 900 is 900).

A DateTime is shifted into tz: first, UTC unless given; a NaiveDateTime is written as it is, its zone UTC, and a Date at midnight UTC, so %H writes 00 and %Z UTC.

Options

  • :format - The format each break is written in, as Calendar.strftime/3 takes it, but for %Y, the year unpadded. The default value is "%Y-%m-%d".

  • :tz (String.t/0) - The time zone each DateTime is shifted into before it is written, one time_zone_database: knows; a Date is written at midnight UTC and a NaiveDateTime or Time as it is. The default value is "Etc/UTC".

  • :time_zone_database (atom/0) - The time zone database that reads tz:, such as Tz.TimeZoneDatabase; Elixir's own, which knows UTC alone, when not given. The default value is Calendar.UTCOnlyTimeZoneDatabase.

Examples

iex> Scales.Labels.date([~D[2020-01-01], nil, ~D[2021-06-15]])
["2020-01-01", nil, "2021-06-15"]

iex> Scales.Labels.date([~N[2020-03-05 14:07:09]], format: "%d %b %H:%M")
["05 Mar 14:07"]

iex> Scales.Labels.date([~U[2021-03-14 06:30:00Z], ~U[2021-03-14 07:30:00Z]],
...>   format: "%H:%M %Z",
...>   tz: "America/New_York",
...>   time_zone_database: Tz.TimeZoneDatabase
...> )
["01:30 EST", "03:30 EDT"]

date_short(breaks, options \\ [])

@spec date_short([Date.t() | NaiveDateTime.t() | DateTime.t() | nil], keyword()) :: [
  String.t() | nil
]

Labels dates and date-times with what changed from the break before, R's label_date_short(): each part, a year, a month, a day and a time of day, written where it or a coarser part changed, finest first, one part to a line.

The first break, and one after a missing break, show every part. The time is left out where every break is at midnight, then the day where every break is a 1st, then the month where every break is in January. A break with no part to write, as a date repeated, is written in full, 2020-01-05, or 2020-01-05 10:00:00 where one such break is not at midnight in tz:.

What changed is read on each break's own clock, a DateTime in its own zone; each part is written as date/2 writes it, a DateTime shifted into tz:.

Options

  • :format (keyword/0) - Each part's format, as Calendar.strftime/3 takes it, but for %Y, the year unpadded; a part left out keeps its default. The default value is [].

    • :year - The year's format, or nil for a year never written. The default value is "%Y".

    • :month - The month's format, or nil for a month never written. The default value is "%b".

    • :day - The day's format, or nil for a day never written. The default value is "%d".

    • :time - The time's format, or nil for a time never written. The default value is "%H:%M".

  • :sep (String.t/0) - The text written between the parts. The default value is "\n".

  • :leading (String.t/0) - The text that replaces a 0 at the start of each part: " " writes the 5th " 5", "" writes it "5". The default value is "0".

  • :tz (String.t/0) - The time zone each DateTime is shifted into before it is written, one time_zone_database: knows; a Date is written at midnight UTC and a NaiveDateTime or Time as it is. The default value is "Etc/UTC".

  • :time_zone_database (atom/0) - The time zone database that reads tz:, such as Tz.TimeZoneDatabase; Elixir's own, which knows UTC alone, when not given. The default value is Calendar.UTCOnlyTimeZoneDatabase.

Examples

iex> Scales.Labels.date_short([~D[2019-11-01], ~D[2019-12-01], ~D[2020-01-01], ~D[2020-02-01]])
["Nov\n2019", "Dec", "Jan\n2020", "Feb"]

iex> Scales.Labels.date_short([~N[2020-01-01 18:00:00], ~N[2020-01-01 21:00:00], ~N[2020-01-02 00:00:00]])
["18:00\n01\nJan\n2020", "21:00", "00:00\n02"]

iex> Scales.Labels.date_short([~D[2020-01-05], ~D[2020-01-06], ~D[2020-02-07]],
...>   format: [month: "%B"],
...>   sep: " ",
...>   leading: ""
...> )
["5 January 2020", "6", "7 February"]

dictionary(breaks, options)

@spec dictionary([term()], keyword()) :: [String.t() | nil]

Labels values by a dictionary, R's label_dictionary(): each value the dictionary holds takes its label, and each it lacks, a missing one included, nomatch:'s; with no nomatch:, a string it lacks is its own label and a missing value stays missing.

A value is matched by Scales.DataRange.key/1, so a key of 2 names 2.0, and strings match exactly, case and accents as written; a key matches a value of its own type, so "1" names the string and never the number 1. A value that is not a string, which the dictionary lacks, needs a nomatch:, or raises.

Options

  • :dictionary - Required. A map of values to their labels, each a string or nil for none.

  • :nomatch - The label of a value the dictionary lacks, a missing one included: a string, or nil for none; :none keeps a string as its own label and a missing value missing. The default value is :none.

Examples

iex> Scales.Labels.dictionary(["a", "b", "c", nil], dictionary: %{"a" => "Apple", "b" => "Banana"})
["Apple", "Banana", "c", nil]

iex> Scales.Labels.dictionary(["a", "c", nil], dictionary: %{"a" => "Apple"}, nomatch: "?")
["Apple", "?", "?"]

iex> Scales.Labels.dictionary([4.0, 6.0, 8.0], dictionary: %{4 => "four", 6 => "six", 8 => "eight"})
["four", "six", "eight"]

format_log(breaks, options \\ [])

@spec format_log([number() | nil], keyword()) :: [String.t() | nil]

Writes numbers as powers of base: in text, R's format_log(): log/2's labels each written base^exponent, its exponent as the exponents are written together, padded on the left to the widest, in up to digits: significant digits.

Options

  • :base - The base of the logarithm, a positive number other than 1. The default value is 10.

  • :signed - Whether each label is signed, + or - before its power and a zero "0"; :auto signs them when any break is zero or negative. Unsigned, a break at or below zero raises. The default value is :auto.

  • :digits - An integer from 1 to 22, the most significant digits shown, though no label shows more than 7 (see the function's documentation). The default value is 7.

Examples

iex> Scales.Labels.format_log([0.01, 1, 100])
["10^-2", "10^ 0", "10^ 2"]

iex> Scales.Labels.format_log([0.5, 2, 8], digits: 3)
["10^-0.301", "10^ 0.301", "10^ 0.903"]

iex> Scales.Labels.format_log([-100, 0, 100])
["-10^2", "0", "+10^2"]

log(breaks, options \\ [])

@spec log([number() | nil], keyword()) :: [power_label()]

Labels numbers as powers of base:, R's label_log(): 1000 is {:power, "10", "3"}, drawn as a superscript.

Each break's exponent, its logarithm in base:, is rounded to the seventh significant digit of the largest exponent's, then the exponents are written together in up to digits: significant digits, sharing their decimals, and each is read back and written alone in up to 15, its zeros after the last significant digit dropped: 2 is {:power, "10", "0.301"}, and 1 beside it {:power, "10", "0"}. No exponent shows more than 7 significant digits. The base is written in up to 15 significant digits too, in plain or scientific notation, whichever is shorter ("2.71828182845905" for e, "1e+06").

Signed, each power has its sign before it, {:signed, "-", power} or {:signed, "+", power}, and a zero is labelled "0". A missing break has no label.

Options

  • :base - The base of the logarithm, a positive number other than 1. The default value is 10.

  • :signed - Whether each label is signed, + or - before its power and a zero "0"; :auto signs them when any break is zero or negative. Unsigned, a break at or below zero raises. The default value is :auto.

  • :digits - An integer from 1 to 22, the most significant digits shown, though no label shows more than 7 (see the function's documentation). The default value is 3.

Examples

iex> Scales.Labels.log([0.01, 1, 100])
[{:power, "10", "-2"}, {:power, "10", "0"}, {:power, "10", "2"}]

iex> Scales.Labels.log([1, 2, 8], base: 2)
[{:power, "2", "0"}, {:power, "2", "1"}, {:power, "2", "3"}]

iex> Scales.Labels.log([-100, 0, 100])
[{:signed, "-", {:power, "10", "2"}}, "0", {:signed, "+", {:power, "10", "2"}}]

number(breaks, options \\ [])

@spec number([number() | nil], keyword()) :: [String.t() | nil]

Labels numbers in decimals, R's label_number(), with a space between each three digits of the whole part.

Options

  • :accuracy - A positive number each label is rounded to a multiple of, in the label's own units, after scale:; worked out from the breaks when not given (see the module's documentation).

  • :scale - A positive number each break is multiplied by before it is written. The default value is 1.

  • :prefix (String.t/0) - Text written before the number, after a negative's sign. The default value is "".

  • :suffix (String.t/0) - Text written after the number. The default value is "".

  • :big_mark (String.t/0) - Text written between each three digits of the whole part. The default value is " ".

  • :decimal_mark - Text written before the decimals, at least one character. The default value is ".".

  • :trim (boolean/0) - false pads each number on the left with spaces to the width of the widest, before its prefix and its sign: "-$ 1" beside "$1,000". The default value is true.

  • :style_negative - How a negative is marked: :hyphen (-1), :minus, a true minus sign (−1), or :parens ((1)). The default value is :hyphen.

  • :style_positive - How a positive is marked: :none (1), :plus (+1) or :space, a figure space as wide as a digit ("\u20071"). The default value is :none.

  • :scale_cut - Steps to shorten each label by, a list of {suffix, threshold} pairs (see the module's documentation).

Examples

iex> Scales.Labels.number([-2.5, 0, 1.0e6])
["-2.5", "0.0", "1 000 000.0"]

iex> Scales.Labels.number([1, nil, 1.5])
["1.0", nil, "1.5"]

iex> Scales.Labels.number([0.001, 0.0025], scale: 1000, suffix: " ms")
["1.0 ms", "2.5 ms"]

number_auto(breaks)

@spec number_auto([number() | nil]) :: [String.t() | nil]

Labels numbers in whichever way suits their size, R's label_number_auto().

The way is chosen by the largest and smallest magnitudes of the breaks other than zero:

  • any above a million: each label in whole numbers with a space between each three digits, as number(breaks, accuracy: 1) writes it, or in scientific notation, whichever is shorter, the whole number on a tie;
  • any below 0.001: each in plain decimals or in scientific notation, whichever is shorter, plain on a tie;
  • all positive, from 1000 to 2200, as years are: in plain decimals, with no mark between thousands;
  • any above 1000: as number(breaks, accuracy: 1) writes them;
  • otherwise in plain decimals, or in scientific notation where that is narrower for all.

Plain decimals and scientific notation are written with every label sharing its decimals, in up to 7 significant digits, each way on its own when the shorter of two is taken. A missing break is labelled with the text "NA" in the years' way and the last, and has no label in the others.

Examples

iex> Scales.Labels.number_auto([0, 2.5e6, 5.0e6])
["0", "2.5e+06", "5.0e+06"]

iex> Scales.Labels.number_auto([1990, 2000, 2010])
["1990", "2000", "2010"]

iex> Scales.Labels.number_auto([0, 5000, 10_000, nil])
["0", "5 000", "10 000", nil]

iex> Scales.Labels.number_auto([0, 0.5, nil])
["0.0", "0.5", "NA"]

ordinal(breaks, options \\ [])

@spec ordinal([number() | nil], keyword()) :: [String.t() | nil]

Labels numbers as ordinals, R's label_ordinal(): 1st, 2nd, 3rd, 4th.

Each break is rounded to a whole number, a half to the even one, and written as number/2 writes it with an accuracy of 1, then the suffix of the first rule whose regex matches the whole number's digits: unmarked and unpadded, a negative's with a - before them ("-12"). R matches the text it writes a number in alone, which for a round number is scientific notation wherever that is shorter (2e+05); the digits are matched here, so a rule reading the last digit sees 200000's 0, and English ordinals from 1e21 up, which R writes ...st from 1e+21, are ...th. A break no rule matches raises, so the rules end with one every break matches, as ordinal_english/0's do.

Options

  • :rules - A list of {suffix, regex} pairs, each break taking the suffix of the first whose regex matches it (see the function's documentation); ordinal_english/0's when not given.

  • :prefix (String.t/0) - Text written before the number, after a negative's sign. The default value is "".

  • :suffix (String.t/0) - Text written after the ordinal's. The default value is "".

  • :big_mark (String.t/0) - Text written between each three digits of the whole part. The default value is " ".

  • :trim (boolean/0) - false pads each number on the left with spaces to the width of the widest, before its prefix and its sign: "-$ 1" beside "$1,000". The default value is true.

  • :style_negative - How a negative is marked: :hyphen (-1), :minus, a true minus sign (−1), or :parens ((1)). The default value is :hyphen.

  • :style_positive - How a positive is marked: :none (1), :plus (+1) or :space, a figure space as wide as a digit ("\u20071"). The default value is :none.

Examples

iex> Scales.Labels.ordinal([1, 2, 3, 4, 11, 12, 13, 21, nil])
["1st", "2nd", "3rd", "4th", "11th", "12th", "13th", "21st", nil]

iex> Scales.Labels.ordinal([1, 1001, 2.5], big_mark: ",", prefix: "#")
["#1st", "#1,001st", "#2nd"]

iex> Scales.Labels.ordinal([1, 2], rules: [{"º", ~r/./}])
["1º", "2º"]

ordinal_english()

@spec ordinal_english() :: [{String.t(), Regex.t()}]

English ordinals' rules for ordinal/2, R's ordinal_english(): st after a 1, nd after a 2 and rd after a 3, but for 11, 12 and 13, and th after anything else.

Examples

iex> Scales.Labels.ordinal_english() |> Enum.map(fn {suffix, _rule} -> suffix end)
["st", "nd", "rd", "th", "th", "th"]

percent(breaks, options \\ [])

@spec percent([number() | nil], keyword()) :: [String.t() | nil]

Labels proportions as percentages, R's label_percent(): each is multiplied by 100 and written with a %.

Thousands are marked with a space (1 234%).

Options

  • :accuracy - A positive number each label is rounded to a multiple of, in the label's own units, after scale:; worked out from the breaks when not given (see the module's documentation).

  • :scale - A positive number each break is multiplied by before it is written. The default value is 100.

  • :prefix (String.t/0) - Text written before the number, after a negative's sign. The default value is "".

  • :suffix (String.t/0) - Text written after the number. The default value is "%".

  • :big_mark (String.t/0) - Text written between each three digits of the whole part. The default value is " ".

  • :decimal_mark - Text written before the decimals, at least one character. The default value is ".".

  • :trim (boolean/0) - false pads each number on the left with spaces to the width of the widest, before its prefix and its sign: "-$ 1" beside "$1,000". The default value is true.

  • :style_negative - How a negative is marked: :hyphen (-1), :minus, a true minus sign (−1), or :parens ((1)). The default value is :hyphen.

  • :style_positive - How a positive is marked: :none (1), :plus (+1) or :space, a figure space as wide as a digit ("\u20071"). The default value is :none.

  • :scale_cut - Steps to shorten each label by, a list of {suffix, threshold} pairs (see the module's documentation).

Examples

iex> Scales.Labels.percent([0, 0.25, 0.5, 0.75, 1])
["0%", "25%", "50%", "75%", "100%"]

iex> Scales.Labels.percent([0.1234, 0.5], accuracy: 0.1)
["12.3%", "50.0%"]

iex> Scales.Labels.percent([-0.5, 0.125, 12.345])
["-50%", "12%", "1 234%"]

plain(breaks)

@spec plain([float()]) :: [String.t()]

Labels numbers plainly, as Elixir shows a float, with what an axis read at a glance needs on top: every label gets as many decimals as the most precise one needs, none when every break is whole, never scientific notation and no thousands separators. Each break is first rounded to 15 significant digits, which drops the rounding error that adding up steps leaves in the last digits (0.3, not 0.30000000000000004), and a negative zero is written 0. R's default labels come from format() instead, which writes 1e+05 where this writes 100000.

Examples

iex> Scales.Labels.plain([0.0, 2.5, 5.0])
["0.0", "2.5", "5.0"]

iex> Scales.Labels.plain([0.0, 250_000.0])
["0", "250000"]

pvalue(breaks, options \\ [])

@spec pvalue([number() | nil], keyword()) :: [String.t() | nil]

Labels p-values, R's label_pvalue(): each rounded to accuracy: as number/2 rounds it, one below the accuracy written as less than it (<0.001) and one above 1 less the accuracy as more than that (>0.999).

Options

  • :accuracy - A positive number no larger than 0.5 each label is rounded to a multiple of; below it a p-value is written as less than it, and above 1 less it as more than that. The default value is 0.001.

  • :decimal_mark - Text written before the decimals, at least one character. The default value is ".".

  • :prefix - Three strings, {below, between, above}, written before a p-value below the accuracy, one between, and one above 1 less it; {"<", "", ">"} when not given.

  • :add_p (boolean/0) - true writes the prefixes {"p<", "p=", "p>"}; not taken with prefix:. The default value is false.

Examples

iex> Scales.Labels.pvalue([0.0001, 0.05, 0.5, 0.9999, nil])
["<0.001", "0.050", "0.500", ">0.999", nil]

iex> Scales.Labels.pvalue([0.001, 0.123, 0.999], accuracy: 0.01, add_p: true)
["p<0.01", "p=0.12", "p>0.99"]

iex> Scales.Labels.pvalue([0.0001, 0.05], prefix: {"less than ", "", "more than "})
["less than 0.001", "0.050"]

scientific(breaks, options \\ [])

@spec scientific([number() | nil], keyword()) :: [String.t() | nil]

Labels numbers in scientific notation, R's label_scientific().

Each break is multiplied by scale: and rounded to digits: significant digits, a half to the even neighbour of the number as a float scaled into its digits (0.15 to one digit is 2e-01). The labels are then written together, their mantissas sharing the most decimals any needs, in at most 7 significant digits: digits: above 7 shows 7. They are never padded. A break that scale: takes past the largest float is written Inf or -Inf.

Options

  • :digits - An integer from 1 to 22, the most significant digits shown, though no label shows more than 7 (see the function's documentation). The default value is 3.

  • :scale - A positive number each break is multiplied by before it is written. The default value is 1.

  • :prefix (String.t/0) - Text written before the number. The default value is "".

  • :suffix (String.t/0) - Text written after the number. The default value is "".

  • :decimal_mark - Text written before the decimals, at least one character. The default value is ".".

Examples

iex> Scales.Labels.scientific([0.15, 1.5, 12345])
["1.50e-01", "1.50e+00", "1.23e+04"]

iex> Scales.Labels.scientific([1, 10, nil, 100])
["1e+00", "1e+01", nil, "1e+02"]

iex> Scales.Labels.scientific([1234, 5], decimal_mark: ",", suffix: " m")
["1,23e+03 m", "5,00e+00 m"]

time(breaks, options \\ [])

@spec time([NaiveDateTime.t() | DateTime.t() | Time.t() | nil], keyword()) :: [
  String.t() | nil
]

Labels times of day and date-times in a format, R's label_time(), by Calendar.strftime/3.

A DateTime is shifted into tz: first, UTC unless given; a NaiveDateTime is written as it is, its zone UTC, and a Time as it is. A format writing a part of a date (%Y, %d) raises on a Time.

Options

  • :format - The format each break is written in, as Calendar.strftime/3 takes it, but for %Y, the year unpadded. The default value is "%H:%M:%S".

  • :tz (String.t/0) - The time zone each DateTime is shifted into before it is written, one time_zone_database: knows; a Date is written at midnight UTC and a NaiveDateTime or Time as it is. The default value is "Etc/UTC".

  • :time_zone_database (atom/0) - The time zone database that reads tz:, such as Tz.TimeZoneDatabase; Elixir's own, which knows UTC alone, when not given. The default value is Calendar.UTCOnlyTimeZoneDatabase.

Examples

iex> Scales.Labels.time([~T[00:00:00], ~T[01:01:01], ~T[23:59:59], nil])
["00:00:00", "01:01:01", "23:59:59", nil]

iex> Scales.Labels.time([~T[00:00:59], ~T[00:59:59.9]], format: "%H:%M")
["00:00", "00:59"]

iex> Scales.Labels.time([~U[2020-01-01 03:00:00Z]],
...>   tz: "America/New_York",
...>   time_zone_database: Tz.TimeZoneDatabase
...> )
["22:00:00"]

timespan(breaks, options \\ [])

@spec timespan([number() | nil], keyword()) :: [String.t() | nil]

Labels spans of time, R's label_timespan(): each break, counted in unit:, turned into seconds and written as number/2 writes it under cut_time_scale/1's steps, 90 minutes being 90m and 120 2h.

Options

  • :unit - The unit the breaks are counted in, :second, :minute, :hour, :day or :week. The default value is :second.

  • :space (boolean/0) - true puts a space before each unit's suffix (90 s). The default value is false.

  • :accuracy - A positive number each label is rounded to a multiple of, in the label's own units, after scale:; worked out from the breaks when not given (see the module's documentation).

  • :scale - A positive number each break is multiplied by before it is written. The default value is 1.

  • :prefix (String.t/0) - Text written before the number, after a negative's sign. The default value is "".

  • :suffix (String.t/0) - Text written after the number. The default value is "".

  • :big_mark (String.t/0) - Text written between each three digits of the whole part. The default value is " ".

  • :decimal_mark - Text written before the decimals, at least one character. The default value is ".".

  • :trim (boolean/0) - false pads each number on the left with spaces to the width of the widest, before its prefix and its sign: "-$ 1" beside "$1,000". The default value is true.

  • :style_negative - How a negative is marked: :hyphen (-1), :minus, a true minus sign (−1), or :parens ((1)). The default value is :hyphen.

  • :style_positive - How a positive is marked: :none (1), :plus (+1) or :space, a figure space as wide as a digit ("\u20071"). The default value is :none.

Examples

iex> Scales.Labels.timespan([0, 30, 90, 3600, 86_400])
["0", "30s", "90s", "1h", "1d"]

iex> Scales.Labels.timespan([0.5, 1.5, 36], unit: :hour)
["30m", "90m", "36h"]

iex> Scales.Labels.timespan([1, 60, nil], space: true, suffix: " ago")
["1 s ago", "1 m ago", nil]

wrap(labels, options)

@spec wrap([String.t() | nil], keyword()) :: [String.t()]

Breaks text into lines shorter than width: columns, R's label_wrap(), as R's strwrap() breaks it, the lines joined by "\n". A missing label, nil, is written "NA".

A word goes on the line before it while the line stays shorter than the width, and a word wider than that has a line of its own, never split. Whitespace between words (spaces, tabs and line breaks, nothing else) becomes one space, or two after a word ending a sentence, in ., ? or ! and then at most one of ), " and ', where two or more whitespace characters followed it. A line holding only whitespace starts a new paragraph, an empty line before it.

A character is as wide as R counts it: most Chinese, Japanese and Korean characters, fullwidth forms and emoji take two columns, and combining marks, zero-width spaces and joiners none, so a word made of those alone is dropped as whitespace is.

Options

  • :width (pos_integer/0) - Required. How many columns each line of a label is kept under.

Examples

iex> Scales.Labels.wrap(["Conservation Dependent", "Least concern", nil], width: 10)
["Conservation\nDependent", "Least\nconcern", "NA"]

iex> Scales.Labels.wrap(["Mr.  Smith went  to Washington."], width: 20)
["Mr.  Smith went to\nWashington."]

iex> Scales.Labels.wrap(["日本語 日本語"], width: 10)
["日本語\n日本語"]