Visualize.Quantity (Visualize v0.4.0)

Copy Markdown View Source

A fixed list of quantities, each with its facts — a base unit, a kind, bounds, additivity and display families by unit system — and the display scaling they choose (spec/05 §4.2–4.3, D-138, D-155).

A quantity is what a unit measures. A unit (a UCUM code, Visualize.Unit) and a quantity together are a measure, %{unit: code, quantity: quantity}, the two facts a field description carries. They are facts about the data, not knobs: a measure does not change a value, it says what the value already is, so a display can choose how to show it — 0–1,500 ms as 0–1.5 s, 2,500,000 By as 2.5 MB, 80 kg in lb for a viewer on the US system.

The list is fixed and grows by proposal: duration, information, data_rate, frequency, ratio, count, temperature, length, speed, power, energy, pressure, area, volume, mass, pace, angle, currency, electric_potential, electric_current, electric_charge, illuminance, sound_level, mass_concentration and amount_concentration. spec/05 §4.2 tables each one's facts and families.

  • Kind is :linear, :affine (a temperature), :logarithmic (a sound level) or :cyclic (an angle, whose mean is circular).
  • Bounds are a display hint, never validation: a difference is legitimately negative, so a value outside them is never refused.
  • Additivity depends on the aggregation: additivity/2 refines the quantity's defaults across series and over time by the field's metric kind (:gauge, :counter, :rate).
  • Unit systems are :metric, :us, :uk and :as_posted; a system picks the family a value is shown in and never touches the data.

Information is decimal by default — UCUM's kBy and Telemetry.Metrics' :kilobyte are 1,000 bytes — and a unit written with a binary prefix (MiBy) or in bit scales in its own family: the data picks it, no setting does.

Tick formats are built on Visualize.Format.Auto (spec/05 §3), and every function that formats takes its locale:.

Examples

iex> Visualize.Quantity.display_unit([0, 1500], %{unit: "ms"})
"s"

iex> Visualize.Quantity.display_unit([0, 80], %{unit: "kg"}, system: :us)
"[lb_av]"

iex> scale = Visualize.Scale.linear() |> Visualize.Scale.domain([0, 1500])
iex> Visualize.Quantity.formatter(scale, %{unit: "ms"}).(1500)
"1.5 s"

iex> Visualize.Quantity.additivity(:data_rate)
%{across: true, over_time: false}

iex> Visualize.Quantity.label(%{unit: "{request}"})
"request"

Summary

Types

How a quantity's values combine (spec/05 §4.2).

A unit and a quantity, either or both; the two facts a field description carries.

How a field's values were measured, which refines its quantity's additivity.

Why a measure does not resolve.

A unit system: a display preference, never applied to the data.

t()

One of the fixed list of quantities.

Functions

Whether a sum of the quantity's values means anything (spec/05 §4.2, D-155): %{across: boolean, over_time: boolean} — summing several series at one instant, and summing one series over time. The quantity's physical defaults are refined by the field's metric kind: a :gauge and a :rate never add over time, and a :counter's increments add over time when the quantity adds across. No quantity (a plain number) defaults to both.

The fixed list of quantities (spec/05 §4.2).

A quantity's base unit, the unit its values are in when no unit is given; nil for a :currency, whose code the data must say.

Where a quantity's values physically lie, in its base unit, {min | nil, max | nil} (spec/05 §4.2): a display hint an axis may anchor at, never validation — a value outside them is never refused.

A quantity's bounds in a unit of it: the base's, converted. A unit without a factor (Cel) takes the base's bounds as they are.

The member of the system's family that fits a domain in a measure (spec/05 §4.3): the largest in which the domain's magnitude is at least 1, else the smallest; a family of one member is that member. A measure with no family under the system, or a unit without a factor (Cel) in its own family, keeps its unit, and so does a zero magnitude when the unit is in the family — otherwise the member nearest it is taken. No unit is nil.

The display family a unit of the quantity belongs to, smallest first; [] for a unit of no family's shape (spec/05 §4.2). A unit belongs to the first family holding a member of its shape — its atoms and exponents, prefixes aside and a binary prefix kept apart — so the unit picks among a quantity's families: bits, binary prefixes, hertz, watt-hours, US lengths. No unit means the base unit.

The display family a system shows a unit of the quantity in (spec/05 §4.2): the family the quantity's table names for :metric, :us or :uk when it names one, else the unit's own (family/2), which is what :as_posted always is.

As format/3 with [] options.

