Visualize.Chart.Frame (Visualize v0.6.0)

Copy Markdown View Source

The realised frame of a chart: its scales, built once, and the static furniture — grid, axes, legend and labels — rendered from them (spec/14 §4, D-60).

new/2 takes a Visualize.Chart and the sources its marks are bound to, realises every :scale node through the existing scale modules — a domain: :auto inferred from the whole column of every mark channel that names the scale, a range: :auto from the plot area by the scale's name — and caches the structs on the frame. scales/1 reads them, the escape hatch for annotations, brush conversion and custom marks; put_scale/3 replaces one. generate/2 is the furniture as one Visualize.IR.Element group with the margins applied and every theme slot resolved through Visualize.Theme.resolve/3; render/2 is the same as a string. Given sources:, both also draw every mark of the design between the grid and the axes through Visualize.Chart.Mark.generate/3 (spec/14 §5.6); without them a frame renders the furniture alone, over its unit domains, which is what the static split of a compiled chart needs (spec/14 §12).

Examples

iex> chart =
...>   Visualize.Chart.from_map!(%{
...>     version: 2,
...>     sources: %{primary: %{fields: [:t, :v]}},
...>     frames: %{main: %{
...>       kind: :cartesian,
...>       scales: %{x: %{kind: :linear}, y: %{kind: :linear, domain: [0, :auto]}},
...>       axes: [%{scale: :x, side: :bottom}, %{scale: :y, side: :left, grid: true}]
...>     }},
...>     marks: [%{type: :line, data: :primary, channels: %{x: :t, y: :v}}]
...>   })
iex> frame = Visualize.Chart.Frame.new(chart, sources: %{primary: %{t: [1, 2, 3], v: [4, 9, 2]}}, size: {300, 200})
iex> Visualize.Chart.Frame.scale(frame, :x).domain
[1, 3]
iex> Visualize.Chart.Frame.scale(frame, :y).domain
[0, 9]
iex> frame.plot
%{width: 240, height: 150}

Summary

Types

A frame kind of spec/14 §4.1.

t()

Functions

A frame node's box as fractions of the render's size (spec/14 §4.2): its box, or the share of the design's layout its cell and span take (§2.8). This is the one place a cell resolves; new/2 reads it, and so does a tool that must know where a frame is drawn without realising it (§18.16). size is {width, height} or %{width, height}, which the gap is taken out of. Raises ArgumentError for a malformed box or cell, as new/2 does.

The attributes of the design's brush for the frame it brushes (spec/14 §2.9, D-143): data-vis-brush, its kind; data-vis-brush-x, the x mapping exactly as sync_attrs/1 gives it; and data-vis-brush-extent, x0,y0,x1,y1, the plot area in the chart's coordinates — what BrushHook captures over and turns into a window in domain units (spec/10 §6.5). %{} for a frame the design does not brush.

The size a frame is realised at when no size: is given (spec/14 §4.2).

As generate/2 with resolve: :css.

The frame as one Visualize.IR.Element group (spec/14 §4.7): the static furniture, and every mark of the design between the grid and the axes when sources are given.

The box the frame's legend is laid out in, %{width: w, height: h} in user units, not counting the 8 between it and the plot (spec/14 §4.5, D-169): the widest row and (rows − 1) × 16 + 12 for a top or bottom legend, the estimated width and (entries − 1) × 16 + 12 for a stacked one, the title counted as an entry. A frame with no legend is %{width: 0, height: 0}. A host laying out around a chart asks it rather than repeating the text estimate.

The frame of a chart, every scale realised and the theme resolved (spec/14 §4.7).

The projection of a :geo frame (spec/14 §4.6), nil for any other kind.

The frame with the realised scale replaced: the override of spec/14 §4.3.

As render/2 with [] options.

One realised scale; ArgumentError when the frame declares no such scale.

The realised scales by name: the escape hatch (spec/14 §4.3).

The attributes of a sync group for the frame it reads (spec/14 §2.9, D-116): data-vis-sync, the group, and data-vis-sync-x, d0,d1,x0,x1,width — the x scale's domain (milliseconds since the Unix epoch for a time scale), the chart pixels of its ends and the chart's width — which is all the hooks need to map a shared x through this frame's own scale. %{} for a frame that is no group's.

Types

kind()

@type kind() :: :cartesian | :polar | :geo | :facet

A frame kind of spec/14 §4.1.

t()

