The colours, font and sizes a chart draws with (spec/08 §7).
A theme is a struct of named slots: :series_1 … :series_n for the categorical
colours, :axis, :grid, :text, :background and :surface — the plane the data is laid
on, between the background and the grid — the five roles :good, :warning, :bad,
:info and :neutral, and :font_family, :font_size, :label_size and :title_size.
Nothing in the library carries a colour literal; every axis, mark and component reads its
values through resolve/3, in the mode its render target needs:
:literal- the stored value, which a canvas needs:css- a custom-property reference with the literal as its fallback,var(--vis-axis, #666666), which the SVG output carries so a stylesheet can override it from any ancestor (D-55)
stylesheet/1 writes a .vis-theme-<name> rule declaring every slot, so a consumer
switches every chart under an element of that class without touching Elixir.
Meaning: ramps and roles
A series colour promises distinct, never a meaning. A theme also carries four
ramps whose meaning the library fixes (spec/08 §7.5, D-135) — :status (good →
caution → bad), :thermal (cold → neutral → hot), :diverging (below → at → above a
midpoint) and :sequential (less → more) — each a list of anchor colours interpolated
in CIE Lab. {:ramp, name, t} is the colour at t in 0..1, and the status roles are
fixed points on the status ramp: :good is {:ramp, :status, 0}, :warning is 0.5
and :bad is 1. Every ramp and role is checked against its contract — hue order,
lightness direction, a neutral midpoint — when a theme is built by new/1 or merge/2,
and a theme that breaks one raises ArgumentError naming the ramp and the term broken.
Examples
iex> Visualize.Theme.resolve(Visualize.Theme.default(), :axis, :css)
"var(--vis-axis, #666666)"
iex> Visualize.Theme.resolve(Visualize.Theme.dark(), {:series, 11}, :literal)
"#7fb3e6"
iex> Visualize.Theme.new(font_family: "Inter, sans-serif").font_family
"Inter, sans-serif"
iex> Visualize.Theme.resolve(Visualize.Theme.default(), :warning, :literal)
"#bd7407"
iex> Visualize.Theme.resolve(Visualize.Theme.default(), {:ramp, :status, 0.5}, :css)
"var(--vis-warning, #bd7407)"
Summary
Types
How a slot renders: a CSS reference with fallback, or the stored value.
One of the four ramps whose meaning the library fixes (spec/08 §7.5).
A position along a ramp: t in 0..1, clamped.
A slot name, or {:series, i} for the i-th series colour, cycling.
Functions
The built-in theme of a name — :default, :dark, :colour_safe or :print — or nil.
:ok, or {:error, subject, message} for the first ramp or role of the theme that
breaks its meaning contract (spec/08 §7.5) — subject is {:ramp, name} or
{:role, name} and message names the term broken with the values measured.
The colour-vision-safe theme (spec/08 §7.2): default/0 with its status ramp blue →
orange → red, which survives the commonest colour-vision deficiencies, in place of green
→ amber → red. Every role and ramp position a design names follows it unchanged.
The theme's colour slots — the series, then axis, grid, text, background and surface, then the five roles (spec/08 §7.1).
The custom-property name of a slot atom: --vis- and the slot with _ as -.
A role's cue besides its hue (spec/08 §7.5, D-142): its glyph, a symbol type, and its
dash, a stroke pattern (nil for solid), so a status reads in greyscale. Fixed by the
library for every theme. Raises ArgumentError for anything but a role.
The dark theme: light text and axes on #1b1e24 (spec/08 §7.2).
The light theme: dark text and axes on white (spec/08 §7.2).
The contrast ink for a text drawn on under (spec/08 §7.3, D-123): the literal of the
theme's text or background, whichever has the higher WCAG contrast ratio against it —
text on a tie — or #000000/#ffffff, whichever contrasts more, when neither slot
reaches 4.5:1. One of black and white always does, so the ink always meets WCAG AA.
The theme with the given fields replaced, ramps and roles merged key by key over the
theme's own; the checks of new/1 apply.
The mode a backend needs: :css for Visualize.Backend.SVG, :literal for any other.
default/0 with the given fields replaced; ramps and roles are merged key by key.
The print theme (spec/08 §7.2, spec/15 §12.4): paper. A white background and surface, so
a printer lays no ink under the data; near-black text and axes and a grid dark enough to
survive a photocopy; the default's series; and the colour-safe theme's blue → orange →
red status ramp, since a printed page is read in whatever light and by whatever eyes
it reaches, often in greyscale, where the ramp's falling lightness and each role's glyph
still carry the order.
The colour at t along the theme's ramp name, as a literal: the anchors are spread
evenly over 0..1 and interpolated in CIE Lab, a position on an anchor is the anchor as
written, and t is clamped to 0..1 (spec/08 §7.5). A theme that lacks the ramp takes
the default theme's — never a series colour.
The four ramps whose meaning the library fixes (spec/08 §7.5).
The slot's value: the stored literal, or a CSS custom-property reference that falls back to it (spec/08 §7.3).
The five roles: the status ramp's three fixed points, then :info and :neutral.
The :series_k slot the positive index i cycles to over the theme's series.
The theme's slot names: :series_1 to :series_n, then the fixed slots.
The .vis-theme-<name> CSS rule declaring every slot of the theme, one line each.
Every ramp and role of the theme that breaks its meaning contract (spec/08 §7.5), as
{subject, message} in the order check/1 reads them — the contract run on each alone,
so a theme editor names every fault where it is and not only the first (spec/14 §20.4).
Types
@type mode() :: :css | :literal
How a slot renders: a CSS reference with fallback, or the stored value.
@type ramp_name() :: :status | :thermal | :diverging | :sequential
One of the four ramps whose meaning the library fixes (spec/08 §7.5).
A position along a ramp: t in 0..1, clamped.
@type slot() :: atom() | {:series, pos_integer()}
A slot name, or {:series, i} for the i-th series colour, cycling.
@type t() :: %Visualize.Theme{ axis: String.t(), background: String.t(), font_family: String.t(), font_size: number(), grid: String.t(), label_size: number(), name: atom(), ramps: %{optional(ramp_name()) => [String.t()]}, roles: %{optional(:info | :neutral) => String.t()}, series: [String.t(), ...], surface: String.t(), text: String.t(), title_size: number() }
Functions
The built-in theme of a name — :default, :dark, :colour_safe or :print — or nil.
iex> Visualize.Theme.builtin(:colour_safe).name
:colour_safe
iex> Visualize.Theme.builtin(:house)
nil
:ok, or {:error, subject, message} for the first ramp or role of the theme that
breaks its meaning contract (spec/08 §7.5) — subject is {:ramp, name} or
{:role, name} and message names the term broken with the values measured.
The check new/1 and merge/2 raise on, for a caller that reports instead.
iex> Visualize.Theme.check(Visualize.Theme.dark())
:ok
iex> theme = %{Visualize.Theme.default() | ramps: %{status: ~w(#c23238 #bd7407 #4ca34b)}}
iex> {:error, {:ramp, :status}, message} = Visualize.Theme.check(theme)
iex> message =~ "good (0) must be green or blue"
true
@spec colour_safe() :: t()
The colour-vision-safe theme (spec/08 §7.2): default/0 with its status ramp blue →
orange → red, which survives the commonest colour-vision deficiencies, in place of green
→ amber → red. Every role and ramp position a design names follows it unchanged.
The theme's colour slots — the series, then axis, grid, text, background and surface, then the five roles (spec/08 §7.1).
The custom-property name of a slot atom: --vis- and the slot with _ as -.
A role's cue besides its hue (spec/08 §7.5, D-142): its glyph, a symbol type, and its
dash, a stroke pattern (nil for solid), so a status reads in greyscale. Fixed by the
library for every theme. Raises ArgumentError for anything but a role.
iex> Visualize.Theme.cue(:warning)
%{glyph: :triangle, dash: "6 3"}
@spec dark() :: t()
The dark theme: light text and axes on #1b1e24 (spec/08 §7.2).
@spec default() :: t()
The light theme: dark text and axes on white (spec/08 §7.2).
@spec ink(t(), String.t() | Visualize.Color.t() | nil) :: String.t()
The contrast ink for a text drawn on under (spec/08 §7.3, D-123): the literal of the
theme's text or background, whichever has the higher WCAG contrast ratio against it —
text on a tie — or #000000/#ffffff, whichever contrasts more, when neither slot
reaches 4.5:1. One of black and white always does, so the ink always meets WCAG AA.
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. An unreadable string is
nil, and a slot whose own value does not parse is skipped.
iex> theme = Visualize.Theme.default()
iex> Visualize.Theme.ink(theme, "#3b6fa8")
"#ffffff"
iex> Visualize.Theme.ink(theme, "#ffd098")
"#333333"
iex> Visualize.Theme.ink(theme, nil)
"#333333"
iex> Visualize.Theme.ink(theme, "#777777")
"#000000"
The theme with the given fields replaced, ramps and roles merged key by key over the
theme's own; the checks of new/1 apply.
The mode a backend needs: :css for Visualize.Backend.SVG, :literal for any other.
Takes what Visualize.Backend.resolve/1 takes: a module, :svg, :canvas or nil.
default/0 with the given fields replaced; ramps and roles are merged key by key.
An unknown field raises KeyError; an empty series, or a ramp or role that breaks its
contract (spec/08 §7.5), raises ArgumentError.
@spec print() :: t()
The print theme (spec/08 §7.2, spec/15 §12.4): paper. A white background and surface, so
a printer lays no ink under the data; near-black text and axes and a grid dark enough to
survive a photocopy; the default's series; and the colour-safe theme's blue → orange →
red status ramp, since a printed page is read in whatever light and by whatever eyes
it reaches, often in greyscale, where the ramp's falling lightness and each role's glyph
still carry the order.
The colour at t along the theme's ramp name, as a literal: the anchors are spread
evenly over 0..1 and interpolated in CIE Lab, a position on an anchor is the anchor as
written, and t is clamped to 0..1 (spec/08 §7.5). A theme that lacks the ramp takes
the default theme's — never a series colour.
iex> theme = Visualize.Theme.default()
iex> Visualize.Theme.ramp(theme, :status, 1)
"#c23238"
iex> Visualize.Theme.ramp(theme, :thermal, 0.25)
"#8da5cb"
@spec ramp_names() :: [ramp_name(), ...]
The four ramps whose meaning the library fixes (spec/08 §7.5).
The slot's value: the stored literal, or a CSS custom-property reference that falls back to it (spec/08 §7.3).
Sizes are numbers under :literal and gain px under :css. A ramp position
{:ramp, name, t} is its literal in both modes — CSS has no property for each point of a
continuum — except where a status role stands, {:ramp, :status, 0 | 0.5 | 1}, which
resolves as :good, :warning or :bad, so a role and its position never disagree.
@spec role_names() :: [atom(), ...]
The five roles: the status ramp's three fixed points, then :info and :neutral.
@spec series_slot(t(), pos_integer()) :: atom()
The :series_k slot the positive index i cycles to over the theme's series.
The theme's slot names: :series_1 to :series_n, then the fixed slots.
The .vis-theme-<name> CSS rule declaring every slot of the theme, one line each.
Include it in a stylesheet and put class="vis-theme-<name>" on any ancestor of the
charts that should take the theme.
Every ramp and role of the theme that breaks its meaning contract (spec/08 §7.5), as
{subject, message} in the order check/1 reads them — the contract run on each alone,
so a theme editor names every fault where it is and not only the first (spec/14 §20.4).
iex> theme = %{Visualize.Theme.default() | ramps: %{status: ~w(#c23238 #bd7407 #4ca34b)}, roles: %{neutral: "#ff0000"}}
iex> theme |> Visualize.Theme.violations() |> Enum.map(&elem(&1, 0))
[{:ramp, :status}, {:role, :neutral}]
iex> Visualize.Theme.violations(Visualize.Theme.dark())
[]