Visualize.Chart (Visualize v0.8.1)

Copy Markdown View Source

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

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.

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

t()

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

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

free_var()

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

layer()

@type layer() :: t() | map() | {term(), t() | map()}

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.

need()

@type need() :: :time | :number | :category

What a scale's kind needs of a field it reads (spec/14 §7.4, D-153).

placements()

@type placements() :: %{optional(term()) => map()}

A chart's items placed once (placements/1, spec/14 §21.5, #811): opaque, and passed only with the chart it was made of.

provenance()

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

t()

@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

apply(design, opts \\ [])

@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 anything Visualize.Data.Table.rows/1 reads (default %{})
  • :vars - a map from variable name to value (default %{})
  • :theme - a %Visualize.Theme{}, as Visualize.Chart.Frame.new/2 takes 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 :window step 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, :uk or :as_posted (default): the viewer's preference, never the design's (spec/14 §4.4, D-156)
  • :schemas - a Visualize.Data.Schema.Cache of 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 (default nil)

  • :generators - the generator modules (Visualize.Generators, spec/14 §3.13) the design's names are checked against and its frames call, beside config :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"}}]}

bind(fragment, vars)

@spec bind(term(), %{required(atom()) => term()}) :: term()

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.

check_types(chart, types)

@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

column_type(design, source, field)

@spec column_type(t() | map(), atom(), atom()) :: atom() | nil

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

compile(design, opts \\ [])

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

compose(fragments)

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

compose(a, b)

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

contract(chart)

@spec contract(t() | map()) :: %{required(atom()) => %{required(atom()) => [atom()]}}

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

explain(layers)

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

explain(applied, opts)

@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

fact(source, field, key)

@spec fact(atom() | String.t(), atom() | String.t(), atom()) ::
  Visualize.Chart.Fact.t()

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.

flatten(fragment, fetch)

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

free_vars(layers)

@spec free_vars(t() | map() | [layer()]) :: [free_var()]

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

from_json(json, opts \\ [])

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

from_map(map, opts \\ [])

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

from_map!(map, opts \\ [])

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

As from_map/2, raising ArgumentError with the formatted errors.

generate(applied)

As generate/2 with [] options.

generate(applied, opts)

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> (default false)
  • :title - the root's accessible name; default the design's meta.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's meta.description, an absent or empty one saying nothing
  • :summary - the generated text alternative written after the description (spec/14 §21.1): true (default) for Visualize.Chart.Summary.text/1, a string for the caller's own, false for none
  • :responsive - the root's, as Visualize.IR.Element.root/3 takes it
  • :color_scheme - false to leave out the root's automatic dark mode: in :css output, 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); default vis- and a hash of the title and description, which two alike charts on one page share
  • :placements - placements/1 of 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)

mask(fragment, paths)

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

placements(applied)

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

render(applied)

@spec render(Visualize.Chart.Applied.t()) :: String.t()

As render/2 with [] options.

render(applied, opts)

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

resolve(use, fetch)

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

stack(layers)

@spec stack([layer()]) :: map()

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

stack(lower, higher)

@spec stack(layer(), layer()) :: map()

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

to_json(chart)

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

The JSON document of a chart (spec/14 §11), or {:error, {:missing_dependency, :jason}}.

to_map(chart)

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

The design map of a chart: every design-level key, nested nodes as stored.

var(name)

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