Status: Implemented
Scales map values from a data domain to a visual range. Visualize.Scale is a facade that constructs every scale kind and dispatches the common operations (domain/2, range/2, apply/2, invert/2, ticks/1,2, nice/1, padding/2, bandwidth/1, clamp/2) to the module named by the scale struct. This document specifies the facade protocol, then each scale module: Linear, Log, Power, Symlog, Time, Ordinal, Band, Quantile, Quantize, Threshold, Color, and Radial. Every scale is an immutable struct; setters return a new struct and never mutate.
1. Common Model
1.1 Scale Families
| Family | Modules | Domain | Range | Output |
|---|---|---|---|---|
| Continuous | Linear, Log, Power, Symlog, Time, Radial | two numbers (or two time values) | two numbers (Radial: radians) | float, interpolated |
| Discrete | Ordinal, Band | list of categories | list of values / two numbers | range element / band start |
| Discretising | Quantile, Quantize, Threshold | samples / two numbers / thresholds | list of values | range element |
| Colour | Color | two or three numbers | scheme, colour list, or function | "#rrggbb" string |
1.2 Domain and Range Representation
Continuous domains and ranges are two-element lists [d0, d1] / [r0, r1] in every scale struct: Linear, Log, Power, Symlog, Time, Radial, Band, Quantize, and Color. Power, Symlog, and Quantize additionally accept a two-tuple on input to domain/2 and range/2 and store it as a list (D-15). Visualize.Axis (spec/05) reads scale.range as a two-element list, so every continuous scale draws with the range it was given.
Domains MAY be descending (d0 > d1) and ranges MAY be descending (r0 > r1); the interpolation formulae below are sign-agnostic. A collapsed domain (d0 == d1) never raises: every continuous scale maps every value to the midpoint of the range, and a collapsed range inverts to the midpoint of the domain, through Visualize.Scale.Behaviour.normalize/3 (d3-scale's normalize, D-49).
1.3 The Facade Protocol
Visualize.Scale.<op>(scale, …) calls scale.__struct__.<op>(scale, …). The protocol is the behaviour Visualize.Scale.Behaviour, whose nine callbacks are domain/2, range/2, apply/2, invert/2, ticks/2, nice/1, padding/2, bandwidth/1, clamp/2. Every scale module MUST declare @behaviour Visualize.Scale.Behaviour and implement all nine (the module also provides the shared fraction, table below), so a missing operation is a compile error under --warnings-as-errors rather than an UndefinedFunctionError at dispatch (D-15). Operations that do not apply to a scale kind MUST behave as identity (nice/1, padding/2, clamp/2), return nil (invert/2), or return 0 (bandwidth/1); the discretising scales return their thresholds from ticks/2.
The table records what each module defines.
| Function | Contract |
|---|---|
Visualize.Scale.Behaviour.normalize/3 | The position of a value between two bounds as a fraction: (v − a) / (b − a), or 0.5 when a == b (D-49). Every continuous scale's apply/2 and invert/2 use it. |
| Module | apply/2 | invert/2 | ticks/2 | nice/1 | padding/2 | bandwidth/1 | clamp/2 | Facade constructor |
|---|---|---|---|---|---|---|---|---|
Linear | yes | yes | yes | yes | identity | 0 | yes | linear/0 |
Log | yes | yes | yes | yes | identity | 0 | yes | log/0,1 |
Power | yes | yes | yes | Linear.nice/1 | identity | 0 | yes | power/0, sqrt/0 |
Symlog | yes | yes | yes | Linear.nice/1 | identity | 0 | yes | symlog/0 |
Time | yes | yes | yes | yes | identity | 0 | yes | time/0 |
Ordinal | yes | nil | domain | identity | identity | 0 | identity | ordinal/0 |
Band | yes | nil | domain | identity | yes | yes | identity | band/0 |
Quantile | yes | nil | quantiles | identity | identity | 0 | identity | quantile/0 |
Quantize | yes | nil | thresholds | identity | identity | 0 | identity | quantize/0 |
Threshold | yes | nil | thresholds | identity | identity | 0 | identity | threshold/0 |
Color | yes | nil | domain extent | identity | identity | 0 | yes | sequential/1, diverging/1 |
Radial | yes | yes | equal divisions | identity | identity | 0 | yes | radial/0 |
Power, Symlog, Quantile, Quantize, and Threshold also keep scale/2 as an alias of apply/2 for one release, after which it is removed; new code MUST call apply/2. test/visualize/scale/protocol_test.exs constructs every kind through the facade and calls all nine operations; it is the recurrence guard for this section.
1.4 Clamping
clamp/2 takes a boolean. The default is false for every scale except Color, whose default is true. When enabled, Linear, Log, Time, Radial, and Color clamp the normalised parameter t to [0, 1] before interpolating; Power and Symlog clamp the output to [min(r0, r1), max(r0, r1)]. The two formulations are equivalent for linear interpolation. invert/2 never clamps.
2. Visualize.Scale Facade
| Function | Contract |
|---|---|
Visualize.Scale.linear/0 | Returns Visualize.Scale.Linear.new/0: domain [0, 1], range [0, 1]. |
Visualize.Scale.log/0 | As log/1 with base 10. |
Visualize.Scale.log/1 | Returns Visualize.Scale.Log.new/1 with the given base. |
Visualize.Scale.power/0 | Returns Visualize.Scale.Power.new/0 (exponent 1). |
Visualize.Scale.sqrt/0 | Returns Visualize.Scale.Power.sqrt/0 (exponent 0.5). |
Visualize.Scale.symlog/0 | Returns Visualize.Scale.Symlog.new/0. |
Visualize.Scale.time/0 | Returns Visualize.Scale.Time.new/0. |
Visualize.Scale.ordinal/0 | Returns Visualize.Scale.Ordinal.new/0. |
Visualize.Scale.band/0 | Returns Visualize.Scale.Band.new/0. |
Visualize.Scale.quantize/0 | Returns Visualize.Scale.Quantize.new/0. |
Visualize.Scale.quantile/0 | Returns Visualize.Scale.Quantile.new/0. |
Visualize.Scale.threshold/0 | Returns Visualize.Scale.Threshold.new/0. |
Visualize.Scale.sequential/1 | Returns Visualize.Scale.Color.sequential/1. |
Visualize.Scale.diverging/1 | Returns Visualize.Scale.Color.diverging/1. |
Visualize.Scale.radial/0 | Returns Visualize.Scale.Radial.new/0: domain [0, 1], range [0, 2π]. |
Visualize.Scale.domain/2 | Dispatches to the scale module's domain/2. |
Visualize.Scale.range/2 | Dispatches to the scale module's range/2. |
Visualize.Scale.apply/2 | Dispatches to the scale module's apply/2; maps a domain value to a range value. Total over every scale this module constructs (1.3). |
Visualize.Scale.invert/2 | Dispatches to the scale module's invert/2; maps a range value back to the domain. Continuous scales only; every other scale returns nil. |
Visualize.Scale.ticks/1 | As ticks/2 with count 10. |
Visualize.Scale.ticks/2 | Dispatches to the scale module's ticks/2; returns representative domain values, ascending for continuous scales, the domain itself for discrete scales, the thresholds for discretising scales. count is a hint, not a guarantee. |
Visualize.Scale.nice/1 | Dispatches to the scale module's nice/1; extends the domain outward to round values. |
Visualize.Scale.padding/2 | Dispatches to the scale module's padding/2; meaningful only for Band. |
Visualize.Scale.bandwidth/1 | Dispatches to the scale module's bandwidth/1; 0 for every scale but Band. |
Visualize.Scale.clamp/2 | Dispatches to the scale module's clamp/2 with a boolean. |
Visualize.Scale.zone/2 | Delegates to Visualize.Scale.Time.zone/2 (7.4). Not part of the protocol: a zone means something only to a time scale, so any other scale is a FunctionClauseError, not an identity. |
3. Visualize.Scale.Linear
3.1 Struct
%Visualize.Scale.Linear{domain: [0, 1], range: [0, 1], clamp?: false}.
3.2 Semantics
apply/2:t = (v − d0) / (d1 − d0)(0.5whend0 == d1); ifclamp?,tis clamped to[0, 1]; result isr0 + t · (r1 − r0), always a float.invert/2:t = (v − r0) / (r1 − r0); resultd0 + t · (d1 − d0). Never clamps.ticks/2(countn): the nice step is computed fromspan = |d1 − d0|,raw = span / max(n, 1),mag = 10^⌊log10 raw⌋,q = raw / mag, and factor1ifq < 1.5,2ifq < 3,5ifq < 7, else10;step = factor · mag. Ticks run from⌈d0/step⌉·stepto⌊d1/step⌋·stepinclusive, each rounded tomax(0, −⌊log10 step⌋)decimal places; when no multiple of the step lies inside the domain the result is[], never a value outside it. The tick count is approximatelyn; it is never guaranteed.nice/1: with the step for count 10, domain becomes[⌊d0/step⌋·step, ⌈d1/step⌉·step](floats).padding/2is identity;bandwidth/1is0.
A descending domain (d0 > d1) yields the same ticks in descending order. A degenerate domain (d0 == d1) yields [d0] from ticks/2 and leaves nice/1 unchanged (D-17).
3.3 Functions
| Function | Contract |
|---|---|
Visualize.Scale.Linear.new/0 | Returns the default struct. |
Visualize.Scale.Linear.domain/2 | Sets the domain; the argument MUST be a two-element list of numbers. |
Visualize.Scale.Linear.range/2 | Sets the range; the argument MUST be a two-element list of numbers. |
Visualize.Scale.Linear.clamp/2 | Sets clamp?. |
Visualize.Scale.Linear.apply/2 | Linear interpolation as in 3.2; returns a float. |
Visualize.Scale.Linear.invert/2 | Inverse interpolation as in 3.2; returns a float. |
Visualize.Scale.Linear.ticks/2 | Nice-step ticks as in 3.2. |
Visualize.Scale.Linear.nice/1 | Extends the domain to multiples of the count-10 nice step. |
Visualize.Scale.Linear.padding/2 | Identity. |
Visualize.Scale.Linear.bandwidth/1 | Returns 0. |
4. Visualize.Scale.Log
4.1 Struct
%Visualize.Scale.Log{domain: [1, 10], range: [0, 1], base: 10, clamp?: false}.
4.2 Semantics
All logarithms are log(x) / log(base).
domain/2requires both bounds> 0; any other input fails to match (FunctionClauseError).apply/2requiresvalue > 0(FunctionClauseErrorotherwise).t = (log v − log d0) / (log d1 − log d0), clamped to[0, 1]whenclamp?; resultr0 + t · (r1 − r0).invert/2:t = (v − r0) / (r1 − r0); resultbase ^ (log d0 + t · (log d1 − log d0)).ticks/2(countn): d3-scale's log ticks (D-17). Withi = log d0,j = log d1(swapped and the result reversed when descending): when the base is an integer andj − i < n, everyk · base^efor1 ≤ k < baseandefrom⌊i⌋to⌈j⌉that lies inside the domain —1, 2, … 9, 10, 20, …— and, when that yields fewer thann / 2values,Visualize.Data.ticks/3over the domain instead; otherwiseVisualize.Data.ticks(i, j, min(⌈j − i⌉, n))mapped throughbase^e, so many decades are thinned to aboutnpowers. Negative exponents divide (3 / 10, not3 · 0.1), and bases 10 and 2 use exact logarithms, so the values print as the decimals they are. A sub-decade domain yields linear ticks rather than[]; as withLinear, a count of 1 MAY still yield[]when no multiple of the step lies inside the domain.nice/1: domain becomes[base^⌊log d0⌋, base^⌈log d1⌉].padding/2identity;bandwidth/10.
4.3 Functions
| Function | Contract |
|---|---|
Visualize.Scale.Log.new/0 | As new/1 with base 10. |
Visualize.Scale.Log.new/1 | Returns a log scale with the given base (any number > 0, ≠ 1). |
Visualize.Scale.Log.domain/2 | Sets the domain; both values MUST be positive. |
Visualize.Scale.Log.range/2 | Sets the range (two-element list). |
Visualize.Scale.Log.clamp/2 | Sets clamp?. |
Visualize.Scale.Log.apply/2 | Log interpolation as in 4.2; value MUST be positive. |
Visualize.Scale.Log.invert/2 | Inverse as in 4.2. |
Visualize.Scale.Log.ticks/2 | d3's log ticks as in 4.2: about count values, multiples within a decade, thinned powers across many. |
Visualize.Scale.Log.nice/1 | Extends the domain to integer powers of the base. |
Visualize.Scale.Log.padding/2 | Identity. |
Visualize.Scale.Log.bandwidth/1 | Returns 0. |
5. Visualize.Scale.Power
5.1 Struct
%Visualize.Scale.Power{domain: [0, 1], range: [0, 1], exponent: 1, clamp?: false}. domain/2 and range/2 accept a two-element list or a two-tuple and store a list.
5.2 Semantics
Let e be the exponent and T(x) = sign(x) · |x|^e (D3's signed power), with inverse T⁻¹(y) = sign(y) · |y|^(1/e).
apply/2:D = T(d1) − T(d0);t = 0.5ifD == 0, else(T(v) − T(d0)) / D; resultr0 + t · (r1 − r0); whenclamp?, the result is clamped to the range bounds in either order. A domain that includes negative values maps symmetrically: on[−10, 10]withe = 2,−5and5sit0.125either side of the midpoint.invert/2:t = (v − r0) / (r1 − r0)(0.5ifr1 == r0); resultT⁻¹(T(d0) + t · D), negative when the domain value is.ticks/1,2: delegate toLinear.ticks/2on[d0, d1](linear spacing in the domain, not in the transformed space).nice/1: the domain becomesLinear.nice/1's domain for[d0, d1].padding/2identity;bandwidth/10.sqrt/0isnew/0with exponent 0.5.
5.3 Functions
| Function | Contract |
|---|---|
Visualize.Scale.Power.new/0 | Returns the default struct (exponent 1). |
Visualize.Scale.Power.sqrt/0 | Returns a power scale with exponent 0.5. |
Visualize.Scale.Power.domain/2 | Sets the domain from a two-element list or tuple; stored as a list. |
Visualize.Scale.Power.range/2 | Sets the range from a two-element list or tuple; stored as a list. |
Visualize.Scale.Power.exponent/2 | Sets the exponent; MUST be a number. |
Visualize.Scale.Power.clamp/2 | Sets clamp?. |
Visualize.Scale.Power.apply/2 | Maps a domain value to the range as in 5.2. |
Visualize.Scale.Power.scale/2 | Alias of apply/2, retained for one release (1.3). |
Visualize.Scale.Power.invert/2 | Inverse as in 5.2; signed. |
Visualize.Scale.Power.ticks/1 | As ticks/2 with count 10. |
Visualize.Scale.Power.ticks/2 | Linear.ticks/2 over the domain. |
Visualize.Scale.Power.nice/1 | Linear.nice/1 over the domain. |
Visualize.Scale.Power.padding/2 | Identity. |
Visualize.Scale.Power.bandwidth/1 | Returns 0. |
6. Visualize.Scale.Symlog
6.1 Struct
%Visualize.Scale.Symlog{domain: [−1, 1], range: [0, 1], constant: 1, clamp?: false}. List or tuple accepted for domain/2 and range/2; stored as lists.
6.2 Semantics
The transform is S(x) = sign(x) · ln(1 + |x| / c) with inverse S⁻¹(y) = sign(y) · c · (e^{|y|} − 1), where c is the constant (> 0, enforced by constant/2).
apply/2:t = (S(v) − S(d0)) / (S(d1) − S(d0))(0.5if the denominator is zero); resultr0 + t · (r1 − r0), clamped to the range bounds whenclamp?.invert/2:t = (v − r0) / (r1 − r0)(0.5if degenerate); resultS⁻¹(S(d0) + t · (S(d1) − S(d0))).nice/1: the domain becomesLinear.nice/1's domain for[d0, d1].padding/2identity;bandwidth/10.ticks/1,2(countn): withlo = min(d0, d1),hi = max(d0, d1), the result is the ascending, de-duplicated union of: negated decades−c · 10^kforkin0, s, 2s, … ≤ ⌈log10(−lo / c)⌉(only whenlo < 0);0whenlo ≤ 0 ≤ hi; and decadesc · 10^kforkup to⌈log10(hi / c)⌉(only whenhi > 0) — every candidate filtered to[lo, hi]. The exponent stride iss = max(1, ⌊⌈log10(bound / c)⌉ / n⌋). The decades start at the constantc, where the log region begins (D-17); withc = 1they are the powers of ten. A domain that contains no decade yields[].
6.3 Functions
| Function | Contract |
|---|---|
Visualize.Scale.Symlog.new/0 | Returns the default struct. |
Visualize.Scale.Symlog.domain/2 | Sets the domain from a two-element list or tuple; MAY include zero and negatives; stored as a list. |
Visualize.Scale.Symlog.range/2 | Sets the range from a two-element list or tuple; stored as a list. |
Visualize.Scale.Symlog.constant/2 | Sets the linear/log transition constant; MUST be a number > 0. |
Visualize.Scale.Symlog.clamp/2 | Sets clamp?. |
Visualize.Scale.Symlog.apply/2 | Maps a domain value to the range as in 6.2. |
Visualize.Scale.Symlog.scale/2 | Alias of apply/2, retained for one release (1.3). |
Visualize.Scale.Symlog.invert/2 | Inverse as in 6.2. |
Visualize.Scale.Symlog.ticks/1 | As ticks/2 with count 10. |
Visualize.Scale.Symlog.ticks/2 | Signed decades of the constant, and zero, as in 6.2. |
Visualize.Scale.Symlog.nice/1 | Linear.nice/1 over the domain. |
Visualize.Scale.Symlog.padding/2 | Identity. |
Visualize.Scale.Symlog.bandwidth/1 | Returns 0. |
7. Visualize.Scale.Time
7.1 Struct
%Visualize.Scale.Time{domain: nil, range: [0, 1], clamp?: false, zone: nil}. The domain MUST be set before use. zone is the display zone of 7.4, an IANA name such as "America/New_York", or nil for UTC.
7.2 Accepted Value Types
Domain bounds and apply/2 arguments MAY be any of DateTime, Date, NaiveDateTime, mixed freely. Each is reduced to Unix seconds: DateTime via DateTime.to_unix/1; NaiveDateTime as if UTC; Date as midnight UTC (days since 1970-01-01 × 86400). Sub-second precision is discarded.
invert/2, ticks/2, and nice/1 produce DateTime values in the scale's zone — UTC when it has none — whatever the domain's type. invert/2 rounds to the nearest whole second. The reduction above does not depend on the zone: a NaiveDateTime is still read as UTC and a Date as midnight UTC, so a zone changes where ticks fall and how values print, never where a value is drawn.
7.3 Semantics
apply/2: linear interpolation over Unix seconds, exactly asLinear.apply/2; clampstwhenclamp?.ticks/2(countn): the interval is chosen as in 7.3.1 fromspan_seconds / max(n, 1); the ticks are every boundary of that interval (7.3.2) fromceil(d0)up to and includingd1. Every tick lies on a boundary inside[d0, d1]; the first never precedesd0. A descending domain yields the same ticks descending. A degenerate domain yields[d0]whend0is a whole second.nice/1: chooses the interval for count 10 and sets the domain to[floor(d0), ceil(d1)](or[ceil(d0), floor(d1)]when descending). A domain already on boundaries is unchanged.interval/2(span_seconds, count) exposes the choice for callers that format ticks by their interval; it is not part of the scale protocol.
This is d3-time's model (D-16); the interval selection and the snapping are the reference behaviour of d3.timeTicks and interval.range, with Monday-based weeks.
7.3.1 Interval Table
An interval is {unit, n}: n units per step. The candidates, with the nominal seconds one step spans (30-day months, 365-day years):
| Interval | Seconds | Boundary |
|---|---|---|
{:second, 1}, {:second, 5}, {:second, 15}, {:second, 30} | 1, 5, 15, 30 | Unix seconds that are a multiple of n |
{:minute, 1}, {:minute, 5}, {:minute, 15}, {:minute, 30} | 60, 300, 900, 1800 | Unix seconds that are a multiple of 60·n |
{:hour, 1}, {:hour, 3}, {:hour, 6}, {:hour, 12} | 3600, 10800, 21600, 43200 | Unix seconds that are a multiple of 3600·n (aligned to midnight UTC) |
{:day, 1}, {:day, 2} | 86400, 172800 | midnight UTC of a day-of-month d with (d − 1) mod n = 0 (two-day ticks restart on the 1st of each month) |
{:week, 1} | 604800 | Monday 00:00 UTC |
{:month, 1}, {:month, 3} | 2592000, 7776000 | 00:00 UTC on the 1st of a month m with (m − 1) mod n = 0 |
{:year, 1} | 31536000 | 00:00 UTC on 1 January |
{:year, k}, k > 1 | k · 31536000 | 00:00 UTC on 1 January of a year y with y mod k = 0 |
Selection, with target = span_seconds / max(n, 1): of the two table rows whose seconds bracket target, the one closer in ratio (target / below < above / target picks the row below); a target below one second picks {:second, 1}. When target exceeds one year the interval is {:year, k} with k = max(1, round(f · 10^⌊log10 raw⌋)) for raw = span_years / n and f from the d3 nice-step thresholds (10 if raw / 10^⌊log10 raw⌋ ≥ √50, 5 if ≥ √10, 2 if ≥ √2, else 1), so 24 years at count 10 step by 2 and three centuries by 20.
Because neighbouring rows differ by at most 4.3×, the number of ticks is within about 2.2× of n either way.
7.3.2 Snapping
floor(t) is the latest boundary at or before t; ceil(t) is floor(t) when t is already a boundary, else the boundary that follows. Stepping from a boundary to the next: sub-day intervals add n units of seconds; {:day, n} adds n days, or moves to the 1st of the next month when that would pass the month's end; {:week, 1} adds seven days; {:month, n} adds n calendar months; {:year, k} adds k years. Month arithmetic clamps the day to the target month's length (31 January + 1 month is the last day of February), so no step constructs an invalid date; ticks themselves always land on the 1st.
The table and the steps above describe a scale without a zone. A zoned scale reads every boundary in its zone's local calendar instead (7.4).
7.4 Display Zone
zone(scale, name) sets the zone the scale's ticks are counted in and its values are shown in (#447). The name is an IANA time zone, looked up in the host's configured Calendar.TimeZoneDatabase through DateTime.shift_zone/3 and DateTime.new/4; the library takes no time zone dependency (D-115). zone(scale, nil) removes it.
Storage does not change. Values are reduced to Unix seconds exactly as in 7.2, so apply/2 is the same function with or without a zone and a position stays linear in real time: a 25-hour day is drawn 25/24 as wide as a 24-hour one. The interval of 7.3.1 is chosen from the span in real seconds, as before.
Boundaries are local. With a zone, the boundary column of 7.3.1 is read in the zone's local calendar — the wall-clock time DateTime.shift_zone/3 gives each instant:
{:second, n},{:minute, n},{:hour, n}: every real instant whose local time has the second, the minute, or the minute and second at zero and the field (second, minute or hour of the local day) a multiple ofn. Across a transition back, the repeated local hour yields its boundaries twice, at distinct instants, in instant order (01:00 EDT, then01:00 EST); across a transition forward, the boundaries of the skipped local hour do not exist and are absent. In a zone whose offset is not a whole hour (Asia/Kolkata,+05:30;Asia/Kathmandu,+05:45) the hour boundaries are the local hours, not the UTC ones.{:day, n},{:week, 1},{:month, n},{:year, k}: the local midnight of every boundary date, the dates chosen by the date arithmetic of 7.3.2 applied to local dates (a local Monday, the 1st of a local month, 1 January of a local year). A day containing a transition is 23 or 25 hours long and its ticks are spaced accordingly. A local midnight that a transition skips is the first instant of that day; one a transition repeats is the earlier instant.
floor, ceil and the step of 7.3.2 follow the same reading, so nice/1 extends the domain to local boundaries. ticks/2 returns DateTime values in the zone, nice/1 sets the domain to them, and invert/2 returns a DateTime in the zone. Without a zone each of these is byte for byte what 7.2 and 7.3 say: UTC boundaries and UTC DateTime values.
The zone is refused loudly. zone/2 checks the name against the host's database at the call and raises ArgumentError naming the zone and the reason — :time_zone_not_found for a name the database does not know, :utc_only_time_zone_database for a host with no database configured, where every zone but Etc/UTC is unknowable. There is no fallback to UTC: a host that asked for New York and silently got UTC would draw every day tick four or five hours from midnight. check_zone/1 returns the same answer as a value, which is what the declarative layer's validator reports (14-declarative-chart §4.3, §10.1).
local(scale, value) is a value as the scale shows it: a time value reduced as in 7.2 and shifted to the zone, or the value itself when the scale has none. An axis that formats explicit tick values passes them through it, so its labels read local time like the ticks it computes (05-axes-and-formatting §2.8).
7.5 Functions
| Function | Contract |
|---|---|
Visualize.Scale.Time.new/0 | Returns the default struct with domain: nil and no zone. |
Visualize.Scale.Time.domain/2 | Sets the domain; a two-element list of DateTime, Date, or NaiveDateTime. |
Visualize.Scale.Time.range/2 | Sets the range (two-element list of numbers). |
Visualize.Scale.Time.clamp/2 | Sets clamp?. |
Visualize.Scale.Time.apply/2 | Maps a time value to the range as in 7.3; returns a float. |
Visualize.Scale.Time.invert/2 | Maps a range value to a DateTime in the scale's zone (UTC without one) at whole-second precision. |
Visualize.Scale.Time.ticks/2 | Every boundary of the chosen interval inside the domain (7.3), in the zone's local calendar when it has one (7.4); a list of DateTime in the scale's zone, UTC without one. |
Visualize.Scale.Time.nice/1 | Extends the domain outward to the boundaries of the count-10 interval (local boundaries with a zone, 7.4); the domain becomes DateTime values in the scale's zone, UTC without one. |
Visualize.Scale.Time.interval/2 | The {unit, n} interval count ticks would use over a span of seconds (7.3.1). |
Visualize.Scale.Time.padding/2 | Identity. |
Visualize.Scale.Time.bandwidth/1 | Returns 0. |
Visualize.Scale.Time.zone/2 | Sets the display zone (7.4), an IANA name or nil; raises ArgumentError naming the zone when the host's time zone database does not know it or the host has none. |
Visualize.Scale.Time.check_zone/1 | :ok, or {:error, :time_zone_not_found} / {:error, :utc_only_time_zone_database}: whether the host's time zone database can show a name (7.4). |
Visualize.Scale.Time.local/2 | A time value as the scale shows it: a DateTime in the scale's zone, or the value unchanged when the scale has none (7.4). |
8. Visualize.Scale.Ordinal
8.1 Struct
%Visualize.Scale.Ordinal{domain: [], range: [], unknown: nil}.
8.2 Semantics
apply/2:unknownwhen the range is empty. Otherwise the indexiof the first domain element equal (==) to the value; when not found theunknownvalue is returned, elseEnum.at(range, rem(i, length(range))): the range cycles when the domain is longer. The domain is never extended implicitly.invert/2returnsnil;ticks/2returns the domain (count ignored);nice/1,clamp/2,padding/2identity;bandwidth/10.unknownis d3'sscaleOrdinal.unknownwithout d3's implicit default: the struct'snilmeans "no value", and the caller decides what that draws. A scale used for colour gives it a colour, so that a value outside the domain is still drawn, and drawn as not one of the categories: the declarative layer'scolorscale does so by default, with the theme's:axisslot, a neutral every theme makes visible on its background (14-declarative-chart §4.3, D-122). Anilvalue is outside every domain inference builds, since inference dropsnils, so at this level it maps tounknownlike any other stranger; the declarative layer reads anilas no value at all and paints nothing (spec/14 §3.4).
8.3 Functions
| Function | Contract |
|---|---|
Visualize.Scale.Ordinal.new/0 | Returns the default struct. |
Visualize.Scale.Ordinal.domain/2 | Sets the domain; MUST be a list. |
Visualize.Scale.Ordinal.range/2 | Sets the range; MUST be a list. |
Visualize.Scale.Ordinal.unknown/2 | Sets the value returned for inputs not in the domain (default nil). |
Visualize.Scale.Ordinal.apply/2 | Category lookup with cycling range as in 8.2. |
Visualize.Scale.Ordinal.invert/2 | Returns nil. |
Visualize.Scale.Ordinal.ticks/2 | Returns the domain. |
Visualize.Scale.Ordinal.nice/1 | Identity. |
Visualize.Scale.Ordinal.clamp/2 | Identity. |
Visualize.Scale.Ordinal.padding/2 | Identity. |
Visualize.Scale.Ordinal.bandwidth/1 | Returns 0. |
9. Visualize.Scale.Band
9.1 Struct
%Visualize.Scale.Band{domain: [], range: [0, 1], padding_inner: 0, padding_outer: 0, align: 0.5, round?: false}.
9.2 Setters and Guards
| Setter | Constraint |
|---|---|
padding/2 | 0 ≤ p < 1; sets both padding_inner and padding_outer. |
padding_inner/2 | 0 ≤ p < 1. |
padding_outer/2 | p ≥ 0 (no upper bound). |
align/2 | 0 ≤ a ≤ 1. |
round/2 | boolean. |
Violations fail to match (FunctionClauseError).
9.3 Band Maths
This is d3-scale's rescale (D-17). With n = length(domain), pi = padding_inner, po = padding_outer, reverse = r1 < r0, lo = min(r0, r1), hi = max(r0, r1):
n == 0:bandwidth = 0,step = 0, every positionnil.- Otherwise
step = (hi − lo) / max(1, n − pi + 2·po), floored whenround?;start = lo + (hi − lo − step · (n − pi)) · align;bandwidth = step · (1 − pi); whenround?,startandbandwidthare rounded (from the floored step) and every position is therefore an integer-valued float. - Position of the category at index
iisstart + i · step, orstart + (n − 1 − i) · stepwhenreverse: a descending range lays the bands out in reverse with positivestepandbandwidth. step/1returnsstep(floored whenround?);bandwidth/1returnsbandwidth.
apply/2 returns nil for a value not in the domain. ticks/2 returns the domain; invert/2 returns nil; nice/1, clamp/2 identity. Under round? the rounded start plus the rounded bandwidth MAY overshoot the range by under one unit, as in D3.
9.4 Functions
| Function | Contract |
|---|---|
Visualize.Scale.Band.new/0 | Returns the default struct. |
Visualize.Scale.Band.domain/2 | Sets the list of categories. |
Visualize.Scale.Band.range/2 | Sets the range (two-element list). |
Visualize.Scale.Band.padding/2 | Sets inner and outer padding together (0 ≤ p < 1). |
Visualize.Scale.Band.padding_inner/2 | Sets inner padding (0 ≤ p < 1). |
Visualize.Scale.Band.padding_outer/2 | Sets outer padding (p ≥ 0). |
Visualize.Scale.Band.align/2 | Sets the distribution of outer space (0 = all at end, 0.5 = centred, 1 = all at start). |
Visualize.Scale.Band.round/2 | Enables rounding: the step is floored, the start and bandwidth rounded. |
Visualize.Scale.Band.apply/2 | Returns the band start for a category, or nil if absent. |
Visualize.Scale.Band.invert/2 | Returns nil. |
Visualize.Scale.Band.bandwidth/1 | Returns the band width as in 9.3. |
Visualize.Scale.Band.step/1 | Returns the distance between adjacent band starts (floored when round?). |
Visualize.Scale.Band.ticks/2 | Returns the domain. |
Visualize.Scale.Band.nice/1 | Identity. |
Visualize.Scale.Band.clamp/2 | Identity. |
10. Visualize.Scale.Quantile
10.1 Struct
%Visualize.Scale.Quantile{domain: [], range: [], thresholds: []}. thresholds is derived and MUST NOT be set by callers.
10.2 Semantics
domain/2sorts the sample list ascending and recomputes thresholds.range/2stores the list and recomputes thresholds.- Thresholds exist only when both domain and range are non-empty and
n = length(range) > 1: foriin1 … n−1, thep = i/nquantile of the sorted samples by linear interpolation at fractional indexp · (len − 1). apply/2:nilif the range is empty; the first range element if there are no thresholds; otherwise the range element at the count of thresholds≤ value(buckets are half-open,[t_{k−1}, t_k)), so a value at or above the last threshold reaches the last range element.invert_extent/2:nilif domain or range is empty or the value is not in the range; otherwise{lo, hi}withlothe first sample (bucket 0) ort_{k−1}, andhithe last sample (last bucket) ort_k.quantiles/1returns the thresholds;ticks/2returns the same list (count ignored).invert/2nil;nice/1,padding/2,clamp/2identity;bandwidth/10.
10.3 Functions
| Function | Contract |
|---|---|
Visualize.Scale.Quantile.new/0 | Returns the default struct. |
Visualize.Scale.Quantile.domain/2 | Sets the sample list (sorted on entry) and recomputes thresholds. |
Visualize.Scale.Quantile.range/2 | Sets the discrete output list and recomputes thresholds. |
Visualize.Scale.Quantile.apply/2 | Maps a value to its bucket's range element as in 10.2. |
Visualize.Scale.Quantile.scale/2 | Alias of apply/2, retained for one release (1.3). |
Visualize.Scale.Quantile.invert_extent/2 | Returns {lo, hi} of sample values mapping to a range element, or nil. |
Visualize.Scale.Quantile.quantiles/1 | Returns the n − 1 interior quantile thresholds. |
Visualize.Scale.Quantile.invert/2 | Returns nil. |
Visualize.Scale.Quantile.ticks/2 | Returns quantiles/1. |
Visualize.Scale.Quantile.nice/1 | Identity. |
Visualize.Scale.Quantile.padding/2 | Identity. |
Visualize.Scale.Quantile.bandwidth/1 | Returns 0. |
Visualize.Scale.Quantile.clamp/2 | Identity. |
11. Visualize.Scale.Quantize
11.1 Struct
%Visualize.Scale.Quantize{domain: [0, 1], range: []}. domain/2 accepts a list or tuple; stored as a list.
11.2 Semantics
With n = length(range) and step = (d1 − d0) / n:
apply/2:nilfor an empty range;t = (v − d0) / (d1 − d0)(0if degenerate), clamped to[0, 1]; bucketmin(⌊t · n⌋, n − 1). Values outside the domain map to the first or last element (always clamped).ticks/2returnsthresholds/1(count ignored);invert/2nil;nice/1,padding/2,clamp/2identity;bandwidth/10.invert_extent/2:nilfor an empty range or unknown value; else{d0 + k·step, d0 + (k+1)·step}.thresholds/1:[]for an empty or single-element range; elsed0 + k·stepforkin1 … n−1.bucket_count/1islength(range).
11.3 Functions
| Function | Contract |
|---|---|
Visualize.Scale.Quantize.new/0 | Returns the default struct. |
Visualize.Scale.Quantize.domain/2 | Sets the continuous domain from a two-element list or tuple; stored as a list. |
Visualize.Scale.Quantize.range/2 | Sets the discrete output list. |
Visualize.Scale.Quantize.apply/2 | Maps a value to a uniform bucket's range element as in 11.2. |
Visualize.Scale.Quantize.scale/2 | Alias of apply/2, retained for one release (1.3). |
Visualize.Scale.Quantize.invert_extent/2 | Returns the domain sub-interval for a range element, or nil. |
Visualize.Scale.Quantize.thresholds/1 | Returns the n − 1 interior bucket boundaries. |
Visualize.Scale.Quantize.bucket_count/1 | Returns the number of range elements. |
Visualize.Scale.Quantize.invert/2 | Returns nil. |
Visualize.Scale.Quantize.ticks/2 | Returns thresholds/1. |
Visualize.Scale.Quantize.nice/1 | Identity. |
Visualize.Scale.Quantize.padding/2 | Identity. |
Visualize.Scale.Quantize.bandwidth/1 | Returns 0. |
Visualize.Scale.Quantize.clamp/2 | Identity. |
12. Visualize.Scale.Threshold
12.1 Struct
%Visualize.Scale.Threshold{domain: [], range: []}. The range SHOULD have exactly one more element than the domain.
12.2 Semantics
domain/2sorts the thresholds ascending.range/2stores the list unchecked.apply/2:nilfor an empty range; otherwiseEnum.at(range, k)wherekis the number of thresholds≤ value(binary search; a value equal to a threshold falls in the upper bucket). A range shorter thanlength(domain) + 1yieldsnilfor the highest buckets.ticks/2returns the domain (count ignored);invert/2nil;nice/1,padding/2,clamp/2identity;bandwidth/10.invert_extent/2:nilfor an empty range or unknown value; bucket 0 →{:neg_infinity, t_0}(or{:neg_infinity, :infinity}with no thresholds); last bucket →{t_{last}, :infinity}; middle →{t_{k−1}, t_k}.thresholds/1returns the domain.copy_with_domain/3(scale, data, nwithn > 0) sortsdataand sets the domain to the de-duplicated values at indices⌊i · len / (n + 1)⌋(capped atlen − 1) foriin1 … n;[]for empty data. The range is untouched.
12.3 Functions
| Function | Contract |
|---|---|
Visualize.Scale.Threshold.new/0 | Returns the default struct. |
Visualize.Scale.Threshold.domain/2 | Sets the thresholds (sorted on entry). |
Visualize.Scale.Threshold.range/2 | Sets the output list; SHOULD be one longer than the domain. |
Visualize.Scale.Threshold.apply/2 | Maps a value to the bucket's range element as in 12.2. |
Visualize.Scale.Threshold.scale/2 | Alias of apply/2, retained for one release (1.3). |
Visualize.Scale.Threshold.invert_extent/2 | Returns {lo, hi} with :neg_infinity/:infinity at the ends, or nil. |
Visualize.Scale.Threshold.thresholds/1 | Returns the domain. |
Visualize.Scale.Threshold.copy_with_domain/3 | Returns the scale with n thresholds inferred from data at evenly spaced ranks. |
Visualize.Scale.Threshold.invert/2 | Returns nil. |
Visualize.Scale.Threshold.ticks/2 | Returns the domain. |
Visualize.Scale.Threshold.nice/1 | Identity. |
Visualize.Scale.Threshold.padding/2 | Identity. |
Visualize.Scale.Threshold.bandwidth/1 | Returns 0. |
Visualize.Scale.Threshold.clamp/2 | Identity. |
13. Visualize.Scale.Color
13.1 Struct
%Visualize.Scale.Color{type: :sequential, domain: [0, 1], interpolator: nil, clamp?: true}. diverging/1 sets type: :diverging and domain: [0, 0.5, 1].
13.2 Interpolators
sequential/1, diverging/1, and range/2 accept exactly one of:
| Input | Resolution |
|---|---|
| atom | a scheme name from 13.4; an unknown atom raises ArgumentError naming schemes/0 (D-17) |
| list of colour strings | used as evenly spaced stops; each MUST be "#rgb" or "#rrggbb" |
| 1-arity function | called with t ∈ [0, 1], MUST return a colour string |
Stop-list interpolation: n = length − 1, i = t · n, i0 = clamp(⌊i⌋, 0, n − 1), i1 = min(i0 + 1, n), local = i − i0; each RGB channel is round(c0 + (c1 − c0) · local) clamped to 0…255; output is lowercase "#rrggbb". With clamp? false and t outside [0, 1], local leaves [0, 1] and the end stops are extrapolated along their last segment; the channel clamp keeps the result a well-formed colour.
13.3 Semantics
domain/2: sequential takes[d0, d1]; diverging takes[d0, d1, d2]withd1the pivot. Other lengths fail to match.apply/2sequential:t = (v − d0) / (d1 − d0). Diverging:t = 0.5 · (v − d0) / (d1 − d0)whenv < d1,0.5 + 0.5 · (v − d1) / (d2 − d1)whenv > d1, else0.5.tis clamped to[0, 1]whenclamp?(default true), then passed to the interpolator.ticks/2:Linear.ticks/2over the domain's extent —[d0, d1]for a sequential scale,[d0, d2]for a diverging one.invert/2nil;nice/1,padding/2identity;bandwidth/10.schemes/0returns the 25 scheme names in unspecified (map-key) order;scheme/1returns the stop list ornil.
13.4 Schemes
Stops are D3-compatible hex strings; the count per scheme is given in parentheses.
Sequential, single-hue (8 stops each): :blues, :greens, :greys, :oranges, :purples, :reds.
Sequential, multi-hue: :viridis (9), :inferno (10), :magma (10), :plasma (10).
Diverging: :brbg (9), :piyg (9), :prgn (9), :rdbu (9), :rdgy (9), :rdylbu (9), :rdylgn (9), :spectral (11).
Categorical: :category10 (10), :accent (8), :dark2 (8), :paired (10), :set1 (9), :set2 (8), :set3 (10).
Categorical schemes are stop lists like any other; used with sequential/1 they are interpolated, not indexed. To pick categorical colours discretely, pass Color.scheme(name) as the range of a Visualize.Scale.Ordinal.
13.5 Functions
| Function | Contract |
|---|---|
Visualize.Scale.Color.sequential/1 | Creates a sequential scale with domain [0, 1] from an interpolator (13.2). |
Visualize.Scale.Color.diverging/1 | Creates a diverging scale with domain [0, 0.5, 1] from an interpolator (13.2). |
Visualize.Scale.Color.domain/2 | Sets a two-element (sequential) or three-element (diverging) domain. |
Visualize.Scale.Color.range/2 | Replaces the interpolator; accepts a scheme atom, a stop list, or a 1-arity function. |
Visualize.Scale.Color.apply/2 | Returns the colour string for a value as in 13.3. |
Visualize.Scale.Color.clamp/2 | Sets clamp? (default true). |
Visualize.Scale.Color.schemes/0 | Returns the list of 25 scheme atoms. |
Visualize.Scale.Color.scheme/1 | Returns the stop list for a scheme atom, or nil. |
Visualize.Scale.Color.invert/2 | Returns nil. |
Visualize.Scale.Color.ticks/2 | Linear.ticks/2 over the domain's extent (see 13.3). |
Visualize.Scale.Color.nice/1 | Identity. |
Visualize.Scale.Color.padding/2 | Identity. |
Visualize.Scale.Color.bandwidth/1 | Returns 0. |
14. Visualize.Scale.Radial
14.1 Struct
%Visualize.Scale.Radial{domain: [0, 1], range: [0, 6.283185307179586], clamp?: false}. The range is in radians and defaults to one full turn, [0, 2π]; range/2 sets any two radians, so a half rose spans [−π/2, π/2].
14.2 Semantics
A radial scale maps a cyclic quantity — compass bearing, hour of day, day of year — to an angle (D-30). Angles follow Visualize.Shape.Arc's convention (spec/04 §6.2): radians clockwise from 12 o'clock, so a bearing domain [0, 360] over the default range puts north at the top and east at the right with no further arithmetic.
apply/2:Linear.apply/2over the domain and range:t = (v − d0) / (d1 − d0), clamped to[0, 1]whenclamp?, resultr0 + t · (r1 − r0)in radians. A value outside the domain extrapolates (past a full turn) unless clamped; callers wrapping a cyclic value MUST reduce it modulo the period themselves.invert/2:Linear.invert/2; never clamps.ticks/2(countn):nequal divisions of the domain fromd0:d0 + i · (d1 − d0) / nforiin0 … n − 1when the range spans a full turn (|r1 − r0| ≥ 2π − 1e−9, whered1coincides withd0on the circle), andiin0 … notherwise.n ≤ 0yields[]. UnlikeLinear.ticks/2, the count is exact and the values are not rounded to a nice step: a compass asks for 4, 8, or 16 and gets them; the nice-step sequence would give0, 100, 200, 300.nice/1: identity. The domain of a cyclic quantity is its period, already round; extending it would break the cycle.padding/2identity;bandwidth/10.
14.3 Functions
| Function | Contract |
|---|---|
Visualize.Scale.Radial.new/0 | Returns the default struct: domain [0, 1], range [0, 2π]. |
Visualize.Scale.Radial.domain/2 | Sets the domain; a two-element list of numbers. |
Visualize.Scale.Radial.range/2 | Sets the range; a two-element list of radians. |
Visualize.Scale.Radial.clamp/2 | Sets clamp?. |
Visualize.Scale.Radial.apply/2 | Maps a domain value to radians as in 14.2; returns a float. |
Visualize.Scale.Radial.invert/2 | Maps radians back to the domain; returns a float. |
Visualize.Scale.Radial.ticks/2 | count equal divisions of the domain as in 14.2. |
Visualize.Scale.Radial.nice/1 | Identity. |
Visualize.Scale.Radial.padding/2 | Identity. |
Visualize.Scale.Radial.bandwidth/1 | Returns 0. |