@type t() :: %Visualize.Chart.Frame{
  axes: [map()],
  background: term() | nil,
  box: [number()],
  brush: :x | nil,
  defs: %{required(atom()) => map()},
  design_styles: %{required(atom()) => map()},
  domains: %{required(atom()) => term()},
  facet: %{by: atom(), columns: pos_integer()} | nil,
  forces: map(),
  kind: kind(),
  label_paths: [non_neg_integer()],
  labels: [map()],
  legend: map() | nil,
  margin: %{top: number(), right: number(), bottom: number(), left: number()},
  mark_paths: [non_neg_integer()],
  marks: [map()],
  name: atom() | nil,
  now: term() | nil,
  panels: [term()],
  plot: %{width: number(), height: number()},
  projection: Visualize.Geo.Projection.t() | nil,
  scales: %{required(atom()) => struct()},
  schemas: %{required(atom()) => Visualize.Data.Schema.t()},
  size: %{width: number(), height: number()},
  styles: %{required(atom()) => map()},
  sync: String.t() | nil,
  system: Visualize.Quantity.system(),
  theme: Visualize.Theme.t(),
  z: integer()
}

Functions

box_of(node, layout, size)

@spec box_of(map(), map() | nil, {number(), number()} | map()) :: [number()]

A frame node's box as fractions of the render's size (spec/14 §4.2): its box, or the share of the design's layout its cell and span take (§2.8). This is the one place a cell resolves; new/2 reads it, and so does a tool that must know where a frame is drawn without realising it (§18.16). size is {width, height} or %{width, height}, which the gap is taken out of. Raises ArgumentError for a malformed box or cell, as new/2 does.

iex> Visualize.Chart.Frame.box_of(%{box: [0.5, 0, 0.5, 1]}, nil, {600, 400})
[0.5, 0, 0.5, 1]

iex> Visualize.Chart.Frame.box_of(%{cell: [1, 0]}, %{columns: [3, 1]}, {600, 400})
[0.75, 0.0, 0.25, 1.0]

brush_attrs(frame)

@spec brush_attrs(t()) :: %{required(String.t()) => String.t()}

The attributes of the design's brush for the frame it brushes (spec/14 §2.9, D-143): data-vis-brush, its kind; data-vis-brush-x, the x mapping exactly as sync_attrs/1 gives it; and data-vis-brush-extent, x0,y0,x1,y1, the plot area in the chart's coordinates — what BrushHook captures over and turns into a window in domain units (spec/10 §6.5). %{} for a frame the design does not brush.

iex> chart = Visualize.Chart.from_map!(%{version: 2,
...>   interaction: %{brush: :x},
...>   frames: %{main: %{kind: :cartesian, scales: %{x: %{kind: :linear, domain: [0, 100]}}}}})
iex> Visualize.Chart.Frame.new(chart, size: {300, 200}) |> Visualize.Chart.Frame.brush_attrs()
%{"data-vis-brush" => "x", "data-vis-brush-extent" => "40,20,280,170", "data-vis-brush-x" => "0,100,40,280,300"}

default_size()

@spec default_size() :: {number(), number()}

The size a frame is realised at when no size: is given (spec/14 §4.2).

generate(frame)

@spec generate(t()) :: Visualize.IR.Element.t()

As generate/2 with resolve: :css.

generate(frame, opts)

@spec generate(t(), keyword()) :: Visualize.IR.Element.t()

The frame as one Visualize.IR.Element group (spec/14 §4.7): the static furniture, and every mark of the design between the grid and the axes when sources are given.

Options

  • :resolve - how the theme's slots render: :css (default) for references a stylesheet can override, :literal for the plain values a canvas needs
  • :sources - a map from source name to anything Visualize.Data.Table.rows/1 reads, as new/2 takes; when given, the marks are drawn from them (spec/14 §5.6)
  • :paths - true for every design node's element to carry data-node with the node's path as Visualize.Chart.Validator.format_path/1 spells it — frame, marks[0], frames.main.axes[0], frames.main.legend, labels[0] — for a caller that must name what it drew, as the builder's graph does (spec/14 §4.7, D-105); false (default) leaves the markup as it is

Examples

