The colors, fonts and sizes a page draws with (spec/04 §2, §3).
A theme is a struct of named slots: :series_1 … :series_n for the categorical
colors; the furniture :axis, :grid, :text, :background and :surface; 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 (§2.2):
the theme's own colors over the default theme's, its extensions' and the CSS named
colors, with the furniture and the series as names too, so Macaw.Layers.Color.color(:red)
is the theme's red. Everything reads a theme through resolve/3, in the mode its output
needs:
:literal- the stored value, which a canvas needs;:css- a custom-property reference with the literal as its fallback,var(--mw-axis, #666666), so a stylesheet can override it from any ancestor.
Meaning: ramps, roles and cues
A series color promises distinct, never a meaning. A theme also carries four ramps
whose meaning the library fixes (§2.3): :status, :thermal, :diverging and
:sequential, each a list of anchors interpolated in CIE Lab, and every role and ramp is
held to its contract when a theme is built by new/1 or merge/2.
Whole styles: the style palette
styles carries the design over to whole styles (§2.4): style series, the roles as whole
looks, view states, furniture styles and modifiers, the names the library's and its
registered extensions' (§3.4). style/2 reads one, the default theme's where the theme
lacks it.
Examples
iex> Macaw.Layers.Theme.resolve(Macaw.Layers.Theme.default(), :axis, :css)
"var(--mw-axis, #666666)"
iex> Macaw.Layers.Theme.resolve(Macaw.Layers.Theme.dark(), {:series, 11}, :literal)
"#7fb3e6"
iex> Macaw.Layers.Theme.resolve(Macaw.Layers.Theme.default(), :warning, :literal)
"#bd7407"
iex> Macaw.Layers.Theme.resolve(Macaw.Layers.Theme.default(), {:ramp, :status, 0.5}, :css)
"var(--mw-warning, #bd7407)"Ported from visualize's Visualize.Theme.
Summary
Types
How a slot renders: a CSS reference with its fallback, or the stored value.
One of the four ramps whose meaning the library fixes (§2.3).
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, or {:style, i} for the i-th style series entry.
Functions
The built-in theme of a name, :default, :dark, :color_safe or :print, by either
spelling, or nil.
:ok, or {:error, subject, message} for the first ramp, color or palette entry of the
theme that breaks its contract, subject being {:ramp, name}, {:color, name} or
{:style, name} and message naming the term broken with the values measured.
A color value held to a profile (§4): under :roles a value with a literal string among
its leaves is {:error, {:literal_color, value}}; under :open every color passes.
Whether name is of the theme's color table, answered without building the list.
Every name of the theme's color table, sorted: its color slots, its own and the default theme's colors, its extensions' and the CSS named colors.
The color-vision-safe theme: default/0 with its status ramp blue → orange → red, which
survives the commonest color-vision deficiencies, in place of green → amber → red.
The color slots: the series, then the furniture, then the five roles.
The custom-property name of a slot or a color name: --mw- and the name with _ as -.
A name is lower-case letters, digits and _, starting with a letter; any other raises
ArgumentError, so a stylesheet can never be broken out of through a name.
A role's cue besides its hue: its glyph and its dash (nil for solid), so a status reads
without its hue. Raises ArgumentError for anything but a role.
The dark theme: light text and axes on #1b1e24.
A data color under the contrast guard (§4): {:ok, literal} where it reaches 3:1 against
the theme's background, {:error, {:contrast, ratio}} where it does not, and {:error, :unreadable} for a value that does not resolve.
The light theme: dark text and axes on white.
The contrast floors a theme offered to people holds against its own background: text
at least 4.5:1, axis and every series color at least 3:1, grid at least 1.2:1, and a
surface different from the grid. :ok, or {:error, faults}, each {slot, {:contrast, ratio, floor}} or {:surface, {:equal, :grid}}.
The contrast ink for a text drawn on under: the literal of the theme's text or
background, whichever contrasts more (text on a tie), or #000000/#ffffff,
whichever contrasts more, when neither reaches 4.5:1. under is a color string, a
%Macaw.Layers.Color{}, or nil for the theme's background; an unreadable string is
nil, and a slot whose own value does not parse is skipped.
The theme with the given fields replaced, ramps, colors, styles and defaults merged
key by key over its own; the checks of new/1 apply.
default/0 with the given fields replaced; ramps, colors, styles and defaults
merged key by key. An unknown field raises KeyError; an empty series, or a ramp,
color or palette entry that breaks its contract, raises ArgumentError (§2.6).
The print theme: paper. A white background and surface, near-black text and axes, a grid
dark enough to survive a photocopy, the color-safe status ramp, and role styles whose
fills are hatches of the role's color, so a grayscale copy keeps each role apart.
The color at t along the theme's ramp name, as a literal: the anchors spread evenly
over 0..1 and interpolated in CIE Lab, a position on an anchor the anchor as written,
t clamped. A theme that lacks the ramp takes the default theme's, never a series color.
The four ramps whose meaning the library fixes.
The slot's value: the stored literal, or a CSS custom-property reference that falls back
to it. Sizes are numbers under :literal and gain px under :css. A ramp position is
its literal in both modes, except where a status role stands, {:ramp, :status, 0 | 0.5 | 1}, which resolves as the role while the table holds the role there. An unknown name, or
a series slot beyond the palette, raises ArgumentError.
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 one style node in its parts: the theme's own, the default
theme's entry whole where the theme lacks it, or an extension's default. {:style, i}
cycles over the style series; in a role's entry :cue becomes the role's dash and glyph,
a solid dash leaving the key out. Any other name raises ArgumentError.
The theme's palette names: its style series, the default's ten when it has none of its own, then the roles, the states, the furniture and the modifiers, then its extensions'.
Every ramp, color and palette entry of the theme that breaks its contract, as
{subject, message} in check/1's order, each contract run alone.
Types
@type mode() :: :css | :literal
How a slot renders: a CSS reference with its fallback, or the stored value.
@type ramp_name() :: :status | :thermal | :diverging | :sequential
One of the four ramps whose meaning the library fixes (§2.3).
A position along a ramp: t in 0..1, clamped.
@type slot() :: atom() | String.t() | {:series, pos_integer()}
A slot name, or {:series, i} for the i-th series color, cycling.
@type style_name() :: atom() | String.t() | {:style, pos_integer()}
A name of the style palette, or {:style, i} for the i-th style series entry.
@type t() :: %Macaw.Layers.Theme{ axis: String.t(), background: String.t(), colors: %{optional(atom() | String.t()) => term()}, dark: atom() | String.t() | nil, defaults: map(), extensions: [module()], 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()) => list()}, series: [String.t(), ...], styles: %{optional(atom()) => map() | list()}, surface: String.t(), text: String.t(), title_size: number() }
Functions
The built-in theme of a name, :default, :dark, :color_safe or :print, by either
spelling, or nil.
iex> Macaw.Layers.Theme.builtin("color_safe").name
:color_safe
iex> Macaw.Layers.Theme.builtin(:house)
nil
:ok, or {:error, subject, message} for the first ramp, color or palette entry of the
theme that breaks its contract, subject being {:ramp, name}, {:color, name} or
{:style, name} and message naming the term broken with the values measured.
iex> Macaw.Layers.Theme.check(Macaw.Layers.Theme.dark())
:ok
iex> theme = %{Macaw.Layers.Theme.default() | ramps: %{status: ~w(#c23238 #bd7407 #4ca34b)}}
iex> {:error, {:ramp, :status}, message} = Macaw.Layers.Theme.check(theme)
iex> message =~ "good (0) must be green or blue"
true
A color value held to a profile (§4): under :roles a value with a literal string among
its leaves is {:error, {:literal_color, value}}; under :open every color passes.
Whether name is of the theme's color table, answered without building the list.
iex> theme = Macaw.Layers.Theme.default()
iex> Enum.map([:red, :bad, :axis, :series_3, :steelblue, :series_99, :thick], &Macaw.Layers.Theme.color_name?(theme, &1))
[true, true, true, true, true, false, false]
Every name of the theme's color table, sorted: its color slots, its own and the default theme's colors, its extensions' and the CSS named colors.
iex> names = Macaw.Layers.Theme.color_names(Macaw.Layers.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: default/0 with its status ramp blue → orange → red, which
survives the commonest color-vision deficiencies, in place of green → amber → red.
The color slots: the series, then the furniture, then the five roles.
The custom-property name of a slot or a color name: --mw- and the name with _ as -.
A name is lower-case letters, digits and _, starting with a letter; any other raises
ArgumentError, so a stylesheet can never be broken out of through a name.
iex> Macaw.Layers.Theme.css_name(:series_1)
"--mw-series-1"
A role's cue besides its hue: its glyph and its dash (nil for solid), so a status reads
without its hue. Raises ArgumentError for anything but a role.
iex> Macaw.Layers.Theme.cue(:warning)
%{glyph: :triangle, dash: "6 3"}
@spec dark() :: t()
The dark theme: light text and axes on #1b1e24.
A data color under the contrast guard (§4): {:ok, literal} where it reaches 3:1 against
the theme's background, {:error, {:contrast, ratio}} where it does not, and {:error, :unreadable} for a value that does not resolve.
@spec default() :: t()
The light theme: dark text and axes on white.
The contrast floors a theme offered to people holds against its own background: text
at least 4.5:1, axis and every series color at least 3:1, grid at least 1.2:1, and a
surface different from the grid. :ok, or {:error, faults}, each {slot, {:contrast, ratio, floor}} or {:surface, {:equal, :grid}}.
@spec ink(t(), String.t() | Macaw.Layers.Color.t() | nil) :: String.t()
The contrast ink for a text drawn on under: the literal of the theme's text or
background, whichever contrasts more (text on a tie), or #000000/#ffffff,
whichever contrasts more, when neither reaches 4.5:1. under is a color string, a
%Macaw.Layers.Color{}, or nil for the theme's background; an unreadable string is
nil, and a slot whose own value does not parse is skipped.
iex> theme = Macaw.Layers.Theme.default()
iex> {Macaw.Layers.Theme.ink(theme, "#3b6fa8"), Macaw.Layers.Theme.ink(theme, nil), Macaw.Layers.Theme.ink(theme, "#777777")}
{"#ffffff", "#333333", "#000000"}
The theme with the given fields replaced, ramps, colors, styles and defaults merged
key by key over its own; the checks of new/1 apply.
iex> theme = Macaw.Layers.Theme.new(name: :house, styles: %{muted: %{opacity: 0.4}})
iex> Macaw.Layers.Theme.style(theme, :muted)
%{opacity: 0.4}
iex> Macaw.Layers.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)
default/0 with the given fields replaced; ramps, colors, styles and defaults
merged key by key. An unknown field raises KeyError; an empty series, or a ramp,
color or palette entry that breaks its contract, raises ArgumentError (§2.6).
@spec print() :: t()
The print theme: paper. A white background and surface, near-black text and axes, a grid
dark enough to survive a photocopy, the color-safe status ramp, and role styles whose
fills are hatches of the role's color, so a grayscale copy keeps each role apart.
The color at t along the theme's ramp name, as a literal: the anchors spread evenly
over 0..1 and interpolated in CIE Lab, a position on an anchor the anchor as written,
t clamped. A theme that lacks the ramp takes the default theme's, never a series color.
iex> Macaw.Layers.Theme.ramp(Macaw.Layers.Theme.default(), :thermal, 0.25)
"#8da5cb"
@spec ramp_names() :: [ramp_name(), ...]
The four ramps whose meaning the library fixes.
The slot's value: the stored literal, or a CSS custom-property reference that falls back
to it. Sizes are numbers under :literal and gain px under :css. A ramp position is
its literal in both modes, except where a status role stands, {:ramp, :status, 0 | 0.5 | 1}, which resolves as the role while the table holds the role there. An unknown name, or
a series slot beyond the palette, raises ArgumentError.
@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 one style node in its parts: the theme's own, the default
theme's entry whole where the theme lacks it, or an extension's default. {:style, i}
cycles over the style series; in a role's entry :cue becomes the role's dash and glyph,
a solid dash leaving the key out. Any other name raises ArgumentError.
iex> Macaw.Layers.Theme.style(Macaw.Layers.Theme.default(), :warning)
%{fill: %{type: :solid, color: {:color, :warning}}, stroke: %{type: :solid, color: {:color, :warning}, dash: "6 3"}, marker: %{shape: :triangle}}
iex> Macaw.Layers.Theme.style(Macaw.Layers.Theme.default(), :muted)
%{opacity: 0.25}
The theme's palette names: its style series, the default's ten when it has none of its own, then the roles, the states, the furniture and the modifiers, then its extensions'.
Every ramp, color and palette entry of the theme that breaks its contract, as
{subject, message} in check/1's order, each contract run alone.