One value of a measure for a label or a tooltip, not an axis (spec/05 §4.3): a :duration in the compound form (1h 24m, 840ms), a :currency as money in its code ($1,240.00, ¥1,240), a :count rounded and grouped (1,240), and anything else to precision decimals, then the unit's suffix (91.40%, 350°, 20.50 kg). Raises ArgumentError for a measure resolve/1 refuses.

The tick formatter of a realised scale in a measure (spec/05 §4.3): each tick scaled into the display unit, written by Visualize.Format.Auto's notation of the scaled domain, then followed by the unit's symbol. A log or symlog scale gives each tick its own display unit; any other scale but the numeric ones is Visualize.Format.Auto.formatter/2, the measure unused. A value ticks/3 placed for the same count and system is labelled with the round number it came from, so the round trip through the data's unit never shows. Raises ArgumentError for a measure resolve/1 refuses.

How a quantity's values combine (spec/05 §4.2): :linear, :affine (a temperature), :logarithmic (a sound level) or :cyclic (an angle).

The label rule (spec/05 §4.3): the unit, else the quantity, else nothing — the unit's symbol with its annotations written when it is not empty, else the quantity's name with spaces for underscores, else nil. Raises ArgumentError for a measure resolve/1 refuses.

The quantity a code's dimension implies, or nil when none of the list has it. A dimensionless code holding % is a :ratio, any other a :count, and a code of one currency alone ({iso4217:USD}) a :currency.

A measure completed and checked (spec/05 §4.2): a unit alone implies its quantity, a quantity alone means its base unit, and a unit whose dimension is not its quantity's is refused. A :ratio or a :count takes any dimensionless unit, and a :currency a code of one currency alone.

What follows a number written in a unit (spec/05 §4.3): its symbol after a space, % and a symbol beginning with / directly, and nothing for an empty symbol or no unit.

The unit systems a display may prefer (spec/05 §4.2), :as_posted the default.

As ticks/3 with [] options.

The tick values of a realised scale in a measure when its display unit is not its unit (spec/05 §4.3, D-156): round numbers in the display unit — Visualize.Scale.ticks/2 of the domain converted into it — each converted back into the data's unit to be placed. nil when the display unit is the unit, for a measure with no unit and for a :currency, and on any scale but Linear, Power and Radial: the scale's own ticks are the axis's. formatter/3 of the same count and system labels each value with its round number. Raises ArgumentError for a measure resolve/1 refuses.

Types

kind()

@type kind() :: :linear | :affine | :logarithmic | :cyclic

How a quantity's values combine (spec/05 §4.2).

measure()

@type measure() :: %{
  optional(:unit) => Visualize.Unit.code() | nil,
  optional(:quantity) => t() | nil
}

A unit and a quantity, either or both; the two facts a field description carries.

metric_kind()

@type metric_kind() :: :gauge | :counter | :rate

How a field's values were measured, which refines its quantity's additivity.

reason()

@type reason() ::
  {:unit, Visualize.Unit.code(), Visualize.Unit.reason()}
  | {:quantity, t(), Visualize.Unit.code()}
  | {:unknown_quantity, term()}

Why a measure does not resolve.

system()

@type system() :: :metric | :us | :uk | :as_posted

A unit system: a display preference, never applied to the data.

t()

@type t() ::
  :duration
  | :information
  | :data_rate
  | :frequency
  | :ratio
  | :count
  | :temperature
  | :length
  | :speed
  | :power
  | :energy
  | :pressure
  | :area
  | :volume
  | :mass
  | :pace
  | :angle
  | :currency
  | :electric_potential
  | :electric_current
  | :electric_charge
  | :illuminance
  | :sound_level
  | :mass_concentration
  | :amount_concentration

One of the fixed list of quantities.

Functions

additivity(quantity, metric_kind \\ nil)

@spec additivity(t() | nil, metric_kind() | nil) :: %{
  across: boolean(),
  over_time: boolean()
}

Whether a sum of the quantity's values means anything (spec/05 §4.2, D-155): %{across: boolean, over_time: boolean} — summing several series at one instant, and summing one series over time. The quantity's physical defaults are refined by the field's metric kind: a :gauge and a :rate never add over time, and a :counter's increments add over time when the quantity adds across. No quantity (a plain number) defaults to both.

Examples

iex> Visualize.Quantity.additivity(:count, :gauge)
%{across: true, over_time: false}

iex> Visualize.Quantity.additivity(:temperature, :counter)
%{across: false, over_time: false}

