Status: Implemented

This document specifies the general-purpose modules used across the library: Visualize.Color (parsing, colour spaces and manipulation), Visualize.Interpolate (interpolator factories), Visualize.Ease (easing curves), Visualize.Random (seeded random variates), Visualize.Data (statistics and data shaping), Visualize.Data.Table (tabular sources as rows) and Visualize.Theme (the colours, font and sizes every mark draws with). All are pure except Random, which reads and advances the calling process's :rand state.

1. Color

1.1 Struct

%Visualize.Color{r, g, b, opacity} with r, g, b in [0, 255] and opacity in [0, 1] (default 1.0). Constructors round channels to integers; interpolation may leave them as floats.

1.2 Parsing

parse/1 trims and lower-cases its argument and returns a colour or nil. A non-binary argument returns nil. Accepted syntaxes:

  • Hex: #rgb, #rgba, #rrggbb, #rrggbbaa. Short digits are doubled; an alpha byte becomes opacity = a / 255. Any other length after # returns nil; a non-hex digit raises ArgumentError.
  • rgb(r, g, b) and rgba(r, g, b, a): integer channels, optional decimal alpha, arbitrary whitespace around separators. Percentages are not accepted. A non-matching string returns nil.
  • hsl(h, s, l) and hsla(h, s, l, a): decimal components, % optional on s and l. A non-matching string returns nil.
  • Named colours: the 148 CSS colour keywords (the 147 classic names plus rebeccapurple, with both gray and grey spellings). Unknown names return nil.

named/1 returns the lower-case hex string for a keyword (case-insensitively) or nil; named_colors/0 returns the keyword list in unspecified order.

1.3 Constructors

  • rgb/3,4 clamps channels to [0, 255] and opacity to [0, 1] without rounding.
  • hsl/3,4 takes h in degrees (truncated to an integer and wrapped into [0, 360)), s and l in [0, 100] (clamped); channels are rounded.
  • lab/3,4 takes CIE Lab* (l 0–100, a and b roughly −128–127), converts through XYZ with reference white Xn = 0.96422, Yn = 1, Zn = 0.82521 (the d3-color constants) to sRGB, clamps and rounds.
  • hcl/3,4 takes hue in degrees, chroma and luminance, converts to Lab (a = c cos h, b = c sin h) and delegates to lab/4.

1.4 Conversions

ConversionOutput
to_rgb/1{r, g, b} rounded integers.
to_rgba/1{r, g, b, opacity}.
to_hsl/1%{h, s, l, opacity} with h a float in [0, 360), s and l in [0, 100].
to_lab/1%{l, a, b, opacity}.
to_hcl/1%{h, c, l, opacity} with h in [0, 360).
to_hex/1#rrggbb in lower case, with aa appended when opacity < 1.
to_string/1rgb(r, g, b) when opacity == 1, otherwise rgba(r, g, b, opacity) with the opacity printed as-is.

1.5 Manipulation

  • brighter/1,2 and darker/1,2: k defaults to 1; the colour is converted to Lab, L is increased (decreased) by 18 * k, and converted back. The factor is additive in Lab luminance, not multiplicative in RGB.
  • saturate/1,2 multiplies HSL saturation by amount (default 1.2); desaturate/1,2 is saturate/2 with default 0.8.
  • rotate/2 adds degrees to the HSL hue.
  • grayscale/1 sets every channel to round(0.299 r + 0.587 g + 0.114 b).
  • invert/1 sets each channel to 255 - channel.
  • with_opacity/2 sets a clamped opacity.
  • luminance/1 is WCAG relative luminance in [0, 1]; contrast/2 is (L_light + 0.05) / (L_dark + 0.05) in [1, 21].
  • mix/2 is interpolate_rgb(c1, c2, 0.5).

1.6 Interpolation

Each interpolate_*/3 takes two colours and t (not clamped) and returns a colour. interpolate_rgb/3 interpolates channels and opacity linearly and leaves them unrounded. interpolate_hsl/3 and interpolate_hcl/3 interpolate in HSL and HCL respectively, taking the shorter arc around the hue circle; interpolate_lab/3 interpolates in Lab. Results other than RGB pass through the corresponding constructor and are rounded.

1.7 Functions

FunctionContract
Visualize.Color.parse/1Parses hex, rgb(), hsl() or a keyword; nil when unrecognised (1.2).
Visualize.Color.named/1Hex string for a colour keyword, or nil.
Visualize.Color.named_colors/0All 148 colour keywords.
Visualize.Color.rgb/3rgb(r, g, b, 1.0).
Visualize.Color.rgb/4Colour from clamped RGB channels and opacity.
Visualize.Color.hsl/3hsl(h, s, l, 1.0).
Visualize.Color.hsl/4Colour from HSL; hue truncated to a whole degree.
Visualize.Color.lab/3lab(l, a, b, 1.0).
Visualize.Color.lab/4Colour from CIE Lab, clamped to sRGB.
Visualize.Color.hcl/3hcl(h, c, l, 1.0).
Visualize.Color.hcl/4Colour from HCL via Lab.
Visualize.Color.to_rgb/1{r, g, b} integers.
Visualize.Color.to_rgba/1{r, g, b, opacity}.
Visualize.Color.to_hsl/1HSL map.
Visualize.Color.to_lab/1Lab map.
Visualize.Color.to_hcl/1HCL map.
Visualize.Color.to_hex/1Lower-case hex, with alpha when translucent.
Visualize.Color.to_string/1CSS rgb()/rgba() string.
Visualize.Color.brighter/1brighter(color, 1).
Visualize.Color.brighter/2Lab luminance plus 18 k.
Visualize.Color.darker/1darker(color, 1).
Visualize.Color.darker/2Lab luminance minus 18 k.
Visualize.Color.with_opacity/2Copy with clamped opacity.
Visualize.Color.saturate/1saturate(color, 1.2).
Visualize.Color.saturate/2HSL saturation multiplied by amount.
Visualize.Color.desaturate/1saturate(color, 0.8).
Visualize.Color.desaturate/2saturate(color, amount).
Visualize.Color.rotate/2Hue rotated by degrees.
Visualize.Color.grayscale/1Luma-weighted grey.
Visualize.Color.invert/1Channel complement.
Visualize.Color.luminance/1WCAG relative luminance.
Visualize.Color.contrast/2WCAG contrast ratio.
Visualize.Color.mix/2Equal RGB mix.
Visualize.Color.interpolate_rgb/3Linear RGB interpolation, unrounded.
Visualize.Color.interpolate_hsl/3HSL interpolation, shortest hue arc.
Visualize.Color.interpolate_lab/3Lab interpolation.
Visualize.Color.interpolate_hcl/3HCL interpolation, shortest hue arc.

