A dashboard as data: designs laid out on a 12-column grid, sharing a time window, variables and a sync group, but never geometry (spec/15, D-141).
A composite flattens into one chart (spec/14 §19.6); a board never flattens. Each
cell is a placement {x, y, w, h} and a reference to a component in the store — a
composite id, whose entry holds a composite or a flat design — and is applied, rendered
and updated as its own chart. The board names no feed: its sources are the aggregate of
its cells' slots, namespaced per cell (required_sources/3), and a connector fills them
with a separate Visualize.Board.Binding.
The board is a value. from_map/2 and to_map/1 are exact inverses, and from_json/2
and to_json/1 the same round trip over JSON, stamped "$version" and read under the
version policy and sanity limits of spec/14 §9.1–9.2. validate/1 checks a board alone
and validate/3 given a store; realise/4 applies every cell, purely. The processes
that keep a board live are the runtime's (#540).
Examples
iex> {:ok, board} =
...> Visualize.Board.from_map(%{
...> cells: %{
...> latency: %{x: 0, y: 0, w: 8, h: 4, ref: {:composite, 1}},
...> errors: %{x: 8, y: 0, w: 4, h: 4, ref: {:composite, 1}, vars: %{unit: "%"}}
...> },
...> window: %{last: 3600},
...> vars: %{service: "api"}
...> })
iex> Visualize.Board.vars(board, :errors)
%{service: "api", unit: "%"}
iex> Visualize.Board.arrange(board, 400)
[latency: %{x: 0, y: 0, w: 12, h: 4}, errors: %{x: 0, y: 4, w: 12, h: 4}]
iex> Visualize.Board.from_map(%{cells: %{a: %{x: 6, y: 0, w: 8, h: 2, ref: {:style, 3}}}})
{:error, [{[:cells, :a], {:off_grid, 12}}, {[:cells, :a, :ref], {:ref_kind, :composite, :style}}]}
Summary
Types
A cell (spec/15 §1.3): a placement and a component reference.
A cell's drill target (spec/15 §13.3): the board a click on a datum opens — :self, or
the host's name for a board — and its variables, each a field of the datum or a value.
A fault: the path from the board to the value, and why (spec/14 §10.1).
A placement on the grid.
What resolution reads (spec/14 §19.6): a fetch function, or a map from id to fragment.
The time window (spec/15 §1.5).
Functions
Each cell's placement at a container width, in reading order — by y, then x, then
name (spec/15 §1.6). At the breakpoint or wider, every cell is where it was placed;
narrower, the cells stack in that order, each across all 12 columns and keeping its
height.
Each cell's box in pixels at a container width, {x, y, w, h}, after reflow (spec/15
§1.6): a column is (width − 11 × gap) / 12 wide and a row row_height high, a cell
spanning its columns and rows and the gaps between them, as a CSS grid with that gap
draws it.
The board's width in grid columns: 12 (spec/15 §1.4).
As components/3 with [] options.
Every cell's component prepared for application, purely (spec/15 §6): {:ok, %{cell => %{chart: chart, vars: vars}}}, or every fault of the board alone and given the store.
A cell's drill target with its variables taken from a datum (spec/15 §13.3):
{:ok, %{board: ref, vars: vars}}, :none for a cell with no drill, or {:error, errors} for a {:datum, field} the row has no value for.
One fault as a line a person reads, in the words of Visualize.Chart.Validator.format/1
and with the root named board.
The board of a JSON document (spec/15 §2.2), with limits: as from_map/2 takes them.
The board of a map, or every fault of it (spec/15 §2.1).
As from_map/2, raising ArgumentError with every fault formatted.
The effective layout: the board's layout over breakpoint: 768, row_height: 80 and
gap: 8, metresis's grid.
The name a field links to through the board's links (spec/15 §13.2), or the field
itself when links does not name it. A link is one step.
Every field that links to the same name as field, itself and that name included, in
name order (spec/15 §13.2): the fields a cell reads a selection at.
As realise/4 with [] options.
Every cell applied, purely (spec/15 §6): {:ok, %{cell => applied}, cache}, or every
fault, each under its cell.
As required_sources/3 with [] options.
The board's sources: every cell's component's slots, keyed {cell, slot}, each with
its fields, its types, its requires contract — the slot's own with every
attribute a fact of it reads, as Visualize.Chart.contract/1 gives it (spec/15 §4.1,
D-136) — and its reads: each field the component reads from it through a scale, as
{path, field, need}, need the type the scale's kind needs (spec/14 §7.4, D-153). One design used in two cells is two sets of slots, bound independently.
The board as a JSON document stamped "$version" (spec/15 §2.2), or
{:error, {:missing_dependency, :jason}}. from_json(to_json(board)) is {:ok, board}.
The board as a map: every key of spec/15 §1.2, version included.
:ok, or every fault of a board — or a board map — alone (spec/15 §3.1): its keys and
types, cells off the grid or overlapping, the window, the zone, and component references
of the wrong kind.
As validate/3 with [] options.
:ok, or every fault of the board alone and then given the store (spec/15 §3.2).
One cell's variables before they are narrowed to its component's (spec/15 §5): the
board's, with the cell's over them. A component's declared default is the third layer,
applied by Visualize.Chart.apply/2.
The window resolved at now (spec/15 §1.5): {from, to} as UTC DateTimes, or nil
for a board with no window. Now is the caller's — the board's — never a feed's.
A resolved window as a person reads it, in the board's zone (spec/15 §1.5, §9.1): each
end shifted from UTC into zone by the host's time-zone database and written
YYYY-MM-DD HH:MM:SS, then the zone's name; nil for no window. A zone the database
does not know leaves the ends in UTC. The board component's header and an export's
(§12.1) both print it.
Types
@type cell() :: %{ :x => non_neg_integer(), :y => non_neg_integer(), :w => pos_integer(), :h => pos_integer(), :ref => {:composite, pos_integer()}, optional(:title) => String.t() | nil, optional(:vars) => %{required(atom()) => term()}, optional(:sync) => boolean(), optional(:drill) => drill() | nil }
A cell (spec/15 §1.3): a placement and a component reference.
@type drill() :: %{ :board => atom() | String.t() | pos_integer(), optional(:vars) => %{required(atom()) => {:datum, atom()} | term()} }
A cell's drill target (spec/15 §13.3): the board a click on a datum opens — :self, or
the host's name for a board — and its variables, each a field of the datum or a value.
A fault: the path from the board to the value, and why (spec/14 §10.1).
@type rect() :: %{ x: non_neg_integer(), y: non_neg_integer(), w: pos_integer(), h: pos_integer() }
A placement on the grid.
What resolution reads (spec/14 §19.6): a fetch function, or a map from id to fragment.
@type window() :: nil | %{last: number()} | %{from: DateTime.t(), to: DateTime.t()}
The time window (spec/15 §1.5).
Functions
Each cell's placement at a container width, in reading order — by y, then x, then
name (spec/15 §1.6). At the breakpoint or wider, every cell is where it was placed;
narrower, the cells stack in that order, each across all 12 columns and keeping its
height.
Each cell's box in pixels at a container width, {x, y, w, h}, after reflow (spec/15
§1.6): a column is (width − 11 × gap) / 12 wide and a row row_height high, a cell
spanning its columns and rows and the gaps between them, as a CSS grid with that gap
draws it.
iex> board = Visualize.Board.from_map!(%{cells: %{a: %{x: 6, y: 1, w: 6, h: 2, ref: {:composite, 1}}}})
iex> Visualize.Board.boxes(board, 1288)
%{a: {648.0, 88, 640.0, 168}}
@spec columns() :: pos_integer()
The board's width in grid columns: 12 (spec/15 §1.4).
@spec components(t(), store()) :: {:ok, %{required(atom()) => %{chart: Visualize.Chart.t(), vars: map()}}} | {:error, [error(), ...]}
As components/3 with [] options.
@spec components(t(), store(), keyword()) :: {:ok, %{required(atom()) => %{chart: Visualize.Chart.t(), vars: map()}}} | {:error, [error(), ...]}
Every cell's component prepared for application, purely (spec/15 §6): {:ok, %{cell => %{chart: chart, vars: vars}}}, or every fault of the board alone and given the store.
Each component is resolved through the store and read with limits:; with the board's
sync set and the cell's true, the board's group is written into its
interaction.sync; the board's zone is written into every :time scale that names no
zone of its own (spec/15 §1.5); and vars is the cascade of spec/15 §5 narrowed to the
variables the component declares. realise/4 applies each; the runtime applies each in
its cell's process (spec/15 §8).
@spec drill(t(), atom(), map()) :: {:ok, %{board: term(), vars: map()}} | :none | {:error, [error(), ...]}
A cell's drill target with its variables taken from a datum (spec/15 §13.3):
{:ok, %{board: ref, vars: vars}}, :none for a cell with no drill, or {:error, errors} for a {:datum, field} the row has no value for.
iex> board = Visualize.Board.from_map!(%{vars: %{service: "api"}, cells: %{a: %{x: 0, y: 0,
...> w: 4, h: 2, ref: {:composite, 1}, drill: %{board: :self, vars: %{service: {:datum, :name}}}}}})
iex> Visualize.Board.drill(board, :a, %{name: "db", v: 3})
{:ok, %{board: :self, vars: %{service: "db"}}}
iex> Visualize.Board.drill(board, :a, %{v: 3})
{:error, [{[:cells, :a, :drill, :vars, :service], {:missing, :datum, :name}}]}
One fault as a line a person reads, in the words of Visualize.Chart.Validator.format/1
and with the root named board.
iex> Visualize.Board.format_error({[:cells, :b], {:overlap, :a}})
"cells.b: overlaps cell :a"
iex> Visualize.Board.format_error({[], {:type, :board}})
"board: must be a board map"
@spec from_json(String.t(), keyword()) :: {:ok, t()} | {:error, [error(), ...] | {:missing_dependency, :jason}}
The board of a JSON document (spec/15 §2.2), with limits: as from_map/2 takes them.
The document's "$version" is checked, and its nesting and the size of its lists and
maps held to the limits, before a key is decoded, so a newer or an oversized board
makes no atom and a path in such an error holds the document's own key strings. A key
the board does not know stays a string and is :unknown_key at its own spelling. A
document that does not parse, or is not an object, is {:error, [{[], {:json, message}}]};
without Jason the result is {:error, {:missing_dependency, :jason}}.
iex> Visualize.Board.from_json(~s({"$version": 9, "cells": {}}))
{:error, [{["$version"], {:newer_schema, 9, 2}}]}
The board of a map, or every fault of it (spec/15 §2.1).
A version newer than this library reads is refused first, before anything else is read
(spec/14 §9.1); then the map is held to the depth and items limits — limits: as
Visualize.Chart.from_map/2 takes them, the defaults when absent — and validated alone
(spec/15 §3.1). A map with no version reads as the current one.
iex> Visualize.Board.from_map(%{version: 3})
{:error, [{[:version], {:newer_schema, 3, 2}}]}
As from_map/2, raising ArgumentError with every fault formatted.
@spec layout(t()) :: %{ breakpoint: pos_integer(), row_height: pos_integer(), gap: non_neg_integer() }
The effective layout: the board's layout over breakpoint: 768, row_height: 80 and
gap: 8, metresis's grid.
The name a field links to through the board's links (spec/15 §13.2), or the field
itself when links does not name it. A link is one step.
iex> board = Visualize.Board.from_map!(%{links: %{name: :service}})
iex> {Visualize.Board.link(board, :name), Visualize.Board.link(board, :host)}
{:service, :host}
Every field that links to the same name as field, itself and that name included, in
name order (spec/15 §13.2): the fields a cell reads a selection at.
iex> board = Visualize.Board.from_map!(%{links: %{name: :service, svc: :service}})
iex> Visualize.Board.linked(board, :name)
[:name, :service, :svc]
@spec realise(t(), store(), %{required(atom()) => map()}) :: {:ok, %{required(atom()) => Visualize.Chart.Applied.t()}, Visualize.Data.Schema.Cache.t()} | {:error, [error(), ...]}
As realise/4 with [] options.
@spec realise(t(), store(), %{required(atom()) => map()}, keyword()) :: {:ok, %{required(atom()) => Visualize.Chart.Applied.t()}, Visualize.Data.Schema.Cache.t()} | {:error, [error(), ...]}
Every cell applied, purely (spec/15 §6): {:ok, %{cell => applied}, cache}, or every
fault, each under its cell.
pools maps a cell's name to its pool, as Visualize.Board.Binding.pools/2 makes it.
For each cell its component is resolved through the store; its variables are the
board's with the cell's over them, narrowed to those it declares; with the board's
sync set and the cell's true, the board's group is written into its
interaction.sync; and it is applied with Visualize.Chart.apply/2 at its box's size.
The board's own faults, alone and given the store, are reported before any cell is
applied.
Options
:width- the board's container width in pixels (default1200); each cell is applied at its box's size there (boxes/2):theme,:now,:system- asVisualize.Chart.apply/2takes them:schemas- the schema cache (default empty), threaded from cell to cell and returned grown:describe-({cell, slot} -> {:ok, schema} | {:error, reason}): the connector maps the cell's slot to its feed through its binding:limits- the limits each component's design is read under
@spec required_sources(t(), store()) :: {:ok, %{required({atom(), atom()}) => Visualize.Board.Binding.need()}} | {:error, [error(), ...]}
As required_sources/3 with [] options.
@spec required_sources(t(), store(), keyword()) :: {:ok, %{required({atom(), atom()}) => Visualize.Board.Binding.need()}} | {:error, [error(), ...]}
The board's sources: every cell's component's slots, keyed {cell, slot}, each with
its fields, its types, its requires contract — the slot's own with every
attribute a fact of it reads, as Visualize.Chart.contract/1 gives it (spec/15 §4.1,
D-136) — and its reads: each field the component reads from it through a scale, as
{path, field, need}, need the type the scale's kind needs (spec/14 §7.4, D-153). One design used in two cells is two sets of slots, bound independently.
Components are resolved through the store as validate/3 resolves them, with
limits:; the faults that keep one from being read are {:error, errors}.
The board as a JSON document stamped "$version" (spec/15 §2.2), or
{:error, {:missing_dependency, :jason}}. from_json(to_json(board)) is {:ok, board}.
The board as a map: every key of spec/15 §1.2, version included.
:ok, or every fault of a board — or a board map — alone (spec/15 §3.1): its keys and
types, cells off the grid or overlapping, the window, the zone, and component references
of the wrong kind.
iex> Visualize.Board.validate(%{cells: %{
...> a: %{x: 0, y: 0, w: 6, h: 2, ref: {:composite, 1}},
...> b: %{x: 4, y: 1, w: 6, h: 2, ref: {:composite, 2}}
...> }})
{:error, [{[:cells, :b], {:overlap, :a}}]}
As validate/3 with [] options.
:ok, or every fault of the board alone and then given the store (spec/15 §3.2).
Each cell's component is fetched, flattened through the store and read through
Visualize.Chart.from_map/2 with limits:. An id the store does not hold is
{:dangling, id} and a component that reaches itself {:cycle, chain}, at the cell's
ref; a component that is not a design has its faults under the ref; a cell variable
its component does not declare is {:undeclared, :var, name}; and a cell that cannot
join the board's sync group has the component's {:sync, …} fault at its sync.
A cell's drill target (spec/15 §13.3) is checked against boards: — a map from a ref
to a board, or a fetch (ref -> {:ok, board} | :error): a ref it does not hold is
{:no_board, ref}, a variable the target does not declare {:undeclared, :var, name},
and a {:datum, field} whose field the component's sources do not declare
{:undeclared, :field, field}. A :self target is checked against this board with or
without boards:.
One cell's variables before they are narrowed to its component's (spec/15 §5): the
board's, with the cell's over them. A component's declared default is the third layer,
applied by Visualize.Chart.apply/2.
@spec window(t(), DateTime.t()) :: {DateTime.t(), DateTime.t()} | nil
The window resolved at now (spec/15 §1.5): {from, to} as UTC DateTimes, or nil
for a board with no window. Now is the caller's — the board's — never a feed's.
iex> board = Visualize.Board.from_map!(%{window: %{last: 3600}})
iex> Visualize.Board.window(board, ~U[2026-10-11 12:00:00Z])
{~U[2026-10-11 11:00:00Z], ~U[2026-10-11 12:00:00Z]}
@spec window_text({DateTime.t(), DateTime.t()} | nil, String.t()) :: String.t() | nil
A resolved window as a person reads it, in the board's zone (spec/15 §1.5, §9.1): each
end shifted from UTC into zone by the host's time-zone database and written
YYYY-MM-DD HH:MM:SS, then the zone's name; nil for no window. A zone the database
does not know leaves the ends in UTC. The board component's header and an export's
(§12.1) both print it.
iex> Visualize.Board.window_text({~U[2026-10-11 11:00:00Z], ~U[2026-10-11 12:00:00Z]}, "Etc/UTC")
"2026-10-11 11:00:00 – 2026-10-11 12:00:00 (Etc/UTC)"
iex> Visualize.Board.window_text(nil, "Etc/UTC")
nil