A chart as a design map (spec/14).
The nested map is the chart: it names its sources, its frame, its marks and its
labels without a function anywhere in it, so a design can be stored, diffed, built by
a tool and bound to different data. This struct is a view of that map — one field per
design-level key (spec/14 §2.1) — and from_map/1 and to_map/1 are exact inverses:
the nested nodes are stored as the author wrote them, and a reader takes any default
from Visualize.Chart.Schema (D-59). from_json/1 and to_json/1 are the same round
trip over a JSON document, behind the optional Jason dependency (spec/14 §11).
A design is a template: its sources are typed slots and its variables are terms.
apply/2 resolves the variables, binds the slots from a pool of named sources and
realises the frame into a Visualize.Chart.Applied that render/2 draws (spec/14 §7.3);
compose/2 merges fragments by the schema's merge rules and stack/1 layers them (§8).
Examples
iex> {:ok, chart} =
...> Visualize.Chart.from_map(%{
...> version: 2,
...> sources: %{primary: %{fields: [:t, :v]}},
...> frames: %{main: %{kind: :cartesian, scales: %{x: %{kind: :time}, y: %{kind: :linear}}}},
...> marks: [%{type: :line, data: :primary, channels: %{x: :t, y: :v}}]
...> })
iex> chart.marks
[%{type: :line, data: :primary, channels: %{x: :t, y: :v}}]
iex> Visualize.Chart.from_map(%{version: 2, frames: %{main: %{kind: :cartesian}}, marks: [%{}]})
{:error, [{[:marks, 0, :data], :required}, {[:marks, 0, :type], :required}]}
Summary
Types
One variable a design still demands, and where it stands (spec/14 §17.2).
One leaf of a stack, with the layer that set it and the layers it overrode (spec/14 §17.1).
Functions
The design applied to its data (spec/14 §7.3): variables resolved, slots bound, the frame realised.
A fragment with its variables substituted at a use site (spec/14 §19.4, D-102): a
literal sets, another var/1 renames. See Visualize.Chart.Use.bind/2.
The type a source's types declares for a field (spec/14 §2.3, #369), or nil when
the design, the source or the type is absent.
The design compiled for rendering per tick (spec/14 §12): the split into static and
dynamic regions computed from the design, a render target per mark chosen from the
bound data, and a Visualize.Chart.Compiled per frame that draws only what moves.
The fragments folded left to right through compose/2 (spec/14 §8); {:ok, %{}} for
an empty list.
The second fragment merged over the first by the merge rule of every key (spec/14 §8).
Where every value of a stack came from (spec/14 §17.1).
A composite resolved through a fetch and stacked: a flat design (spec/14 §19.6, D-103).
A design that is not a composite flattens to itself. See Visualize.Chart.Composite.flatten/2.
The variables a design still demands (spec/14 §17.2).
The chart of a JSON document (spec/14 §11).
The chart of a design map, or every fault in it.
As from_map/1, raising ArgumentError with the formatted errors.
As generate/2 with [] options.
The applied chart as a Visualize.IR.Element (spec/14 §4.7): the frame's group over
the bound sources — resolve: and paths: as Visualize.Chart.Frame.generate/2 takes
them — or, with root: true, that group inside the accessible root of
Visualize.IR.Element.root/3 at the frame's size (#131).
A fragment with paths removed at a use site (spec/14 §19.5). See Visualize.Chart.Use.mask/2.
As render/2 with [] options.
The applied chart drawn: generate/2 serialised through Visualize.Render.to_string/2
— backend: and resolve: (default Visualize.Theme.mode/1 of the backend), and
paths:, root:, title:, description: and responsive: as generate/2 takes them.
One use site resolved through a fetch — ref → local → mask → bind (spec/14 §19.3).
See Visualize.Chart.Use.resolve/2.
The layers cascaded left to right, the later layer the higher (spec/14 §8.2).
The higher fragment cascaded over the lower by the merge rule of every key (spec/14 §8.2).
The JSON document of a chart (spec/14 §11), or {:error, {:missing_dependency, :jason}}.
The design map of a chart: every design-level key, nested nodes as stored.
A variable term for the name (spec/14 §7.1).
Types
@type free_var() :: %{ name: atom(), default: term(), required: boolean(), uses: [ %{ path: Visualize.Chart.Validator.path(), expects: Visualize.Chart.Schema.type(), layer: term() } ] }
One variable a design still demands, and where it stands (spec/14 §17.2).
default is the declaration's from the stacked vars node, nil where no layer
declares one, and required is true exactly then. A use's expects is the schema's
type at that path (Visualize.Chart.Schema.type/0) and its layer is named as
explain/1 names one.
A layer of a stack (spec/14 §8.2): a fragment, a chart, or a {name, layer} pair whose
name stack/1 ignores and explain/1 reports.
@type provenance() :: %{ path: Visualize.Chart.Validator.path(), value: term(), layer: term(), overrode: [term()] }
One leaf of a stack, with the layer that set it and the layers it overrode (spec/14 §17.1).
layer is a layer's name, or its zero-based position in the list when it was given
without one; overrode reads lowest first and is [] where nothing was overridden.
@type t() :: %Visualize.Chart{ defs: %{required(atom()) => map()}, frames: %{required(atom()) => map()} | nil, interaction: map(), labels: [map()], layout: map(), marks: [map()], meta: map(), sources: %{required(atom()) => map()}, styles: %{required(atom()) => map()}, theme: atom() | map(), vars: %{required(atom()) => map()}, version: pos_integer() }
Functions
@spec apply(t() | map(), keyword()) :: {:ok, Visualize.Chart.Applied.t()} | {:error, [Visualize.Chart.Validator.error(), ...]}
The design applied to its data (spec/14 §7.3): variables resolved, slots bound, the frame realised.
The design is a chart or a design map, which goes through from_map/1 first. Three
steps follow, each reporting every fault it finds by path and stopping before the
next: every variable is replaced by the value vars: gives it, else by its declared
default ({:unresolved, :var, name} where it stood; {:undeclared, :var, name} at
[:vars, name] for a value no declaration carries); every slot binds to the pool's
entry under its own name, else its default ({:unbound, :source, name} at
[:sources, name]), and every field it declares must be a key of some row of the
bound source ({:missing_field, field} there); then the result is validated and
Visualize.Chart.Frame.new/2 realises its frame, inferring every :auto domain.
Options
:sources- the pool: a map from name to anythingVisualize.Data.Table.rows/1reads (default%{}):vars- a map from variable name to value (default%{}):theme- a%Visualize.Theme{}, asVisualize.Chart.Frame.new/2takes it:size- the host's container as{width, height}(default{600, 400}, spec/14 §4.2); a design records no size:now- the instant a:windowstep measures its durations back from (spec/14 §5.4.1); without it each such step takes the newest reading of its own column
Raises ArgumentError as Visualize.Chart.Frame.new/2 does, for a value a scale
cannot take or a theme name it cannot resolve.
Examples
iex> design = %{
...> version: 2,
...> sources: %{primary: %{fields: [:t, :v], default: :uptime}},
...> vars: %{unit: %{default: "%"}},
...> frames: %{main: %{kind: :cartesian, scales: %{x: %{kind: :linear}, y: %{kind: :linear}}}},
...> marks: [%{type: :line, data: :primary, channels: %{x: :t, y: :v}}],
...> labels: [%{anchor: {:axis, :y}, text: ["unit: ", Visualize.Chart.var(:unit)]}]
...> }
iex> {:ok, applied} = Visualize.Chart.apply(design, sources: %{uptime: %{t: [1, 2], v: [3, 9]}})
iex> applied.chart.labels
[%{anchor: {:axis, :y}, text: "unit: %"}]
iex> Visualize.Chart.Frame.scale(applied.frame, :y).domain
[3, 9]
iex> Visualize.Chart.apply(design, sources: %{primary: %{t: [1, 2]}}, vars: %{unit: "kg"})
{:error, [{[:sources, :primary], {:missing_field, :v}}]}
A fragment with its variables substituted at a use site (spec/14 §19.4, D-102): a
literal sets, another var/1 renames. See Visualize.Chart.Use.bind/2.
The type a source's types declares for a field (spec/14 §2.3, #369), or nil when
the design, the source or the type is absent.
iex> design = %{sources: %{s: %{fields: [:t, :v], types: %{t: :time}}}}
iex> Visualize.Chart.column_type(design, :s, :t)
:time
iex> Visualize.Chart.column_type(design, :s, :v)
nil
iex> Visualize.Chart.column_type(design, :other, :t)
nil
@spec compile(Visualize.Chart.Applied.t() | t() | map(), keyword()) :: {:ok, %{required(atom()) => Visualize.Chart.Compiled.t()}} | {:error, [Visualize.Chart.Validator.error(), ...]}
The design compiled for rendering per tick (spec/14 §12): the split into static and
dynamic regions computed from the design, a render target per mark chosen from the
bound data, and a Visualize.Chart.Compiled per frame that draws only what moves.
The result is one compiled chart per frame, by name (§12.1, #385) — a design of one frame is a map of one, so a caller has one shape to read whatever the design holds. Each is compiled over its own realised frame and the marks and labels that say it.
An applied chart is compiled as it is; a chart or a design map is applied first with
the sources:, vars: and theme: options of apply/2, and its errors are this
function's. The one option of the compiler's own is :ceiling, the point count above
which render: :auto leaves SVG for the canvas (default 1000, spec/00 §3.2).
Examples
iex> design = %{
...> version: 2,
...> sources: %{primary: %{fields: [:t, :v]}},
...> frames: %{main: %{kind: :cartesian, scales: %{x: %{kind: :linear, domain: [0, 10]}, y: %{kind: :linear, domain: [0, 10]}}}},
...> marks: [%{type: :line, data: :primary, channels: %{x: :t, y: :v}}]
...> }
iex> {:ok, %{main: compiled}} = Visualize.Chart.compile(design, sources: %{primary: %{t: [1, 2], v: [3, 9]}}, ceiling: 1)
iex> Visualize.Chart.Compiled.dynamic_regions(compiled)
[{{:mark, 0}, :data}]
iex> Visualize.Chart.Compiled.targets(compiled)
[{:binary, 2}]
@spec compose([t() | map()]) :: {:ok, map()} | {:error, [Visualize.Chart.Validator.error(), ...]}
The fragments folded left to right through compose/2 (spec/14 §8); {:ok, %{}} for
an empty list.
@spec compose(t() | map(), t() | map()) :: {:ok, map()} | {:error, [Visualize.Chart.Validator.error(), ...]}
The second fragment merged over the first by the merge rule of every key (spec/14 §8).
A fragment is a design map — it need not carry version or frame — or a chart,
taken as its map. Lists of nodes concatenate, name-keyed declarations unite (a name
the two declare differently is :conflict at its path), scalars are the later's, and
a node both give as a map merges key by key by its own rules. Nothing is validated:
the result is a fragment too, and becomes a chart through from_map/1 or apply/2.
Examples
iex> house = %{styles: %{series: %{stroke_width: 2}}, labels: [%{anchor: :caption, text: ["© house"]}]}
iex> chart = %{version: 2, frames: %{main: %{kind: :cartesian}}, labels: [%{anchor: :title, text: ["Uptime"]}]}
iex> {:ok, design} = Visualize.Chart.compose(house, chart)
iex> Enum.map(design.labels, & &1.anchor)
[:caption, :title]
iex> Visualize.Chart.compose(house, %{styles: %{series: %{stroke_width: 3}}})
{:error, [{[:styles, :series], :conflict}]}
@spec explain([layer()]) :: [provenance()]
Where every value of a stack came from (spec/14 §17.1).
Takes the layers — the same list stack/1 takes, each a fragment, a chart or a
{name, layer} pair — and returns one provenance/0 per leaf path of stack/1 of
those layers, ordered by path: the value, the layer that set it, and the layers it
overrode, lowest first. A layer without a name is reported by its zero-based position.
A leaf is a value the cascade does not descend into: the walk descends through every node, every name-keyed declaration and every element of a collection, and stops at a scalar, a name, a text, an extent or a variable. The stack itself carries no tags — a tagged fragment would not validate and could not be a layer of another stack — so this recomputes the cascade of spec/14 §8.2, which is why the two cannot disagree.
Examples
iex> house = %{theme: :dark, frames: %{main: %{margin: %{top: 800, bottom: 400}}}}
iex> panel = %{frames: %{main: %{margin: %{top: 600}}}}
iex> Visualize.Chart.explain([{:house, house}, {:panel, panel}])
[
%{path: [:frames, :main, :margin, :bottom], value: 400, layer: :house, overrode: []},
%{path: [:frames, :main, :margin, :top], value: 600, layer: :panel, overrode: [:house]},
%{path: [:theme], value: :dark, layer: :house, overrode: []}
]
@spec flatten(map(), (Visualize.Chart.Fragment.id() -> {:ok, map()} | :error)) :: {:ok, map(), Visualize.Chart.Composite.report()} | {:error, {:cycle, [Visualize.Chart.Fragment.id()]}}
A composite resolved through a fetch and stacked: a flat design (spec/14 §19.6, D-103).
A design that is not a composite flattens to itself. See Visualize.Chart.Composite.flatten/2.
The variables a design still demands (spec/14 §17.2).
Takes a fragment, a chart or the layers of a stack — the list stack/1 takes, stacked
first — and returns one free_var/0 per variable still in the result, ordered by
name: its default from the stacked vars node, required where no layer declares
one, and one uses entry per path it stands at with the schema's expectation there
(§1.4) and the layer that set the leaf it stands in. A fragment is a one-layer stack,
so its uses are reported as layer 0.
The walk is apply/2's own, so this lists exactly the variables application will
demand: a variable resolution does not reach — inside an extent, inside a value another
variable is bound to — is not listed, because it is not resolved either.
Examples
iex> house = %{vars: %{accent: %{default: :series_1}}, styles: %{s: %{stroke: Visualize.Chart.var(:accent)}}}
iex> panel = %{labels: [%{anchor: :title, text: ["Uptime in ", Visualize.Chart.var(:unit)]}]}
iex> Visualize.Chart.free_vars([{:house, house}, {:panel, panel}])
[
%{
name: :accent,
default: :series_1,
required: false,
uses: [%{path: [:styles, :s, :stroke], expects: :colour, layer: :house}]
},
%{
name: :unit,
default: nil,
required: true,
uses: [%{path: [:labels, 0, :text], expects: :text, layer: :panel}]
}
]
@spec from_json(String.t()) :: {:ok, t()} | {:error, [Visualize.Chart.Validator.error(), ...] | {:missing_dependency, :jason}}
The chart of a JSON document (spec/14 §11).
A document that does not parse is {:error, [{[], {:json, message}}]}; without
Jason the result is {:error, {:missing_dependency, :jason}}.
@spec from_map(term()) :: {:ok, t()} | {:error, [Visualize.Chart.Validator.error(), ...]}
The chart of a design map, or every fault in it.
The map is migrated to the current version (spec/14 §9) and validated (§10) first; design-level keys absent from the map take the defaults of spec/14 §2.1.
As from_map/1, raising ArgumentError with the formatted errors.
@spec generate(Visualize.Chart.Applied.t()) :: Visualize.IR.Element.t()
As generate/2 with [] options.
@spec generate(Visualize.Chart.Applied.t(), keyword()) :: Visualize.IR.Element.t()
The applied chart as a Visualize.IR.Element (spec/14 §4.7): the frame's group over
the bound sources — resolve: and paths: as Visualize.Chart.Frame.generate/2 takes
them — or, with root: true, that group inside the accessible root of
Visualize.IR.Element.root/3 at the frame's size (#131).
Options
:root- wrap the group in a root<svg>(defaultfalse):titleand:description- the root's; default the design'smeta.nameandmeta.description, an absent or empty one leaving its element out:responsive- the root's, asVisualize.IR.Element.root/3takes it
@spec mask(map(), [Visualize.Chart.Use.path() | atom()] | :all) :: map()
A fragment with paths removed at a use site (spec/14 §19.5). See Visualize.Chart.Use.mask/2.
@spec render(Visualize.Chart.Applied.t()) :: String.t()
As render/2 with [] options.
@spec render(Visualize.Chart.Applied.t(), keyword()) :: String.t()
The applied chart drawn: generate/2 serialised through Visualize.Render.to_string/2
— backend: and resolve: (default Visualize.Theme.mode/1 of the backend), and
paths:, root:, title:, description: and responsive: as generate/2 takes them.
@spec resolve(Visualize.Chart.Use.t(), (Visualize.Chart.Fragment.id() -> {:ok, map()} | :error)) :: {:ok, map()} | {:missing, Visualize.Chart.Fragment.id(), map()}
One use site resolved through a fetch — ref → local → mask → bind (spec/14 §19.3).
See Visualize.Chart.Use.resolve/2.
The layers cascaded left to right, the later layer the higher (spec/14 §8.2).
stack([]) is %{} and stack([a]) is a itself, taken as its map: the cascade
cannot fail, so there is no tuple to unwrap.
Examples
iex> theme = %{version: 2, theme: :default, frames: %{main: %{kind: :cartesian}}}
iex> house = %{theme: :dark, frames: %{main: %{margin: %{top: 800, bottom: 400}}}}
iex> panel = %{frames: %{main: %{margin: %{top: 600, bottom: 300}}}}
iex> design = Visualize.Chart.stack([theme, house, panel])
iex> {design.theme, design.frames.main.kind, design.frames.main.margin}
{:dark, :cartesian, %{top: 600, bottom: 300}}
The higher fragment cascaded over the lower by the merge rule of every key (spec/14 §8.2).
For every key the higher layer wins. Lists of nodes append, except that a higher element
whose identity equals a lower element's replaces it in place — a mark's and a label's
id, an axis's {scale, side} — which is what lets a layer restyle the shared frame's
x-axis rather than draw a second one; a name-keyed declaration both layers carry is the
higher layer's entry whole, where compose/2 reports :conflict, since derivation
within a style is extends (spec/14 §3.5) and never the cascade; and a node both layers
give as a map cascades key by key, so a panel's frame adds its scales to a shared
frame's and takes precedence on the frame's scalars. Nothing is validated or defaulted:
the result is a fragment too, and a layer of another stack.
compose/2 is the other operation over fragments and is unchanged: it assembles parts
that are meant to be disjoint and reports a name declared twice. Where two fragments
are disjoint the two agree (spec/14 §8.3).
Examples
iex> house = %{theme: :dark, styles: %{series: %{stroke: :series_1, stroke_width: 2}}}
iex> panel = %{styles: %{series: %{stroke_width: 3}}, labels: [%{anchor: :title, text: ["Uptime"]}]}
iex> design = Visualize.Chart.stack(house, panel)
iex> design.styles.series
%{stroke_width: 3}
iex> {design.theme, length(design.labels)}
{:dark, 1}
iex> Visualize.Chart.compose(house, panel)
{:error, [{[:styles, :series], :conflict}]}
The JSON document of a chart (spec/14 §11), or {:error, {:missing_dependency, :jason}}.
The design map of a chart: every design-level key, nested nodes as stored.
@spec var(atom()) :: Visualize.Chart.Var.t()
A variable term for the name (spec/14 §7.1).