2. Interpolate

Every function in Visualize.Interpolate returns a one-argument function of t. t is not clamped unless stated; callers pass [0, 1] for the nominal range.

2.1 Colour inputs and output

rgb/2, hsl/2 and hsl_long/2 accept each endpoint as a hex string with or without # (3 or 6 digits), an {r, g, b} tuple, or one of thirteen basic names (red, green, blue, white, black, yellow, cyan, magenta, orange, purple, pink, gray, grey). Anything else is treated as black. %Visualize.Color{} structs are not accepted. The returned function yields #RRGGBB with upper-case hex digits (as produced by Integer.to_string/2).

2.2 Interpolators

FactoryInputReturned function
number/2Two numberst -> a + (b - a) * t.
round/2Two numberst -> round(a + (b - a) * t), an integer.
discrete/1Non-empty list of n valuest -> values[clamp(floor(t * n), 0, n - 1)].
rgb/2Two colours (2.1)Per-channel linear blend, rounded and clamped, as hex.
hsl/2Two coloursHSL blend with the shorter hue arc, as hex.
hsl_long/2Two coloursHSL blend with the longer hue arc, as hex.
array/2Two equal-length number listsElement-wise number/2; returns a list.
datetime/2Two DateTimeMillisecond-precision blend, rounded; returns a DateTime.
date/2Two DateDay-precision blend, rounded; returns a Date.
string/2Two stringsNumbers embedded in b (pattern -?\d+\.?\d*) are blended pairwise with the numbers in a; non-numeric text is taken from b. When a has fewer numbers the missing ones start from 0; extra numbers in a are ignored. A whole result prints as an integer, otherwise as a float rounded to 4 decimals.
transform/2Two SVG transform stringsRecognises the first translate(x[, y]) (missing y is 0), scale(s[, sy]) (a single value applies to both axes) and rotate(a) in each string; rotate(a, cx, cy) parses as 0. A component present in only one string is held constant; one absent from both defaults to identity. Output is translate(x, y) scale(sx, sy) rotate(r) joined by spaces, omitting components equal to identity, or "". Numbers print as integers when whole, otherwise rounded to 4 decimals.
basis/1List of at least two numbersUniform cubic B-spline through the values with clamped end conditions; t is clamped to [0, 1], and t = 0 and t = 1 return the first and last values exactly.
zoom/2Two views [cx, cy, w]van Wijk–Nuij smooth zoom with ρ = 1.4; returns [cx, cy, w]. When the centres coincide (distance below 1.0e-6) only w changes, exponentially.

2.3 Functions

FunctionContract
Visualize.Interpolate.number/2Linear numeric interpolator.
Visualize.Interpolate.round/2Linear interpolator rounded to an integer.
Visualize.Interpolate.discrete/1Stepped interpolator over a non-empty list.
Visualize.Interpolate.rgb/2RGB colour interpolator returning hex.
Visualize.Interpolate.hsl/2HSL interpolator, short hue arc.
Visualize.Interpolate.hsl_long/2HSL interpolator, long hue arc.
Visualize.Interpolate.array/2Element-wise interpolator over equal-length lists.
Visualize.Interpolate.datetime/2DateTime interpolator at millisecond precision.
Visualize.Interpolate.date/2Date interpolator at day precision.
Visualize.Interpolate.string/2Interpolates numbers embedded in a string template.
Visualize.Interpolate.transform/2Interpolates translate, scale and rotate of SVG transform strings.
Visualize.Interpolate.basis/1B-spline interpolator through a value list.
Visualize.Interpolate.zoom/2Smooth zoom interpolator between [cx, cy, w] views.

3. Ease

Visualize.Ease provides 25 easing functions from t in [0, 1] to a float, nominally in [0, 1] (the elastic_* and back_* families overshoot). names/0 returns the 25 atoms below in this order; by_name/1 returns the arity-1 function for an atom and raises FunctionClauseError for any other atom. frames/2 with count > 1 returns count values at t = i / (count - 1) for i in 0..count - 1; frames(_, 1) returns [1.0].

3.1 Formulas

With c1 = 1.70158, c2 = c1 * 1.525, c3 = c1 + 1, c4 = 2π / 3, c5 = 2π / 4.5, n1 = 7.5625, d1 = 2.75:

