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: a color a literal%Macaw.Layers.Color{};:css- a custom-property reference with the literal as its fallback,var(--mw-axis, #666666), so a stylesheet can override it from any ancestor.
Its colors are terms (§2.1): a literal is %Macaw.Layers.Color{}, read from text at the
edge (Macaw.Layers.Color.parse!/1, ~RGB"#3b6fa8"), and a string is never a color.
Its names are strings (§2.1), the library's, its extensions' and a host's alike: its
own name and its dark and light, the slots, the keys of ramps, colors and styles, and the name in every typed color
({:color, "good"}, {:ramp, "status", 0.5}). A name given as an atom is read as its
string, once, when the theme is built (new/1, merge/2, a document read), so :info
and "info" are one name; every function that takes a name takes either spelling. Its
words — a style's keys and atom values, defaults — are kept as given.
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)
%Macaw.Layers.Color{r: 127.0, g: 179.0, b: 230.0, opacity: 1.0}
iex> Macaw.Layers.Theme.resolve(Macaw.Layers.Theme.default(), :warning, :css)
"var(--mw-warning, #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): "status", "thermal",
"diverging" or "sequential", an atom given read as its string.
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", an atom
read as its string, 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 color among
its leaves is {:error, {:literal_color, value}}; under :open every color passes. A term
that is no color value, text included, raises ArgumentError.
Whether name is of the theme's color table, answered without building the list. A
name is its string, an atom read as its string.
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. The role by its name, an atom read as its string. 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}, its literal color, where it
reaches 3:1 against the theme's background, {:error, {:contrast, ratio}} where it does
not, and {:error, :unreadable} for a term that is no color value or does not resolve,
text included.
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}}, the slot by its name, or {"surface", {:equal, "grid"}}.
The contrast ink for a text drawn on under: the literal color of the theme's text or
background, whichever contrasts more (text on a tie), or black or white, whichever
contrasts more, when neither reaches 4.5:1. under is a literal %Macaw.Layers.Color{},
or nil for the theme's background; anything else, text included, raises
ArgumentError. A slot that holds no literal color, in a theme built by hand, 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, a series or
furniture color that is not a literal color, or a ramp, color or palette entry that breaks
its contract, raises ArgumentError (§2.6). A string is never a color: read text with
Macaw.Layers.Color.parse!/1, or write it in the source as ~RGB"#555555".
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 color: the anchors spread
evenly over 0..1 and interpolated in CIE Lab, a position on an anchor the anchor's
literal, 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, by name.
The slot's value: the stored literal, or a CSS custom-property reference that falls back
to its CSS text. A color is a literal %Macaw.Layers.Color{} under :literal; sizes are
numbers under :literal and gain px under :css. A ramp position is its literal in
both modes, as CSS text under :css, except where a status role stands, {:ramp, "status", 0 | 0.5 | 1}, which resolves as the role while the table holds the role there.
A name, of a slot, a color or a ramp, is its string, an atom read as its string. 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.
One of the four ramps whose meaning the library fixes (§2.3): "status", "thermal",
"diverging" or "sequential", an atom given read as its string.
A position along a ramp: t in 0..1, clamped.
@type slot() :: String.t() | atom() | {:series, pos_integer()}
A slot name, or {:series, i} for the i-th series color, cycling.
@type style_name() :: String.t() | atom() | {: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: Macaw.Layers.Color.t(), background: Macaw.Layers.Color.t(), colors: %{optional(String.t()) => term()}, dark: String.t() | nil, defaults: map(), extensions: [module()], font_family: String.t(), font_size: number(), grid: Macaw.Layers.Color.t(), label_size: number(), light: String.t() | nil, name: String.t(), ramps: %{optional(String.t()) => list()}, series: [Macaw.Layers.Color.t(), ...], styles: %{optional(String.t()) => map() | list() | String.t()}, surface: Macaw.Layers.Color.t(), text: Macaw.Layers.Color.t(), title_size: number() }
Functions
The built-in theme of a name, "default", "dark", "color_safe" or "print", an atom
read as its string, 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> status = Enum.map(~w(#c23238 #bd7407 #4ca34b), &Macaw.Layers.Color.parse!/1)
iex> theme = %{Macaw.Layers.Theme.default() | ramps: %{"status" => status}}
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 color among
its leaves is {:error, {:literal_color, value}}; under :open every color passes. A term
that is no color value, text included, raises ArgumentError.
Whether name is of the theme's color table, answered without building the list. A
name is its string, an atom read as its string.
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?(~w(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. The role by its name, an atom read as its string. 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.
@spec data_color(t(), term()) :: {:ok, Macaw.Layers.Color.t()} | {:error, {:contrast, float()} | :unreadable}
A data color under the contrast guard (§4): {:ok, literal}, its literal color, where it
reaches 3:1 against the theme's background, {:error, {:contrast, ratio}} where it does
not, and {:error, :unreadable} for a term that is no color value or does not resolve,
text included.
@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}}, the slot by its name, or {"surface", {:equal, "grid"}}.
@spec ink(t(), Macaw.Layers.Color.t() | nil) :: Macaw.Layers.Color.t()
The contrast ink for a text drawn on under: the literal color of the theme's text or
background, whichever contrasts more (text on a tie), or black or white, whichever
contrasts more, when neither reaches 4.5:1. under is a literal %Macaw.Layers.Color{},
or nil for the theme's background; anything else, text included, raises
ArgumentError. A slot that holds no literal color, in a theme built by hand, is skipped.
iex> import Macaw.Layers.Color, only: [sigil_RGB: 2]
iex> theme = Macaw.Layers.Theme.default()
iex> [Macaw.Layers.Theme.ink(theme, ~RGB"#3b6fa8"), Macaw.Layers.Theme.ink(theme, nil), Macaw.Layers.Theme.ink(theme, ~RGB"#777777")]
...> |> Enum.map(&Macaw.Layers.Color.to_css/1)
["#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: Macaw.Layers.Color.parse!("#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)Names are strings (§2.1): a name given as an atom is its string, so a host's "info"
and the library's :info are one name, the one given winning; a name given both ways in
one map raises.
iex> Macaw.Layers.Theme.new(colors: %{"info" => Macaw.Layers.Color.parse!("#2f5d8a")})
...> |> Macaw.Layers.Theme.resolve(:info, :css)
"var(--mw-info, #2f5d8a)"
default/0 with the given fields replaced; ramps, colors, styles and defaults
merged key by key. An unknown field raises KeyError; an empty series, a series or
furniture color that is not a literal color, or a ramp, color or palette entry that breaks
its contract, raises ArgumentError (§2.6). A string is never a color: read text with
Macaw.Layers.Color.parse!/1, or write it in the source as ~RGB"#555555".
iex> Macaw.Layers.Theme.new(axis: Macaw.Layers.Color.parse!("#555555")).axis
%Macaw.Layers.Color{r: 85.0, g: 85.0, b: 85.0, opacity: 1.0}
iex> Macaw.Layers.Theme.new(axis: "#555")
** (ArgumentError) theme "default": axis "#555" is text, and a string is never a color: read it with Macaw.Layers.Color.parse!/1, or write ~RGB"#555"
@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.
@spec ramp(t(), ramp_name(), number()) :: Macaw.Layers.Color.t()
The color at t along the theme's ramp name, as a literal color: the anchors spread
evenly over 0..1 and interpolated in CIE Lab, a position on an anchor the anchor's
literal, 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)
%Macaw.Layers.Color{r: 141.0, g: 165.0, b: 203.0, opacity: 1.0}
@spec ramp_names() :: [String.t(), ...]
The four ramps whose meaning the library fixes, by name.
iex> Macaw.Layers.Theme.ramp_names()
["status", "thermal", "diverging", "sequential"]
@spec resolve(t(), slot() | ramp_position(), mode()) :: Macaw.Layers.Color.t() | String.t() | number()
The slot's value: the stored literal, or a CSS custom-property reference that falls back
to its CSS text. A color is a literal %Macaw.Layers.Color{} under :literal; sizes are
numbers under :literal and gain px under :css. A ramp position is its literal in
both modes, as CSS text under :css, except where a status role stands, {:ramp, "status", 0 | 0.5 | 1}, which resolves as the role while the table holds the role there.
A name, of a slot, a color or a ramp, is its string, an atom read as its string. An
unknown name, or a series slot beyond the palette, raises ArgumentError.
@spec role_names() :: [String.t(), ...]
The five roles: the status ramp's three fixed points, then "info" and "neutral".
@spec series_slot(t(), pos_integer()) :: String.t()
The "series_k" slot the positive index i cycles to over the theme's series.
iex> Macaw.Layers.Theme.series_slot(Macaw.Layers.Theme.default(), 11)
"series_1"
The theme's slot names: "series_1" to "series_n", then the fixed slots.
iex> Macaw.Layers.Theme.new(series: [Macaw.Layers.Color.parse!("#3b6fa8"), Macaw.Layers.Color.parse!("#d9730d")])
...> |> Macaw.Layers.Theme.slots()
...> |> Enum.take(4)
["series_1", "series_2", "axis", "grid"]
@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.