The colors, 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
colors, :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.
Its colors are one color table (spec/08 §7.1, D-173): the theme's own colors — its
hues, such as :red, and names by use, the roles among them — over the default theme's
and the CSS named colors, with the furniture and the series as names too, so
Visualize.Color.color(:red) is the theme's red.
Nothing in the library carries a color 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 color 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 colors interpolated
in CIE Lab. {:ramp, name, t} is the color at t in 0..1, and the status roles are
by default fixed points on the status ramp: the table holds :good as
{:ramp, :status, 0}, :warning as 0.5 and :bad as 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.
Whole styles: the style palette
styles carries the same design over to whole styles (spec/08 §7.7, D-171): style series
:style_1 … :style_n, which {:style, i} cycles over; the five roles as whole looks;
the view states :hover, :focused, :selected, :active, :disabled, :muted,
:emphasis and :breakpoint; the furniture styles :axis, :grid, :label, :title,
:caption, :value and :annotation; and the modifiers :bold, :italic, :thin,
:thick, :dashed, :dotted and :outline, each changing one thing (D-172). Entries
are style nodes in their parts (fill, stroke, font, …) written in slots, the names
are the library's, and each entry is held to its group's contract when a theme is built.
style/2 reads one, the default theme's where the theme lacks it.
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 color, cycling.
A name of the style palette (spec/08 §7.7): :style_k, a role, a state or a furniture
name, or {:style, i} for the i-th style series entry, cycling.
Functions
The built-in theme of a name — :default, :dark, :color_safe or :print — or nil.
:ok, or {:error, subject, message} for the first ramp, color or palette style of the
theme that breaks its contract (spec/08 §7.1, §7.5, §7.7) — subject is {:ramp, name},
{:color, name} or {:style, name} and message names the term broken with the values
measured. Ramps are read first, then the color table's entries and the :info and
:neutral contracts, then the palette.
Whether name is of the theme's color table (spec/08 §7.1, §7.6): name in color_names(theme), answered without building the list — so a caller that tests many
names does not rebuild and sort the table per name (#704, D-184).
Every name of the theme's color table (spec/08 §7.1): its color slots, its own and the
default theme's colors, and the CSS named colors, sorted.
The color-vision-safe theme (spec/08 §7.2): default/0 with its status ramp blue →
orange → red, which survives the commonest color-vision deficiencies, in place of green
→ amber → red. Every role and ramp position a design names follows it unchanged.
The rule that switches a chart to the theme's counterpart in the viewer's color scheme
(spec/08 §7.3, #668): @media (prefers-color-scheme: dark) — light for a theme that
declares a light counterpart — around selector declaring every property of the built-in
counterpart as stylesheet/1 writes them. nil for a theme with no counterpart, or one
that is not a built-in theme.
The theme's color 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 or a color table name: --vis- and the name with
_ as - (spec/08 §7.3). A slot, a CSS named color or a name of the default theme's
table; any other atom raises ArgumentError.
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 grayscale. 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 theme's root defaults, every part defined (spec/08 §7.1, spec/14 §3.8, D-174): the
library's base — what an element with no value for a key already draws — with the
theme's own defaults laid over it by the merge rule.
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, colors and styles 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, colors and styles 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 color-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 grayscale, where the ramp's falling lightness and each role's glyph
still carry the order.
The color 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 color.
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 palette entry name as a style node in its parts (spec/08 §7.7, D-172): the theme's
own, or the default theme's entry of that name, whole, where the theme lacks it.
The theme's palette names (spec/08 §7.7): its style series :style_1 … :style_n — the
default theme's ten when it has none of its own — then the roles, the state styles and
the furniture styles.
The .vis-theme-<name> CSS rule declaring every slot of the theme, one line each.
Every ramp, color and palette style of the theme that breaks its contract (spec/08 §7.5,
§7.7), 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 color, cycling.
@type style_name() :: atom() | {:style, pos_integer()}
A name of the style palette (spec/08 §7.7): :style_k, a role, a state or a furniture
name, or {:style, i} for the i-th style series entry, cycling.
@type t() :: %Visualize.Theme{ axis: String.t(), background: String.t(), colors: %{ optional(atom() | String.t()) => String.t() | Visualize.Color.value() }, dark: atom() | String.t() | nil, defaults: map(), font_family: String.t(), font_size: number(), grid: String.t(), label_size: number(), light: atom() | String.t() | nil, name: atom() | String.t(), ramps: %{optional(ramp_name()) => [String.t()]}, series: [String.t(), ...], styles: %{optional(atom()) => map()}, surface: String.t(), text: String.t(), title_size: number() }
Functions
The built-in theme of a name — :default, :dark, :color_safe or :print — or nil.
iex> Visualize.Theme.builtin(:color_safe).name
:color_safe
iex> Visualize.Theme.builtin(:house)
nil
:ok, or {:error, subject, message} for the first ramp, color or palette style of the
theme that breaks its contract (spec/08 §7.1, §7.5, §7.7) — subject is {:ramp, name},
{:color, name} or {:style, name} and message names the term broken with the values
measured. Ramps are read first, then the color table's entries and the :info and
:neutral contracts, then the palette.
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
Whether name is of the theme's color table (spec/08 §7.1, §7.6): name in color_names(theme), answered without building the list — so a caller that tests many
names does not rebuild and sort the table per name (#704, D-184).
iex> theme = Visualize.Theme.default()
iex> Enum.map([:red, :bad, :axis, :series_3, :steelblue], &Visualize.Theme.color_name?(theme, &1))
[true, true, true, true, true]
iex> Enum.map([:series_99, :thick, "red"], &Visualize.Theme.color_name?(theme, &1))
[false, false, false]
Every name of the theme's color table (spec/08 §7.1): its color slots, its own and the
default theme's colors, and the CSS named colors, sorted.
iex> names = Visualize.Theme.color_names(Visualize.Theme.default())
iex> Enum.all?([:red, :amber, :bad, :axis, :series_3, :steelblue], &(&1 in names))
true
@spec color_safe() :: t()
The color-vision-safe theme (spec/08 §7.2): default/0 with its status ramp blue →
orange → red, which survives the commonest color-vision deficiencies, in place of green
→ amber → red. Every role and ramp position a design names follows it unchanged.
The rule that switches a chart to the theme's counterpart in the viewer's color scheme
(spec/08 §7.3, #668): @media (prefers-color-scheme: dark) — light for a theme that
declares a light counterpart — around selector declaring every property of the built-in
counterpart as stylesheet/1 writes them. nil for a theme with no counterpart, or one
that is not a built-in theme.
iex> css = Visualize.Theme.color_scheme(Visualize.Theme.default(), "#chart")
iex> String.starts_with?(css, "@media (prefers-color-scheme: dark) {") and css =~ "--vis-series-1: #7fb3e6;"
true
iex> Visualize.Theme.color_scheme(Visualize.Theme.color_safe(), "#chart")
nil
The theme's color 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 or a color table name: --vis- and the name with
_ as - (spec/08 §7.3). A slot, a CSS named color or a name of the default theme's
table; any other atom raises ArgumentError.
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 grayscale. 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).
The theme's root defaults, every part defined (spec/08 §7.1, spec/14 §3.8, D-174): the
library's base — what an element with no value for a key already draws — with the
theme's own defaults laid over it by the merge rule.
iex> Visualize.Theme.default() |> Visualize.Theme.defaults() |> Map.fetch!(:stroke)
%{opacity: 1, width: 1, join: :miter, cap: :butt, placement: :center}
iex> Visualize.Theme.new(defaults: %{stroke: %{width: 1.5}}) |> Visualize.Theme.defaults() |> get_in([:stroke, :width])
1.5
@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 color 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, colors and styles merged key by key
over the theme's own; the checks of new/1 apply.
iex> theme = Visualize.Theme.new(name: :house, styles: %{muted: %{opacity: 0.4}})
iex> Visualize.Theme.style(theme, :muted)
%{opacity: 0.4}
iex> Visualize.Theme.new(name: :house, colors: %{warning: "#f5c242"})
** (ArgumentError) theme "house": style :warning breaks its contract: fill {:color, :warning} is 1.65:1 against #ffffff; a role style's paint must reach 3:1 (WCAG 1.4.11, non-text contrast)
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, colors and styles are merged key by
key.
An unknown field raises KeyError; an empty series, or a ramp, role or palette style
that breaks its contract (spec/08 §7.5, §7.7), 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 color-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 grayscale, where the ramp's falling lightness and each role's glyph
still carry the order.
The color 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 color.
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.
@spec style(t(), style_name()) :: map()
The palette entry name as a style node in its parts (spec/08 §7.7, D-172): the theme's
own, or the default theme's entry of that name, whole, where the theme lacks it.
name is an atom of style_names/1, or {:style, i} for the i-th style series entry,
cycling over the theme's style series. In a role's entry :cue — a stroke's dash, a
marker's shape — becomes the role's dash and glyph (cue/1), and a solid dash leaves
the key out. Any other name raises ArgumentError.
iex> Visualize.Theme.style(Visualize.Theme.default(), :warning)
%{fill: %{type: :solid, color: {:color, :warning}}, stroke: %{type: :solid, color: {:color, :warning}, dash: "6 3"}, marker: %{shape: :triangle}}
iex> Visualize.Theme.style(Visualize.Theme.dark(), {:style, 12}) == Visualize.Theme.style(Visualize.Theme.dark(), :style_2)
true
iex> Visualize.Theme.style(Visualize.Theme.default(), :muted)
%{opacity: 0.25}
The theme's palette names (spec/08 §7.7): its style series :style_1 … :style_n — the
default theme's ten when it has none of its own — then the roles, the state styles and
the furniture styles.
iex> Visualize.Theme.default() |> Visualize.Theme.style_names() |> Enum.drop(10)
[:good, :warning, :bad, :info, :neutral, :hover, :focused, :selected, :active, :disabled, :muted, :emphasis, :breakpoint, :axis, :grid, :label, :title, :caption, :value, :annotation, :bold, :italic, :thin, :thick, :dashed, :dotted, :outline]
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, color and palette style of the theme that breaks its contract (spec/08 §7.5,
§7.7), 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.build(Visualize.Theme.default(), ramps: %{status: ~w(#c23238 #bd7407 #4ca34b)}, colors: %{neutral: "#ff0000"})
iex> theme |> Visualize.Theme.violations() |> Enum.map(&elem(&1, 0))
[{:ramp, :status}, {:color, :neutral}]
iex> Visualize.Theme.violations(Visualize.Theme.dark())
[]