NameFormula
:lineart
:quad_int²
:quad_outt (2 - t)
:quad_in_outt < 0.5: 2t²; else -1 + (4 - 2t) t
:cubic_int³
:cubic_out(t - 1)³ + 1
:cubic_in_outt < 0.5: 4t³; else (2t - 2)³ / 2 + 1
:sin_in1 - cos(πt / 2)
:sin_outsin(πt / 2)
:sin_in_out-(cos(πt) - 1) / 2
:exp_in2^(10(t - 1)); exactly 0.0 for the integer 0
:exp_out1 - 2^(-10t); exactly 1.0 for the integer 1
:exp_in_outt < 0.5: 2^(20t - 10) / 2; else (2 - 2^(-20t + 10)) / 2; exact at integer 0 and 1
:circle_in1 - sqrt(1 - t²)
:circle_outsqrt(1 - (t - 1)²)
:circle_in_outt < 0.5: (1 - sqrt(1 - 4t²)) / 2; else (sqrt(1 - (-2t + 2)²) + 1) / 2
:elastic_in-2^(10t - 10) sin((10t - 10.75) c4); exact at integer 0 and 1
:elastic_out2^(-10t) sin((10t - 0.75) c4) + 1; exact at integer 0 and 1
:elastic_in_outt < 0.5: -2^(20t - 10) sin((20t - 11.125) c5) / 2; else 2^(-20t + 10) sin((20t - 11.125) c5) / 2 + 1; exact at integer 0 and 1
:back_inc3 t³ - c1 t²
:back_out1 + c3 (t - 1)³ + c1 (t - 1)²
:back_in_outt < 0.5: (2t)² ((c2 + 1) 2t - c2) / 2; else ((2t - 2)² ((c2 + 1)(2t - 2) + c2) + 2) / 2
:bounce_in1 - bounce_out(1 - t)
:bounce_outt < 1/d1: n1 t²; t < 2/d1: n1 (t - 1.5/d1)² + 0.75; t < 2.5/d1: n1 (t - 2.25/d1)² + 0.9375; else n1 (t - 2.625/d1)² + 0.984375
:bounce_in_outt < 0.5: (1 - bounce_out(1 - 2t)) / 2; else (1 + bounce_out(2t - 1)) / 2

The exact-endpoint clauses match the integer literals 0 and 1 only; the floats 0.0 and 1.0 fall through to the formula (exp_in(0.0) is 2^-10).

Parameterised variants: poly_in/2, poly_out/2 and poly_in_out/2 take an exponent e (default 3): t^e; 1 - (1 - t)^e; t < 0.5: 2^(e-1) t^e else 1 - (-2t + 2)^e / 2. back_in/2 and back_out/2 take an overshoot s replacing c1 (with c3 = s + 1); there is no back_in_out/2.

3.2 Functions

FunctionContract
Visualize.Ease.linear/1Identity as a float.
Visualize.Ease.quad_in/1Quadratic ease-in.
Visualize.Ease.quad_out/1Quadratic ease-out.
Visualize.Ease.quad_in_out/1Quadratic ease-in-out.
Visualize.Ease.cubic_in/1Cubic ease-in.
Visualize.Ease.cubic_out/1Cubic ease-out.
Visualize.Ease.cubic_in_out/1Cubic ease-in-out.
Visualize.Ease.poly_in/1poly_in(t, 3).
Visualize.Ease.poly_in/2Polynomial ease-in with exponent.
Visualize.Ease.poly_out/1poly_out(t, 3).
Visualize.Ease.poly_out/2Polynomial ease-out with exponent.
Visualize.Ease.poly_in_out/1poly_in_out(t, 3).
Visualize.Ease.poly_in_out/2Polynomial ease-in-out with exponent.
Visualize.Ease.sin_in/1Sinusoidal ease-in.
Visualize.Ease.sin_out/1Sinusoidal ease-out.
Visualize.Ease.sin_in_out/1Sinusoidal ease-in-out.
Visualize.Ease.exp_in/1Exponential ease-in.
Visualize.Ease.exp_out/1Exponential ease-out.
Visualize.Ease.exp_in_out/1Exponential ease-in-out.
Visualize.Ease.circle_in/1Circular ease-in.
Visualize.Ease.circle_out/1Circular ease-out.
Visualize.Ease.circle_in_out/1Circular ease-in-out.
Visualize.Ease.elastic_in/1Elastic ease-in.
Visualize.Ease.elastic_out/1Elastic ease-out.
Visualize.Ease.elastic_in_out/1Elastic ease-in-out.
Visualize.Ease.back_in/1Back ease-in with overshoot 1.70158.
Visualize.Ease.back_in/2Back ease-in with custom overshoot.
Visualize.Ease.back_out/1Back ease-out with overshoot 1.70158.
Visualize.Ease.back_out/2Back ease-out with custom overshoot.
Visualize.Ease.back_in_out/1Back ease-in-out.
Visualize.Ease.bounce_in/1Bounce ease-in.
Visualize.Ease.bounce_out/1Bounce ease-out.
Visualize.Ease.bounce_in_out/1Bounce ease-in-out.
Visualize.Ease.by_name/1Arity-1 function for one of the 25 names; raises otherwise.
Visualize.Ease.names/0The 25 easing names.
Visualize.Ease.frames/2count eased samples at evenly spaced t.

4. Random

Visualize.Random draws from the process-local :rand generator. seed/1 calls :rand.seed(:exsss, {s, 2s, 3s}) and returns the new state, making subsequent draws in the calling process reproducible.

4.1 Distributions

FamilyParameters and defaultsResult
uniform/0,1,2min = 0, max = 1Float in [min, max).
uniform_int/2min, maxInteger in [min, max].
normal/0,1,2mean = 0, stddev = 1Box–Muller normal variate.
log_normal/0,1,2mu = 0, sigma = 1exp(normal(mu, sigma)).
exponential/0,1lambda = 1-ln(1 - u) / lambda.
pareto/0,1alpha = 11 / (1 - u)^(1 / alpha), minimum 1.
bernoulli/0,1p = 0.51 with probability p, else 0.
binomial/2n, pNumber of successes in n Bernoulli trials; MUST be 0 for n = 0.
geometric/1pNumber of failures before the first success, trunc(ln(1 - u) / ln(1 - p)).
poisson/1lambdaKnuth's multiplication method.
gamma/1,2shape > 0, scale = 1Marsaglia–Tsang; shape < 1 is boosted through shape + 1.
beta/2alpha, betax / (x + y) with x = gamma(alpha), y = gamma(beta).
weibull/1,2shape, scale = 1scale * (-ln(1 - u))^(1 / shape).
cauchy/0,1,2location = 0, scale = 1location + scale * tan(π (u - 0.5)).