all()

@spec all() :: [t(), ...]

The fixed list of quantities (spec/05 §4.2).

base_unit(quantity)

@spec base_unit(t()) :: Visualize.Unit.code() | nil

A quantity's base unit, the unit its values are in when no unit is given; nil for a :currency, whose code the data must say.

bounds(quantity)

@spec bounds(t()) :: {number() | nil, number() | nil}

Where a quantity's values physically lie, in its base unit, {min | nil, max | nil} (spec/05 §4.2): a display hint an axis may anchor at, never validation — a value outside them is never refused.

Examples

iex> Visualize.Quantity.bounds(:duration)
{0, nil}

bounds(quantity, unit)

@spec bounds(t(), Visualize.Unit.code()) :: {number() | nil, number() | nil}

A quantity's bounds in a unit of it: the base's, converted. A unit without a factor (Cel) takes the base's bounds as they are.

Examples

iex> Visualize.Quantity.bounds(:ratio, "%")
{0.0, 100.0}

display_unit(domain, measure)

@spec display_unit([term()], measure()) :: Visualize.Unit.code() | nil

As display_unit/3 with [] options.

display_unit(domain, measure, opts)

@spec display_unit([term()], measure(), keyword()) :: Visualize.Unit.code() | nil

The member of the system's family that fits a domain in a measure (spec/05 §4.3): the largest in which the domain's magnitude is at least 1, else the smallest; a family of one member is that member. A measure with no family under the system, or a unit without a factor (Cel) in its own family, keeps its unit, and so does a zero magnitude when the unit is in the family — otherwise the member nearest it is taken. No unit is nil.

Options

  • :system - :metric, :us, :uk or :as_posted (default)

family(quantity, unit)

@spec family(t(), Visualize.Unit.code() | nil) :: [Visualize.Unit.code()]

The display family a unit of the quantity belongs to, smallest first; [] for a unit of no family's shape (spec/05 §4.2). A unit belongs to the first family holding a member of its shape — its atoms and exponents, prefixes aside and a binary prefix kept apart — so the unit picks among a quantity's families: bits, binary prefixes, hertz, watt-hours, US lengths. No unit means the base unit.

Examples

iex> Visualize.Quantity.family(:information, "MiBy")
["By", "KiBy", "MiBy", "GiBy", "TiBy"]

iex> Visualize.Quantity.family(:length, "[ft_i]")
["[in_i]", "[ft_i]", "[mi_i]"]

family(quantity, unit, system)

@spec family(t(), Visualize.Unit.code() | nil, system()) :: [Visualize.Unit.code()]

The display family a system shows a unit of the quantity in (spec/05 §4.2): the family the quantity's table names for :metric, :us or :uk when it names one, else the unit's own (family/2), which is what :as_posted always is.

Examples

iex> Visualize.Quantity.family(:mass, "kg", :uk)
["[oz_av]", "[lb_av]", "[stone_av]"]

iex> Visualize.Quantity.family(:duration, "ms", :us)
["ns", "us", "ms", "s", "min", "h", "d"]

format(value, measure)

@spec format(number(), measure()) :: String.t()

As format/3 with [] options.

format(value, measure, opts)

@spec format(number(), measure(), keyword()) :: String.t()

One value of a measure for a label or a tooltip, not an axis (spec/05 §4.3): a :duration in the compound form (1h 24m, 840ms), a :currency as money in its code ($1,240.00, ¥1,240), a :count rounded and grouped (1,240), and anything else to precision decimals, then the unit's suffix (91.40%, 350°, 20.50 kg). Raises ArgumentError for a measure resolve/1 refuses.

Options

  • :precision - decimals (default 2); a currency's are its minor units
  • :locale - default "en"
  • :system - the unit system (default :as_posted); under another, the value is written in its display_unit/3

Examples

iex> Visualize.Quantity.format(5040, %{unit: "s"})
"1h 24m"

iex> Visualize.Quantity.format(1240, %{unit: "{iso4217:USD}"})
"$1,240.00"

iex> Visualize.Quantity.format(350, %{unit: "deg"}, precision: 0)
"350°"

iex> Visualize.Quantity.format(80, %{unit: "kg"}, system: :us)
"176.37 lb"

formatter(scale, measure, opts \\ [])

@spec formatter(struct(), measure(), keyword()) :: (term() -> String.t()) | nil

