Visualize.Board (Visualize v0.8.3)

Copy Markdown View Source

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; realize/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).

What a selection names (spec/15 §13.2): {field, value}, or a map from field to value for an identity the schema declares over several fields (spec/08 §10.1, D-167).

A placement on the grid.

What resolution reads (spec/14 §19.6): a fetch function, or a map from id to fragment.

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.

The board's width in grid columns: 12 (spec/15 §1.4).

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.

Every scope's own variables by scope (spec/15 §5, §14.2, D-229): the board's under [] and each cell's under [cell], every cell present — what the board shows its virtual sources' engine (Visualize.VirtualSource.Engine.vars/3), which holds the cascade.

Every cell applied, purely (spec/15 §6): {:ok, %{cell => applied}, cache}, or every fault, each under its cell.

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.

Every virtual source of the board and its cells by path (spec/15 §1.2, spec/08 §10.5.9): [id] for the board's own, [cell, id] for a cell's, each a %Visualize.VirtualSource{}.

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

cell()

@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(String.t()) => term()},
  optional(:sync) => boolean(),
  optional(:drill) => drill() | nil,
  optional(:virtual_sources) => %{required(String.t()) => map()}
}

A cell (spec/15 §1.3): a placement and a component reference.

drill()

@type drill() :: %{
  :board => atom() | String.t() | pos_integer(),
  optional(:vars) => %{required(String.t()) => {:datum, String.t()} | 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.

error()

@type error() :: {list(), term()}

A fault: the path from the board to the value, and why (spec/14 §10.1).

identity()

@type identity() ::
  {atom() | String.t(), term()} | %{required(atom() | String.t()) => term()}

What a selection names (spec/15 §13.2): {field, value}, or a map from field to value for an identity the schema declares over several fields (spec/08 §10.1, D-167).

rect()

@type rect() :: %{
  x: non_neg_integer(),
  y: non_neg_integer(),
  w: pos_integer(),
  h: pos_integer()
}

A placement on the grid.

store()

@type store() :: (term() -> {:ok, map()} | :error) | %{required(term()) => map()}

What resolution reads (spec/14 §19.6): a fetch function, or a map from id to fragment.

t()

@type t() :: %Visualize.Board{
  cells: %{required(String.t()) => cell()},
  layout: map(),
  links: %{required(String.t()) => String.t()},
  meta: map(),
  stale_after: number() | nil,
  sync: String.t() | nil,
  vars: %{required(String.t()) => term()},
  version: pos_integer(),
  virtual_sources: %{required(String.t()) => map()},
  window: window(),
  zone: String.t()
}

window()

@type window() :: nil | %{last: number()} | %{from: DateTime.t(), to: DateTime.t()}

The time window (spec/15 §1.5).

Functions

arrange(board, width)

@spec arrange(t(), number()) :: [{String.t(), rect()}]

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.

boxes(board, width)

@spec boxes(t(), number()) :: %{
  required(String.t()) => {number(), number(), number(), number()}
}

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}}

columns()

@spec columns() :: pos_integer()

The board's width in grid columns: 12 (spec/15 §1.4).

components(board, store)

@spec components(t(), store()) ::
  {:ok, %{required(String.t()) => %{chart: Visualize.Chart.t(), vars: map()}}}
  | {:error, [error(), ...]}

As components/3 with [] options.

components(board, store, opts)

@spec components(t(), store(), keyword()) ::
  {:ok, %{required(String.t()) => %{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: and generators:; 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. realize/4 applies each; the runtime applies each in its cell's process (spec/15 §8).

drill(board, cell, row)

@spec drill(t(), atom() | String.t(), 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"}}]}

format_error(error)

@spec format_error(error()) :: String.t()

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"

from_json(json, opts \\ [])

@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, 3}}]}

from_map(map, opts \\ [])

@spec from_map(term(), keyword()) :: {:ok, t()} | {:error, [error(), ...]}

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: 4})
{:error, [{[:version], {:newer_schema, 4, 3}}]}

from_map!(map, opts \\ [])

@spec from_map!(term(), keyword()) :: t()

As from_map/2, raising ArgumentError with every fault formatted.

layout(board)

@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.

link(board, field)

@spec link(t(), atom() | String.t()) :: String.t()

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"}

linked(board, field)

@spec linked(t(), atom() | String.t()) :: [String.t()]

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"]

own_vars(board)

@spec own_vars(t()) :: %{required([String.t()]) => %{required(String.t()) => term()}}

Every scope's own variables by scope (spec/15 §5, §14.2, D-229): the board's under [] and each cell's under [cell], every cell present — what the board shows its virtual sources' engine (Visualize.VirtualSource.Engine.vars/3), which holds the cascade.

iex> board = Visualize.Board.from_map!(%{
...>   vars: %{service: "api"},
...>   cells: %{a: %{x: 0, y: 0, w: 6, h: 3, ref: {:composite, 1}, vars: %{service: "web"}}}
...> })
iex> Visualize.Board.own_vars(board)
%{[] => %{"service" => "api"}, ["a"] => %{"service" => "web"}}

realize(board, store, pools)

@spec realize(t(), store(), %{required(atom() | String.t()) => map()}) ::
  {:ok, %{required(String.t()) => Visualize.Chart.Applied.t()},
   Visualize.Data.Schema.Cache.t()}
  | {:error, [error(), ...]}

As realize/4 with [] options.

realize(board, store, pools, opts)

@spec realize(t(), store(), %{required(atom() | String.t()) => map()}, keyword()) ::
  {:ok, %{required(String.t()) => 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 (default 1200); each cell is applied at its box's size there (boxes/2)
  • :theme, :now, :system - as Visualize.Chart.apply/2 takes them
  • :generators, :assigns - the generator modules each component is read with and its frames call, and the host's assigns their context carries, as Visualize.Chart.apply/2 takes them (spec/14 §3.13)
  • :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

required_sources(board, store)

@spec required_sources(t(), store()) ::
  {:ok, %{required({String.t(), String.t()}) => Visualize.Board.Binding.need()}}
  | {:error, [error(), ...]}

As required_sources/3 with [] options.

required_sources(board, store, opts)

@spec required_sources(t(), store(), keyword()) ::
  {:ok, %{required({String.t(), String.t()}) => 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: and generators:; the faults that keep one from being read are {:error, errors}.

to_json(board)

@spec to_json(t()) :: {:ok, String.t()} | {:error, {:missing_dependency, :jason}}

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}.

to_map(board)

@spec to_map(t()) :: map()

The board as a map: every key of spec/15 §1.2, version included.

validate(board)

@spec validate(t() | map()) :: :ok | {:error, [error(), ...]}

: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"}}]}

validate(board, store)

@spec validate(t() | map(), store()) :: :ok | {:error, [error(), ...]}

As validate/3 with [] options.

validate(board, store, opts)

@spec validate(t() | map(), store(), keyword()) :: :ok | {:error, [error(), ...]}

: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: and generators:. 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:.

vars(board, cell)

@spec vars(t(), atom() | String.t()) :: %{required(String.t()) => term()}

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.

virtual_sources(board)

@spec virtual_sources(t()) :: %{required([String.t()]) => Visualize.VirtualSource.t()}

Every virtual source of the board and its cells by path (spec/15 §1.2, spec/08 §10.5.9): [id] for the board's own, [cell, id] for a cell's, each a %Visualize.VirtualSource{}.

window(board, now)

@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]}

window_text(arg1, zone)

@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