4.2 Points and collections

  • in_circle/0..3 (cx = 0, cy = 0, radius = 1): uniform over the disc using r = radius * sqrt(u).
  • on_circle/0..3 (same defaults): uniform on the circumference.
  • in_rect/4 (x0, y0, x1, y1): uniform over the rectangle.
  • samples/2 (fun, n): a list of n calls of the zero-arity function; MUST be [] for n = 0.
  • shuffle/1: Enum.shuffle/1.
  • sample/2 (list, n): n distinct elements of a shuffled list (fewer when the list is shorter).
  • pick/1: a random element, or nil for [].

binomial(0, p) is 0 and samples(fun, 0) is []: both iterate 1..n//1, which is empty for n = 0 (D-44).

4.3 Functions

FunctionContract
Visualize.Random.seed/1Seeds :rand with :exsss from an integer.
Visualize.Random.uniform/0uniform(0, 1).
Visualize.Random.uniform/1uniform(min, 1).
Visualize.Random.uniform/2Float in [min, max).
Visualize.Random.uniform_int/2Integer in [min, max].
Visualize.Random.normal/0normal(0, 1).
Visualize.Random.normal/1normal(mean, 1).
Visualize.Random.normal/2Gaussian variate.
Visualize.Random.log_normal/0log_normal(0, 1).
Visualize.Random.log_normal/1log_normal(mu, 1).
Visualize.Random.log_normal/2Log-normal variate.
Visualize.Random.exponential/0exponential(1).
Visualize.Random.exponential/1Exponential variate with rate lambda.
Visualize.Random.pareto/0pareto(1).
Visualize.Random.pareto/1Pareto variate with shape alpha and minimum 1.
Visualize.Random.bernoulli/0bernoulli(0.5).
Visualize.Random.bernoulli/11 with probability p, else 0.
Visualize.Random.binomial/2Successes in n trials of probability p.
Visualize.Random.geometric/1Failures before the first success.
Visualize.Random.poisson/1Poisson count with mean lambda.
Visualize.Random.gamma/1gamma(shape, 1).
Visualize.Random.gamma/2Gamma variate; requires shape > 0.
Visualize.Random.beta/2Beta variate.
Visualize.Random.weibull/1weibull(shape, 1).
Visualize.Random.weibull/2Weibull variate.
Visualize.Random.cauchy/0cauchy(0, 1).
Visualize.Random.cauchy/1cauchy(location, 1).
Visualize.Random.cauchy/2Cauchy variate.
Visualize.Random.in_circle/0in_circle(0, 0, 1).
Visualize.Random.in_circle/1in_circle(cx, 0, 1).
Visualize.Random.in_circle/2in_circle(cx, cy, 1).
Visualize.Random.in_circle/3Uniform point in a disc.
Visualize.Random.on_circle/0on_circle(0, 0, 1).
Visualize.Random.on_circle/1on_circle(cx, 0, 1).
Visualize.Random.on_circle/2on_circle(cx, cy, 1).
Visualize.Random.on_circle/3Uniform point on a circle.
Visualize.Random.in_rect/4Uniform point in a rectangle.
Visualize.Random.samples/2n draws from a zero-arity function.
Visualize.Random.shuffle/1Random permutation.
Visualize.Random.sample/2n distinct random elements.
Visualize.Random.pick/1One random element or nil.

5. Data

Visualize.Data shadows Kernel.min/2 and Kernel.max/2 inside the module. Every statistic takes an optional arity-1 accessor as its last argument (nil means the elements are the values).

5.1 Statistics

StatisticEmpty inputResult
min/1,2, max/1,2nilLeast or greatest value by term order.
extent/1,2nil{min, max}.
sum/1,20Sum.
mean/1,2nilArithmetic mean (float).
median/1,2nilMiddle of the sorted values; mean of the two middles for even counts.
quantile/2,3nilLinear interpolation at index (n - 1) * p of the sorted values; p MUST be in [0, 1] (otherwise FunctionClauseError).
variance/1,2nil; also nil for one elementSample variance with n - 1 denominator.
deviation/1,2nilSquare root of variance/2.