iex> chart = Visualize.Chart.from_map!(%{version: 2, sources: %{s: %{fields: [:x, :y]}},
...>   frames: %{main: %{kind: :cartesian, scales: %{x: %{kind: :linear}, y: %{kind: :linear}}}},
...>   marks: [%{type: :line, data: :s, channels: %{x: :x, y: :y}}]})
iex> rows = [%{x: 0, y: 1}, %{x: 1, y: 2}]
iex> group = Visualize.Chart.Frame.new(chart, sources: %{s: rows}) |> Visualize.Chart.Frame.generate(sources: %{s: rows}, paths: true)
iex> {group.attrs["data-node"], Enum.map(group.children, & &1.attrs["data-node"])}
{"frames.main", ["marks[0]"]}

legend_extent(frame)

@spec legend_extent(t()) :: %{width: number(), height: number()}

The box the frame's legend is laid out in, %{width: w, height: h} in user units, not counting the 8 between it and the plot (spec/14 §4.5, D-169): the widest row and (rows − 1) × 16 + 12 for a top or bottom legend, the estimated width and (entries − 1) × 16 + 12 for a stacked one, the title counted as an entry. A frame with no legend is %{width: 0, height: 0}. A host laying out around a chart asks it rather than repeating the text estimate.

new(chart, opts \\ [])

@spec new(Visualize.Chart.t(), keyword()) :: t()

The frame of a chart, every scale realised and the theme resolved (spec/14 §4.7).

Options

  • :sources - a map from source name to anything Visualize.Data.Table.rows/1 reads; the columns :auto domains are inferred from (default %{})
  • :theme - a %Visualize.Theme{}; taken for any theme name the design gives, and required for one that is neither built-in nor inline
  • :size - the {width, height} the frame is realised at (default {600, 400}, spec/14 §4.2); a design records none
  • :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
  • :schemas - the schema of each slot by name, whose declared identity a :window step's latest reads (spec/14 §5.4.1, D-168); Visualize.Chart.apply/2 passes the applied chart's (default %{})
  • :system - the unit system every axis shows its measure in, :metric, :us, :uk or :as_posted (default): the viewer's, never the design's (spec/14 §4.4, D-156)
  • :warm - the force layouts a compiled chart carries between ticks (spec/14 §12.5, D-132); every :force step is cold without it (default %{})

Each distinct :force step of the frame's marks is laid out once, before any domain is inferred, and held in forces: every pipeline that reaches the step reads that layout (spec/14 §5.4.3).

Raises ArgumentError for an unresolved variable, a theme name it cannot resolve, a system: not of Visualize.Quantity.systems/0, a bound value a scale cannot take, a legend in a side margin that cannot hold it, a top or bottom legend with a row or a single entry wider than the plot, or a max_label below 1 (spec/14 §4.5). A top or bottom legend in a margin too small for its rows grows that margin, and the plot is that much shorter (D-169).

projection(frame)

@spec projection(t()) :: Visualize.Geo.Projection.t() | nil

The projection of a :geo frame (spec/14 §4.6), nil for any other kind.

put_scale(frame, name, scale)

@spec put_scale(t(), atom(), struct()) :: t()

The frame with the realised scale replaced: the override of spec/14 §4.3.

render(frame)

@spec render(t()) :: String.t()

As render/2 with [] options.

render(frame, opts)

@spec render(t(), keyword()) :: String.t()

generate/2 rendered through Visualize.Render.to_string/2.

Options

scale(frame, name)

@spec scale(t(), atom()) :: struct()

One realised scale; ArgumentError when the frame declares no such scale.

scales(frame)

@spec scales(t()) :: %{required(atom()) => struct()}

The realised scales by name: the escape hatch (spec/14 §4.3).

sync_attrs(frame)

@spec sync_attrs(t()) :: %{required(String.t()) => String.t()}

The attributes of a sync group for the frame it reads (spec/14 §2.9, D-116): data-vis-sync, the group, and data-vis-sync-x, d0,d1,x0,x1,width — the x scale's domain (milliseconds since the Unix epoch for a time scale), the chart pixels of its ends and the chart's width — which is all the hooks need to map a shared x through this frame's own scale. %{} for a frame that is no group's.

iex> chart = Visualize.Chart.from_map!(%{version: 2,
...>   interaction: %{sync: "dash"},
...>   frames: %{main: %{kind: :cartesian, scales: %{x: %{kind: :linear, domain: [0, 100]}}}}})
iex> Visualize.Chart.Frame.new(chart, size: {300, 200}) |> Visualize.Chart.Frame.sync_attrs()
%{"data-vis-sync" => "dash", "data-vis-sync-x" => "0,100,40,280,300"}