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
realizes 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
What is known of a slot's fields' types (spec/14 §7.4): its Visualize.Data.Schema, what
they mean, or a map from field to type, how rows write them.
One variable a design still demands, and where it stands (spec/14 §17.2).
What a scale's kind needs of a field it reads (spec/14 §7.4, D-153).
A chart's items placed once (placements/1, spec/14 §21.5, #811): opaque, and passed
only with the chart it was made of.
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 realized.
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.
:ok, or every field a mark reads through a scale whose known type the scale's kind
cannot take (spec/14 §7.4, D-153): {:type_mismatch, slot, field, expected, given} at
the read's path, ordered by path.
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).
Each slot's requirements of its feed's schema (spec/14 §7.4): its requires with every
attribute a fact of it reads, as %{slot => %{field => [key, …]}} in the order of
Visualize.Data.Schema.attributes/0. A slot with none is absent. A picker tests a
feed's schema against it before offering the feed (§7.2).
Where every value of a stack came from (spec/14 §17.1).
Where each part of one element's style came from (spec/14 §17.3, D-172).
A fact of the feed's schema bound to slot source: the attribute key of field
(spec/14 §7.4). Allowed wherever a value can appear, and in a text as
#{fact(:source, :field, :key)}; apply/2 replaces it by the attribute, and a
schema that does not give it fails application.
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), with limits:, urls: and generators: as
from_map/2 takes them.
The chart of a design map, or every fault in it.
As from_map/2, 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.
The chart's items placed once (spec/14 §21.5, #811): every data mark of a cartesian
frame, its rows and their pixels. generate/2 and render/2 take it as placements:,
and so does Visualize.Hooks.Keyboard.attrs/2, so a chart drawn with its keyboard
points places each item once rather than twice. Pass it only with the chart it was made
of; what it holds is opaque.
As render/2 with [] options.
The applied chart drawn: generate/2 serialized through Visualize.Render.to_string/2
— backend: and resolve: (default Visualize.Theme.mode/1 of the backend), and
paths:, root:, id:, title:, description:, summary:, responsive:,
color_scheme: and placements: 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): an atom or a string, held as the string (§1.4, D-195).
Types
@type field_types() :: Visualize.Data.Schema.t() | %{optional(atom() | String.t()) => atom()}
What is known of a slot's fields' types (spec/14 §7.4): its Visualize.Data.Schema, what
they mean, or a map from field to type, how rows write them.
@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 need() :: :time | :number | :category
What a scale's kind needs of a field it reads (spec/14 §7.4, D-153).
A chart's items placed once (placements/1, spec/14 §21.5, #811): opaque, and passed
only with the chart it was made of.
@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{ budget: pos_integer() | {:per_px, number()} | nil, defaults: map() | nil, defs: %{required(String.t()) => 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 realized.
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 realizes 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:system- the unit system the axes show their measures in,:metric,:us,:ukor:as_posted(default): the viewer's preference, never the design's (spec/14 §4.4, D-156):schemas- aVisualize.Data.Schema.Cacheof feeds' schemas by version (default empty), read for every slot whose contract is not empty (spec/14 §7.4):describe-(feed -> {:ok, schema} | {:error, reason}), asked once for a version the cache lacks (defaultnil):generators- the generator modules (Visualize.Generators, spec/14 §3.13) the design's names are checked against and its frames call, besideconfig :visualize, generators:(default[]):assigns- the host's assigns, the viewer's, which every generator's context carries (default%{})
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.
@spec check_types(t() | map(), %{required(atom()) => field_types()}) :: :ok | {:error, [Visualize.Chart.Validator.error(), ...]}
:ok, or every field a mark reads through a scale whose known type the scale's kind
cannot take (spec/14 §7.4, D-153): {:type_mismatch, slot, field, expected, given} at
the read's path, ordered by path.
A read is a channel naming a scale, a style's {:field, f} color or an offset's field,
over rows that are the slot's own. A time scale needs :time; band and ordinal need
:category; every other kind needs :number. types maps a slot to what is known of
its fields. A Visualize.Data.Schema says what they mean: :text satisfies a category,
and :time is instants (D-154), which no numeric scale takes. A map from field to
type, as Visualize.Data.Table.column_types/1 reads it from rows, says how they are
written: :time is instants there too, and a category takes anything. A
slot it does not hold, and a field it does not type, are not checked. Pure and total
over any map, so a tool calls it before apply/2 to show a wrong binding without
drawing.
iex> design = %{
...> sources: %{primary: %{fields: [:t, :v]}},
...> frames: %{main: %{kind: :cartesian,
...> scales: %{x: %{kind: :linear}, y: %{kind: :linear}}}},
...> marks: [%{type: :line, data: :primary, channels: %{x: :t, y: :v}}]
...> }
iex> Visualize.Chart.check_types(design, %{primary: %{t: :time, v: :number}})
{:error, [{[:marks, 0, :channels, :x], {:type_mismatch, "primary", "t", :number, :time}}]}
iex> schema = Visualize.Data.Schema.new!(%{t: %{type: :time}, v: %{type: :category}})
iex> Visualize.Chart.check_types(design, %{primary: schema})
{:error, [{[:marks, 0, :channels, :x], {:type_mismatch, "primary", "t", :number, :time}},
{[:marks, 0, :channels, :y], {:type_mismatch, "primary", "v", :number, :category}}]}
iex> Visualize.Chart.check_types(design, %{primary: %{t: :number, v: :number}})
:ok
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 realized 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:, theme:, size:, now:, system:, generators: and assigns: options of apply/2, and its errors are this
function's. The compiler's own options are :ceiling, the point count above which
render: :auto leaves SVG for the canvas (default 1000, spec/00 §3.2), and :motion,
:full (default) or :reduce: a reader who asks for less motion gets every transition
and mark motion as a step to its target (spec/14 §21.6, D-142).
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}]}
Each slot's requirements of its feed's schema (spec/14 §7.4): its requires with every
attribute a fact of it reads, as %{slot => %{field => [key, …]}} in the order of
Visualize.Data.Schema.attributes/0. A slot with none is absent. A picker tests a
feed's schema against it before offering the feed (§7.2).
iex> design = %{
...> sources: %{s: %{fields: [:v], requires: %{v: [:label]}}},
...> labels: [%{anchor: :title, text: [Visualize.Chart.fact(:s, :v, :unit)]}]
...> }
iex> Visualize.Chart.contract(design)
%{"s" => %{"v" => [:unit, :label]}}
@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 explain(Visualize.Chart.Applied.t(), keyword()) :: map()
Where each part of one element's style came from (spec/14 §17.3, D-172).
applied is a %Visualize.Chart.Applied{}; at: the path of the node whose style is
explained — [:marks, i], [:marks, i, :label], [:labels, i],
[:frames, f, :axes, i] or [:frames, f, :legend]; datum: the element's datum, which
a {:field, f} entry reads; state: a view state laid at its layer (§3.7). The answer is
%{at, kind, state, parts, generators}, parts each part's value, whether the kind
draws it (drawn), and its chain of contributions, lowest first: each
%{layer, style, via, field, value, set, generators}. A generator (§3.13) is called as
the frame calls it, and generators lists each it called — %{name, args, at, result, layer}, result {:fault, reason} for one that applied nothing.
iex> design = %{version: 3, sources: %{s: %{fields: [:x, :y, :k]}},
...> styles: %{base: %{extends: :bad, stroke: %{width: 3}}},
...> frames: %{main: %{kind: :cartesian, scales: %{x: %{kind: :linear}, y: %{kind: :linear}}}},
...> marks: [%{type: :line, data: :s, channels: %{x: :x, y: :y}, style: [:base, %{opacity: 0.5}]}]}
iex> {:ok, applied} = Visualize.Chart.apply(design, sources: %{s: [%{x: 0, y: 1, k: "a"}]})
iex> %{parts: %{stroke: stroke, fill: fill}} = Visualize.Chart.explain(applied, at: [:marks, 0])
iex> Enum.map(stroke.chain, &{&1.layer, &1.style, &1.via})
[{:theme, nil, []}, {:style, "base", ["bad"]}]
iex> stroke.value.width
3
iex> fill.drawn
false
A fact of the feed's schema bound to slot source: the attribute key of field
(spec/14 §7.4). Allowed wherever a value can appear, and in a text as
#{fact(:source, :field, :key)}; apply/2 replaces it by the attribute, and a
schema that does not give it fails application.
@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: :color, layer: :house}]
},
%{
name: "unit",
default: nil,
required: true,
uses: [%{path: [:labels, 0, :text], expects: :text, layer: :panel}]
}
]
@spec from_json(String.t(), keyword()) :: {:ok, t()} | {:error, [Visualize.Chart.Validator.error(), ...] | {:missing_dependency, :jason}}
The chart of a JSON document (spec/14 §11), with limits:, urls: and generators: as
from_map/2 takes them.
The parsed document's "version" is checked, and its nesting and the size of its lists
and maps held to the limits, before the codec decodes a key (spec/14 §9.1, §9.2): a
newer or an oversized document is refused without an atom made from it, and a path in
such an error holds the document's own key strings. Then its tags are held to the one
table (spec/14 §11.2, D-182): an unknown tag is {:unknown_tag, tag} and a retired one
{:retired_tag, tag, why}, at the path of its object. A document that does not parse is
{:error, [{[], {:json, message}}]}; without Jason the result is
{:error, {:missing_dependency, :jason}}.
@spec from_map(term(), keyword()) :: {:ok, t()} | {:error, [Visualize.Chart.Validator.error(), ...]}
The chart of a design map, or every fault in it.
A version newer than this library reads is refused first, before anything of the map is read (spec/14 §9.1); then the map is held to the sanity limits (§9.2), migrated to the current version (§9) and validated (§10). Design-level keys absent from the map take the defaults of spec/14 §2.1.
limits: tightens the limits for a design from elsewhere — a keyword list or map over
Visualize.Chart.Limits.defaults/0, which apply when it is not given; the host knows
its inputs and holds the number. Raises ArgumentError for a name that is not a limit
or a value that is not a positive integer.
generators: names the generator modules (Visualize.Generators, spec/14 §3.13) the
design's generator names are checked against, beside config :visualize, generators:;
a name neither defines is {:undeclared, :generator, name}.
iex> Visualize.Chart.from_map(%{version: 4, frames: %{main: %{}}})
{:error, [{[:version], {:newer_schema, 4, 3}}]}
As from_map/2, 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):title- the root's accessible name; default the design'smeta.name, else the chart's kind (Visualize.Chart.Summary, spec/14 §21.2), so every chart is named:description- the root's own description; default the design'smeta.description, an absent or empty one saying nothing:summary- the generated text alternative written after the description (spec/14 §21.1):true(default) forVisualize.Chart.Summary.text/1, a string for the caller's own,falsefor none:responsive- the root's, asVisualize.IR.Element.root/3takes it:color_scheme-falseto leave out the root's automatic dark mode: in:cssoutput, under the implicit default theme, the root carries a<style>that switches to the theme's dark counterpart when the viewer prefers it (spec/08 §7.3, #668):id- the root's id, the stem of its title's and description's ids and the selector of its scoped rules (spec/14 §4.7, #712, D-188); defaultvis-and a hash of the title and description, which two alike charts on one page share:placements-placements/1of this same chart: the marks read their rows and pixels from it rather than placing them again, so a caller that also builds the chart's keyboard points (Visualize.Hooks.Keyboard.attrs/2) places once (#811)
@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 placements(Visualize.Chart.Applied.t()) :: placements()
The chart's items placed once (spec/14 §21.5, #811): every data mark of a cartesian
frame, its rows and their pixels. generate/2 and render/2 take it as placements:,
and so does Visualize.Hooks.Keyboard.attrs/2, so a chart drawn with its keyboard
points places each item once rather than twice. Pass it only with the chart it was made
of; what it holds is opaque.
@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 serialized through Visualize.Render.to_string/2
— backend: and resolve: (default Visualize.Theme.mode/1 of the backend), and
paths:, root:, id:, title:, description:, summary:, responsive:,
color_scheme: and placements: 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() | String.t()) :: Visualize.Chart.Var.t()
A variable term for the name (spec/14 §7.1): an atom or a string, held as the string (§1.4, D-195).
iex> Visualize.Chart.var(:unit)
%Visualize.Chart.Var{name: "unit"}