5.2 Grouping and shaping

  • group/2 — Enum.group_by/2: a map from key to the elements with that key, in input order.

  • rollup/3 — group/2 followed by applying the reducer to each group's list: a map from key to reduced value.

  • bin/1,2 — histogram bins. Options: :thresholds — an integer count n (default 10), producing n - 1 interior thresholds evenly spaced over the domain and hence n bins, or an explicit list of thresholds producing length + 1 bins; :domain — [min, max], defaulting to the data extent. Returns [%{x0, x1, values}] where each value is placed in the first bin with x0 <= v < x1, a value equal to the last x1 goes in the last bin, and values outside the domain are dropped; values keep input order. [] yields [].

  • cross/2,3 — the Cartesian product of two lists as {a, b} tuples, or as combine.(a, b) when a two-arity function is given.

  • range/2,3 — start, stop, step = 1: max(0, ceil((stop - start) / step)) values start + i * step, so the result is empty when stop is not beyond start in the direction of step; negative steps count down. step MUST be non-zero (otherwise FunctionClauseError).

  • ticks/3 — start, stop, count > 0: nice tick values. The step is 10^k times 1, 2, 5 or 10, chosen from |stop - start| / count with the d3 thresholds sqrt(2), sqrt(10), sqrt(50); ticks run from ceil(start / step) * step to floor(stop / step) * step inclusive, rounded to 10 decimals, and are [] when no multiple of the step lies inside the span. A zero span returns [start]; start > stop returns the ticks of the reversed span in descending order. count MUST be positive (otherwise FunctionClauseError).

  • sort/2,3 — Enum.sort_by/3 on the accessor, :asc (default) or :desc.

  • unique/1,2 — Enum.uniq/1, or Enum.uniq_by/2 with an accessor.

  • lttb/2,3 and m4/2,3 — shape-preserving downsampling of a series (#448): a subsequence of the input elements, never new ones, so a tooltip over a kept point reads a real datum. The accessor returns {x, y}; without one the elements are {x, y} tuples. A datum is defined when both x and y are numbers; any other datum is a gap. The defined data split at the gaps into runs, and both functions reduce each run separately and keep exactly the first element of every run of gaps, in place — so a line drawn over the result still breaks where the input broke (spec/04's defined/2). Both are one pass over the input plus a constant amount per element: O(n) time, and O(n) space for the random access a run's buckets need. Both read the elements in input order and expect x ascending within a run, as a series is; neither sorts.

    • lttb(data, n, accessor) — Largest-Triangle-Three-Buckets (Steinarsson 2013, Downsampling Time Series for Visual Representation, §4.2). n is an integer >= 3 (otherwise FunctionClauseError). Input of n or fewer elements, gaps counted, is returned unchanged. A single run of L > n elements keeps exactly n: its first and last, and one per bucket of the n − 2 buckets that split the interior evenly — bucket i (0 ≤ i < n − 2) is the interior indices [floor(i·e) + 1, floor((i + 1)·e) + 1) with e = (L − 2) / (n − 2) — choosing in each the element that forms the largest triangle with the element chosen in the bucket before it (the first element, for bucket 0) and the mean point of the bucket after it (indices [floor((i + 1)·e) + 1, min(floor((i + 2)·e) + 1, L)), which for the last bucket holds the last element); a tie keeps the earliest. With gaps, a run of L defined elements out of D in all has the budget b = max(min(L, 2), floor(n · L / D)): a run with b ≥ L is kept whole, b = 2 keeps its first and last, and otherwise LTTB with b. The total is therefore about n and exactly n without gaps.
    • m4(data, width, accessor) — M4 (Jugel et al. 2014, M4: A Visualization-Oriented Time Series Data Aggregation): per pixel column, the first, last, minimum-y and maximum-y elements. width is an integer >= 1 (otherwise FunctionClauseError), the column count. The columns split the defined x extent [x_min, x_max] evenly: a defined datum's column is min(width − 1, floor((x − x_min) / (x_max − x_min) · width)), every datum column 0 when x_min == x_max. A column visit is a maximal stretch of consecutive defined elements of one run in one column; each visit keeps its first, its last, its earliest least y and its earliest greatest y, deduplicated and in input order — so at most four per visit, and a run whose every visit has four or fewer elements is kept whole. Drawn as a line on a plot width pixels wide whose x scale spans [x_min, x_max], the result covers every pixel column the input covers with the same vertical extent and the same entry and exit points, which is why it is lossless at that resolution: a peak is never dropped.

    Neither has an Nx path. LTTB's choice in a bucket depends on the choice in the bucket before it, a serial dependency a vectorised pass cannot express; M4 needs a segmented argmin and argmax per column, which Nx offers no primitive for; and both keep whole elements, which a tensor does not hold, so the cost an Nx path would save — the arithmetic — is not the cost of the call, which is reading the accessor over every element. Visualize.Shape.LineNx's optional-Nx pattern (spec/04 §10.1) is therefore deferred until a measurement shows the arithmetic, not the reading, is what a reduction costs; the benchmark of #448 (a tagged test, not a gate) is that measurement's baseline.

5.3 Functions

FunctionContract
Visualize.Data.min/1min(data, nil).
Visualize.Data.min/2Minimum by accessor; nil for [].
Visualize.Data.max/1max(data, nil).
Visualize.Data.max/2Maximum by accessor; nil for [].
Visualize.Data.extent/1extent(data, nil).
Visualize.Data.extent/2{min, max} by accessor; nil for [].
Visualize.Data.sum/1sum(data, nil).
Visualize.Data.sum/2Sum by accessor; 0 for [].
Visualize.Data.mean/1mean(data, nil).
Visualize.Data.mean/2Mean by accessor; nil for [].
Visualize.Data.median/1median(data, nil).
Visualize.Data.median/2Median by accessor; nil for [].
Visualize.Data.quantile/2quantile(data, p, nil).
Visualize.Data.quantile/3Interpolated p-quantile by accessor; nil for [].
Visualize.Data.variance/1variance(data, nil).
Visualize.Data.variance/2Sample variance; nil for fewer than two values.
Visualize.Data.deviation/1deviation(data, nil).
Visualize.Data.deviation/2Sample standard deviation; nil for fewer than two values.
Visualize.Data.group/2Map from key to element list.
Visualize.Data.rollup/3Map from key to reduced group.
Visualize.Data.bin/1bin(data, []).
Visualize.Data.bin/2Histogram bins %{x0, x1, values} (5.2).
Visualize.Data.cross/2Cartesian product as tuples.
Visualize.Data.cross/3Cartesian product combined by a function.
Visualize.Data.range/2range(start, stop, 1).
Visualize.Data.range/3Arithmetic sequence from start toward stop by step.
Visualize.Data.ticks/3Nice tick values between start and stop (5.2).
Visualize.Data.sort/2sort(data, accessor, :asc).
Visualize.Data.sort/3Sort by accessor in :asc or :desc order.
Visualize.Data.unique/1unique(data, nil).
Visualize.Data.unique/2Distinct elements by accessor, first occurrence kept.
Visualize.Data.lttb/2lttb(data, n, nil): the elements are {x, y} tuples.
Visualize.Data.lttb/3Largest-Triangle-Three-Buckets to about n elements, each run of gaps kept as its first element (5.2); unchanged at n or fewer elements; n >= 3.
Visualize.Data.m4/2m4(data, width, nil): the elements are {x, y} tuples.
Visualize.Data.m4/3Per pixel column of width over the defined x extent, each column visit's first, last, least-y and greatest-y elements, each run of gaps kept as its first element (5.2); width >= 1.

6. Data.Table

Visualize.Data.Table turns the shapes chart data arrives in into the list of rows every generator consumes (04-shapes-and-curves §1). The table package (the Table.Reader protocol and its List and Map impls) is an optional dependency like Nx (D-52): a host that has it reads Explorer frames and any other struct implementing the protocol; without it lists, column maps and tensors still work and a struct source raises UndefinedFunctionError at Table.to_rows/1.

6.1 Sources

Visualize.Data.Table.rows/1 accepts, and distinguishes by shape:

SourceRows
A list of rows (maps or tuples)The list itself: no copy, no traversal.
A keyword list whose values are equal-length listsOne map per index, keyed by the keys. A keyword list is read as columns before rows, as table reads a list; [{:a, [1]}] is therefore one column. A keyword list whose lists differ in length is rows.
A map whose values are equal-length listsOne map per index, keyed by the map's keys (atoms or strings, as given). Any other non-empty plain map raises ArgumentError.
%Nx.Tensor{} of rank 2One tuple per row in column order, which is what the default Visualize.Shape.Line accessors read. Rank 1 yields the list of values; any other rank raises ArgumentError.
Any other structTable.to_rows/1: rows keyed by the reader's column names as it spells them (Explorer: strings), whether the reader traverses by rows or by columns. A struct with no Table.Reader impl raises Protocol.UndefinedError; a reader answering :none raises ArgumentError.

[], %{} and the empty keyword list yield []. A rank-1 tensor bound to a dense series stays a first-class source for Visualize.Shape.LineNx and the binary path; Visualize.Data.Table.rows/1 does not stand between them.

6.2 Column names

A source names its columns with atoms or strings and a caller writes accessors in either. Visualize.Data.Table.get/2 reads the row by the given spelling first, then by the other: an atom name falls back to Atom.to_string/1, a string name to the atom key that spells the same. No atom is ever created. nil when neither is a key, so a missing column behaves as an absent map key does in 04-shapes-and-curves §1.1. Visualize.Data.Table.accessor/1 is fn row -> get(row, name) end, and returns a 1-arity function unchanged so a generator normalises every accessor form through one call. A tuple row has no names: get/2 raises BadMapError.

6.3 Time columns

Visualize.Data.Table.time_columns/1 names the columns a time scale could be inferred from: every key of every map row, sorted by term order (a row without the key contributes nil), kept when the column has at least one non-nil value and every value is nil or temporal?/1. Visualize.Data.Table.temporal?/1 is true for DateTime, Date and NaiveDateTime and for a string one of DateTime.from_iso8601/1, NaiveDateTime.from_iso8601/1 or Date.from_iso8601/1 parses; anything else, including a bare year or a non-ISO date, is false. Detection reads the whole column, not the first row as Tucan does: every row is visited and every string parsed, so the cost is the size of the table, and a column that starts temporal and stops being so is not temporal. Tuple rows have no names and yield [].

Visualize.Data.Table.column_types/1 (#369) types every named column of a table as spec/14 §2.3's four, reading the whole column as time_columns/1 does: a column whose non-nil values are all temporal?/1 is :time; all numbers, :number; all strings (none temporal), :text; all atoms or booleans, :category; a column with no non-nil value, or with values of more than one of those shapes, has no type and is left out. The result is a map from name to type, %{} for an empty table or tuple rows. It is a suggestion for a tool to pre-fill a design's types, never something the layer reads at render.

6.4 Functions

FunctionContract
Visualize.Data.Table.rows/1The row list of any source in 6.1; a list of rows is returned as-is.
Visualize.Data.Table.get/2The named column of a map row by either spelling of the name (6.2); nil when absent.
Visualize.Data.Table.accessor/1fn row -> get(row, name) end; a function is returned unchanged.
Visualize.Data.Table.time_columns/1The names whose whole column is temporal (6.3).
Visualize.Data.Table.column_types/1Every named column's type — :time, :number, :category, :text — read whole; a mixed or empty column is left out (6.3).
Visualize.Data.Table.temporal?/1Whether a value is a DateTime, Date, NaiveDateTime or ISO 8601 string.

7. Theme

Visualize.Theme names the colours, font and sizes a chart draws with, so that no axis, mark or component carries a colour literal and a consumer restyles every chart from one place — a theme struct in Elixir, or CSS custom properties in a stylesheet (D-55). Ggity's Theme and Tucan's themes are the precedent; the CSS half is this library's own, because its output lives in a page.

7.1 Struct and slots

%Visualize.Theme{name, series, axis, grid, text, background, surface, font_family, font_size, label_size, title_size}. name is an atom naming the theme (:default, :dark, or the consumer's own); series is a non-empty list of colour strings; axis, grid, text, background and surface are colour strings; font_family is a CSS font-family string; the three sizes are numbers in user units (pixels).

A slot names one value of a theme:

SlotFieldUsed for
:series_1 … :series_n, or {:series, i}seriesthe i-th categorical colour; {:series, i} cycles, so on a ten-colour theme {:series, 11} is :series_1, and i is any positive integer
:axisaxisdomain lines and tick lines
:gridgridgrid lines, tree links, and any mark that sits behind the data
:texttexttick labels, axis titles and any label beside a mark
:backgroundbackgroundthe colour the chart is drawn on: separators between marks, and — with text — one of the two inks a label drawn over a mark chooses between (7.3)
:surfacesurfacethe plane a chart's data is laid on, between the background and the grid: a map's ocean, and any field that is neither the page nor a data colour and has the grid drawn over it (#422)
:font_familyfont_familyevery text
:font_sizefont_sizetick labels
:label_sizelabel_sizemark labels and axis titles
:title_sizetitle_sizechart titles

slots/1 lists a theme's slots in that order, :series_1 to :series_n first. series_slot/2 is the :series_k atom that {:series, i} cycles to. A slot that is none of these, or :series_k with k > n or k < 1, raises ArgumentError in every function that takes a slot.

7.2 Built-in themes

Fielddefault/0dark/0
name:default:dark
series#3b6fa8 #d9730d #c8373b #2f8f86 #3f8a37 #a8891a #8f5f99 #c9527f #8a6a4f #6f7480#7fb3e6 #f7a35c #f47c7c #6fd3c8 #86d67c #f2d15b #c99bd6 #ffb3c1 #cfa98b #b7bcc6
axis#666666#a0a4ab
grid#e0e0e0#33373f
text#333333#e6e6e6
background#ffffff#1b1e24
surface#eef2f6#23272f
font_familysans-serifsans-serif
font_size1010
label_size1212
title_size1616

Both palettes are ten hues in the order of d3's category10 — blue, orange, red, teal, green, yellow, purple, pink, brown, grey — each adjusted until it holds at least 3:1 (Visualize.Color.contrast/2, §1.5) against its own background; text holds at least 4.5:1 and axis at least 3:1. Neither palette is a d3 scheme, because every d3 categorical scheme has a colour under 2.1:1 on white (category10's olive is 2.0:1), and a theme that promises contrast cannot ship one unchanged. 7.4 states the floors.

surface is a step off the background towards the grid — a cool grey on the light theme, a step above #1b1e24 on the dark — so a field reads as a plane under the data without becoming a data colour. new/1 is default/0 with the given keyword fields replaced, so a host theme that names no surface takes the light one, as it takes every other field it leaves out; merge/2 replaces the fields of any theme. An unknown field raises KeyError; an empty series raises ArgumentError.

7.3 Resolution

resolve(theme, slot, mode) is the one door through which anything reads a theme, and the mode is what the render target needs:

  • :literal — the slot's value as stored: a colour or font string, or a number for a size. A canvas colour is a literal, so every canvas backend resolves this way.
  • :css — a CSS custom-property reference with the literal as its fallback: var(--vis-axis, #666666), var(--vis-series-3, #c8373b), var(--vis-font-size, 10px) (a size gains px). What the SVG output carries.

css_name/1 is the property name of a slot atom: --vis- and the slot with _ as - (:series_1 → --vis-series-1, :font_family → --vis-font-family). mode/1 maps a backend — a module, :svg, :canvas or nil, resolved by Visualize.Backend.resolve/1 (spec/02 §3.2) — to the mode: Visualize.Backend.SVG is :css, every other backend :literal. Visualize.Axis.render/2 applies it (spec/05 §1.6) and the style grammar of the declarative layer resolves through the same call.

The reference carries its fallback rather than the root declaring the property (D-55). A declaration on the <svg> — inline or in a <style> — would beat every ancestor rule in the cascade, so a consumer could not switch a theme from a class without !important; with the fallback form a chart rendered outside any stylesheet (a standalone .svg, an <img>) still draws in its theme, and a rule on any ancestor, .night { --vis-axis: #a0a4ab }, overrides it. stylesheet/1 writes that rule for a whole theme — .vis-theme-<name> { then one --vis-<slot>: <value>; line per slot of slots/1, sizes with px, then } — so a consumer that includes stylesheet(dark()) in its CSS switches every chart under an element with class="vis-theme-dark" without touching Elixir.

The contrast ink (#486, D-123). ink(theme, under) is the colour a text drawn on under should take: the theme's text or its background, whichever has the higher contrast ratio against under by Visualize.Color.contrast/2 — text on a tie — always as the slot's literal, never a reference. When the higher of the two is under 4.5:1 (WCAG AA for text), it is #000000 or #ffffff instead, whichever contrasts more, and one of those two always holds 4.58:1 or better against any colour, so the ink always meets AA against the colour it is chosen for. under is a colour string Visualize.Color.parse/1 reads, a %Visualize.Color{}, or nil for "nothing under the text" — the theme's own background — and a string it cannot parse is nil; a slot whose own value does not parse is skipped. It is a literal because the choice is made between literals: a var(--vis-text, …) that a stylesheet re-pointed would undo it. The style grammar's :contrast value resolves through it (spec/14 §3.2, §5.5).

7.4 Contrast

A theme MUST hold, by Visualize.Color.contrast/2 against its own background: text at least 4.5:1 (WCAG AA for text), axis at least 3:1 (WCAG non-text contrast), every series colour at least 3:1, and grid at least 1.2:1 — visible, and faint on purpose. surface MUST differ from grid: the grid is drawn over the surface (a map's graticule over its ocean), and a surface equal to it would swallow every line (#422). Both built-in themes do; a theme from new/1 is not checked, since a house palette is the consumer's choice. Across the colour schemes of Visualize.Scale.Color.schemes/0, every colour parses with Visualize.Color.parse/1 and every scheme has at least one colour at 3:1 or better against the dark background and at 2.5:1 or better against the light one (set2, a pastel scheme, tops out at 2.6:1 on white), so any scheme has a usable colour on either theme; a scheme's lightest colours vanish on the light theme (blues opens at 1.04:1 on white) and its darkest on the dark (blues closes at 1.8:1 on #1b1e24), which is why the built-in themes carry their own palettes (7.2). test/visualize/theme_contrast_test.exs holds both themes and every scheme to this.

7.5 Functions

FunctionContract
Visualize.Theme.default/0The light theme of 7.2.
Visualize.Theme.dark/0The dark theme of 7.2.
Visualize.Theme.new/1default/0 with the given keyword fields replaced (7.2).
Visualize.Theme.merge/2The theme with the given keyword fields replaced (7.2).
Visualize.Theme.slots/1The theme's slot names in the order of 7.1.
Visualize.Theme.colour_slots/1The theme's colour slots alone — the series, then axis, grid, text, background and surface — for a control that offers a colour as a slot (spec/14 §19.10).
Visualize.Theme.series_slot/2series_slot(theme, i): the :series_k atom the positive index i cycles to (7.1).
Visualize.Theme.resolve/3The slot's value as a literal or as a CSS reference with the literal fallback (7.3).
Visualize.Theme.css_name/1The custom-property name of a slot atom (7.3).
Visualize.Theme.mode/1:css for the SVG backend, :literal for any other (7.3).
Visualize.Theme.ink/2ink(theme, under): the literal of text or background, whichever contrasts more with under, else black or white when neither reaches 4.5:1 (7.3).
Visualize.Theme.stylesheet/1The .vis-theme-<name> rule declaring every slot (7.3).

8. Signals

Implemented (#244). A signal is a generated source: a description from which Visualize.Signals yields rows, pure and deterministic for a tick, so a chart under design has feeds that move without a host supplying any. A deployed design still binds to the host's pool (spec/14 §7.3) under the same names; signals are the builder's stand-in for it.

8.1 A signal

KeyTypeDefaultNotes
kind:sine | :square | :random | :discreterequiredthe wave every value column follows; :discrete is a counter — non-negative integers stepping up by one per tick, wrapping past max
fields[atom()]requiredthe columns: the first is time and carries the tick number; every other is a value column
windowpos_integer()24the rows a tick yields — the last window ticks, ending at the tick
periodnumber()12ticks per cycle of a :sine or a :square
min, maxnumber()0, 100the value range: a sine swings between them, a square alternates them, a random draws within them, a discrete counts from min to max
seedinteger()0for :random, so a chart replays the same feed

Value columns differ from each other: the n-th value column (0-based) of a sine or a square is shifted by n / count of a cycle, so two columns are never the same line; a random column draws its own stream from seed and the column's index; a discrete column starts n higher.

8.2 The rows

rows(signal, tick) yields window rows, one per tick from tick − window + 1 to tick in order — a tick below 0 yields no row, so the window fills over its first ticks — each a map of the signal's fields: the time column the tick, and each value column its kind's value at that tick:

  • :sine — min + (max − min) · (1 + sin(2π (t / period + phase))) / 2, so at tick 0 the first column stands at the midpoint;
  • :square — max for the first half of a cycle, min for the second, the column's phase applied;
  • :random — a value in [min, max) from :rand.uniform_real/0 under a seed derived from seed, the column's index and the tick, so the same tick always draws the same value;
  • :discrete — min + rem(t + n, max − min + 1).

The window slides: tick 25 over a window of 24 yields ticks 2..25, and the next tick drops 2 and adds 26. pool(signals, tick) yields a pool, name to rows, for a map of named signals, ready for Visualize.Chart.apply/2.

8.3 Functions

FunctionContract
Visualize.Signals.kinds/0The four kinds, in the order of 8.1.
Visualize.Signals.new/2new(kind, fields) — a signal of the kind over the fields with 8.1's defaults; new/3 takes the remaining keys as options.
Visualize.Signals.new/3As new/2 with options.
Visualize.Signals.rows/2rows(signal, tick): the window of rows ending at the tick (8.2).
Visualize.Signals.pool/2pool(signals, tick): rows/2 of every signal of a map, under its name.

9. FFT

9.1 The transform

Visualize.Data.FFT (#375) is the fast Fourier transform in pure Elixir: a radix-2 Cooley–Tukey decimation in time over lists, O(N log N), with no native code. fft/1 takes a sequence whose length is a power of two — numbers read as real, {re, im} pairs as complex — and returns the N complex terms X_k = Σ x_n e^{−2πi kn/N} as {re, im} pairs; any other length raises ArgumentError. magnitudes/1 is sqrt(re² + im²) of every term. Parseval holds: the sum of the squared magnitudes over N equals the sum of the squared samples, to floating precision.

9.2 Windows and the spectrum

window/2 tapers a sequence by :hann (0.5 − 0.5 cos(2πi/(N−1))), :hamming (0.54 − 0.46 cos(2πi/(N−1))) or :none — a sequence shorter than four is not tapered, the Hann of two points being all zeros; windows/0 lists them, :hann first. spectrum/3 is the reading a chart draws: spectrum(values, every, opts) takes the newest samples values — the option, a power of two, else the largest power of two not above the count — subtracts their mean when detrend: true (#376; an offset otherwise leaks from the DC line into its neighbours under a window), tapers them, transforms them and returns samples / 2 lines %{frequency, magnitude, phase}: frequency is k / (samples · every) in hertz for every seconds between samples; magnitude the one-sided amplitude |X_k| · 2 / (samples · gain) with the window's mean gain divided out, so a sine of amplitude A reads A at its frequency whatever the taper, the DC line not doubled; phase the argument of X_k. Fewer than two values, or fewer than samples, yield []. A 4096-point spectrum costs single-digit milliseconds, a 1024-point one about two. The test suite holds the cost's growth by count, not by clock (#462): four times the samples is under six times the work in reductions, as n log n gives and a quadratic transform's sixteen does not; the millisecond figures are a :benchmark test, stated rather than gated (12-testing-and-conformance §6).

9.3 Functions

FunctionContract
Visualize.Data.FFT.fft/1The N complex terms of a sequence whose length is a power of two (9.1).
Visualize.Data.FFT.magnitudes/1sqrt(re² + im²) of every term.
Visualize.Data.FFT.window/2window(values, kind): the sequence tapered by :hann, :hamming or :none (9.2).
Visualize.Data.FFT.windows/0The three window kinds, :hann first.
Visualize.Data.FFT.spectrum/2As spectrum/3 with no options.
Visualize.Data.FFT.spectrum/3spectrum(values, every, opts): the one-sided amplitude spectrum of the newest power of two of values sampled every every seconds (9.2).