The tick formatter of a realised scale in a measure (spec/05 §4.3): each tick scaled into the display unit, written by Visualize.Format.Auto's notation of the scaled domain, then followed by the unit's symbol. A log or symlog scale gives each tick its own display unit; any other scale but the numeric ones is Visualize.Format.Auto.formatter/2, the measure unused. A value ticks/3 placed for the same count and system is labelled with the round number it came from, so the round trip through the data's unit never shows. Raises ArgumentError for a measure resolve/1 refuses.

Options

  • :count - the tick count the axis asks for (default 10)
  • :locale - default "en"
  • :system - the unit system, as display_unit/3 (default :as_posted)

kind(quantity)

@spec kind(t()) :: kind()

How a quantity's values combine (spec/05 §4.2): :linear, :affine (a temperature), :logarithmic (a sound level) or :cyclic (an angle).

Examples

iex> Visualize.Quantity.kind(:angle)
:cyclic

label(measure)

@spec label(measure()) :: String.t() | nil

The label rule (spec/05 §4.3): the unit, else the quantity, else nothing — the unit's symbol with its annotations written when it is not empty, else the quantity's name with spaces for underscores, else nil. Raises ArgumentError for a measure resolve/1 refuses.

Examples

iex> Visualize.Quantity.label(%{unit: "ms"})
"ms"

iex> Visualize.Quantity.label(%{quantity: :count})
"count"

iex> Visualize.Quantity.label(%{})
nil

of_unit(unit)

@spec of_unit(Visualize.Unit.code()) :: t() | nil

The quantity a code's dimension implies, or nil when none of the list has it. A dimensionless code holding % is a :ratio, any other a :count, and a code of one currency alone ({iso4217:USD}) a :currency.

Examples

iex> Visualize.Quantity.of_unit("kW.h")
:energy

iex> Visualize.Quantity.of_unit("{request}")
:count

resolve(measure)

@spec resolve(measure()) ::
  {:ok, %{unit: Visualize.Unit.code() | nil, quantity: t() | nil}}
  | {:error, reason()}

A measure completed and checked (spec/05 §4.2): a unit alone implies its quantity, a quantity alone means its base unit, and a unit whose dimension is not its quantity's is refused. A :ratio or a :count takes any dimensionless unit, and a :currency a code of one currency alone.

Examples

iex> Visualize.Quantity.resolve(%{quantity: :duration})
{:ok, %{unit: "s", quantity: :duration}}

iex> Visualize.Quantity.resolve(%{unit: "By", quantity: :duration})
{:error, {:quantity, :duration, "By"}}

suffix(unit)

@spec suffix(Visualize.Unit.code() | nil) :: String.t()

What follows a number written in a unit (spec/05 §4.3): its symbol after a space, % and a symbol beginning with / directly, and nothing for an empty symbol or no unit.

Examples

iex> Visualize.Quantity.suffix("Cel")
" °C"

iex> Visualize.Quantity.suffix("{request}/s")
"/s"

systems()

@spec systems() :: [system(), ...]

The unit systems a display may prefer (spec/05 §4.2), :as_posted the default.

ticks(scale, measure)

@spec ticks(struct(), measure()) :: [float()] | nil

As ticks/3 with [] options.

ticks(scale, measure, opts)

@spec ticks(struct(), measure(), keyword()) :: [float()] | nil

The tick values of a realised scale in a measure when its display unit is not its unit (spec/05 §4.3, D-156): round numbers in the display unit — Visualize.Scale.ticks/2 of the domain converted into it — each converted back into the data's unit to be placed. nil when the display unit is the unit, for a measure with no unit and for a :currency, and on any scale but Linear, Power and Radial: the scale's own ticks are the axis's. formatter/3 of the same count and system labels each value with its round number. Raises ArgumentError for a measure resolve/1 refuses.

Options

  • :count - the tick count the axis asks for (default 10)
  • :system - the unit system, as display_unit/3 (default :as_posted)

Examples

iex> scale = Visualize.Scale.linear() |> Visualize.Scale.domain([0, 100])
iex> ticks = Visualize.Quantity.ticks(scale, %{unit: "s"}, count: 5)
[0.0, 30.0, 60.0, 90.0]
iex> Enum.map(ticks, Visualize.Quantity.formatter(scale, %{unit: "s"}, count: 5))
["0 min", "0.5 min", "1 min", "1.5 min"]

iex> scale = Visualize.Scale.linear() |> Visualize.Scale.domain([0, 900])
iex> Visualize.Quantity.ticks(scale, %{unit: "ms"})
nil