Visualize.Chart.Builder.Scales (Visualize v0.8.1)

Copy Markdown View Source

The scales a design's bound channels need and the frame lacks (spec/14 §18.16, #370).

A channel finds its scale by name and a scale the frame does not declare is the identity (§4.3) — right for a layout that emits pixels, a trap for a person binding a column. So whenever the builder binds a channel it asks derive/2 for the scales to declare, each kind read from the column's type (§2.3, or what its rows suggest), and unscaled/2 for the position channels it could not type, which are needs.

Every function here is pure; the builder writes what derive/2 returns.

This module compiles only when Phoenix.Component is loaded (spec/10 §1.1, D-45, D-85).

Summary

Types

The types the pool's rows suggest, by source name.

Functions

The type the design declares for a source's field, else the type its rows suggest.

For every mark that binds a source, the columns a channel may pick (spec/14 §18.6, #371): the source's declared fields and the pool's columns for it, sorted, each with its type — the design's, else the rows' — keyed by the channels node's path.

columns/2 with the pool's types already read (pool_types/1), as the editing pass reads them once for every field (spec/14 §19.10, D-189).

The scales the design's bound channels name and the frame does not declare, as %{name => node}, each %{kind: kind, domain: :auto} with the kind from the channel's evidence: time → :time, number → :linear, a category or text → :band (the layer's one categorical position scale, spec/03); on the color scale a category → :ordinal, a number → :sequential. A channel with no evidence derives nothing; a scale already declared is never in the result.

derive/2 for one frame of the design (#615): the scales the channels of the marks drawn in frame name and that frame does not declare. A design of several frames derives each frame's from its own marks, since a scale name belongs to its frame (§4.3).

The types the pool's rows suggest, by source, over each source's first 200 rows (Visualize.Data.Table.column_types/1); a source with no rows suggests nothing.

The position scales a mark reads — x, y, angle, r — through its channels and its scales renames, in channel order (spec/14 §18.16, #373).

The position channels bound to a field that names a scale the frame lacks and no type could be read for: {[:marks, i, :channels, ch], {:unscaled, field}} each, the builder's needs (spec/14 §18.16).

Types

pool_types()

@type pool_types() :: %{required(atom()) => %{required(atom()) => atom()}}

The types the pool's rows suggest, by source name.

Functions

column_type(design, pool_types, source, field)

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

The type the design declares for a source's field, else the type its rows suggest.

columns(design, pool)

@spec columns(map(), map()) :: %{
  required([term()]) => [%{name: atom(), type: atom() | nil}]
}

For every mark that binds a source, the columns a channel may pick (spec/14 §18.6, #371): the source's declared fields and the pool's columns for it, sorted, each with its type — the design's, else the rows' — keyed by the channels node's path.

iex> design = Visualize.Chart.Migration.names(%{sources: %{s: %{fields: [:t, :v], types: %{t: :time}}},
...>   marks: [%{type: :line, data: :s}, %{type: :line}]})
iex> Visualize.Chart.Builder.Scales.columns(design, %{s: [%{t: 1, v: 2, extra: "x"}]})
%{[:marks, 0, :channels] => [%{name: "extra", type: :text}, %{name: "t", type: :time}, %{name: "v", type: :number}]}

columns(design, pool, pool_types)

@spec columns(map(), map(), pool_types()) :: %{
  required([term()]) => [%{name: atom(), type: atom() | nil}]
}

columns/2 with the pool's types already read (pool_types/1), as the editing pass reads them once for every field (spec/14 §19.10, D-189).

derive(design, pool_types)

@spec derive(map(), pool_types()) :: %{required(atom()) => map()}

The scales the design's bound channels name and the frame does not declare, as %{name => node}, each %{kind: kind, domain: :auto} with the kind from the channel's evidence: time → :time, number → :linear, a category or text → :band (the layer's one categorical position scale, spec/03); on the color scale a category → :ordinal, a number → :sequential. A channel with no evidence derives nothing; a scale already declared is never in the result.

iex> design = Visualize.Chart.Migration.names(%{frames: %{main: %{kind: :cartesian}},
...>   sources: %{s: %{fields: [:t, :v, :k], types: %{t: :time, v: :number, k: :category}}},
...>   marks: [%{type: :line, data: :s, channels: %{x: :t, y: :v}},
...>           %{type: :rect, data: :s, channels: %{x0: :k, x1: :k, y0: 0, y1: :v, value: :k}}]})
iex> Visualize.Chart.Builder.Scales.derive(design, %{})
%{"x" => %{kind: :time, domain: :auto}, "y" => %{kind: :linear, domain: :auto}, "color" => %{kind: :ordinal, domain: :auto}}
iex> Visualize.Chart.Builder.Scales.derive(%{frames: %{"main" => %{kind: :cartesian}}}, %{})
%{}

derive(design, pool_types, frame)

@spec derive(map(), pool_types(), atom()) :: %{required(atom()) => map()}

derive/2 for one frame of the design (#615): the scales the channels of the marks drawn in frame name and that frame does not declare. A design of several frames derives each frame's from its own marks, since a scale name belongs to its frame (§4.3).

iex> design = Visualize.Chart.Migration.names(%{frames: %{a: %{kind: :cartesian}, b: %{kind: :cartesian, scales: %{x: %{kind: :linear}}}},
...>   sources: %{s: %{fields: [:t, :v], types: %{t: :number, v: :number}}},
...>   marks: [%{type: :line, frame: :b, data: :s, channels: %{x: :t, y: :v}}]})
iex> {Visualize.Chart.Builder.Scales.derive(design, %{}, "a"), Visualize.Chart.Builder.Scales.derive(design, %{}, "b")}
{%{}, %{"y" => %{kind: :linear, domain: :auto}}}

pool_types(pool)

@spec pool_types(map()) :: pool_types()

The types the pool's rows suggest, by source, over each source's first 200 rows (Visualize.Data.Table.column_types/1); a source with no rows suggests nothing.

iex> Visualize.Chart.Builder.Scales.pool_types(%{s: [%{t: 1, k: "a"}], e: []})
%{s: %{t: :number, k: :text}, e: %{}}

position_scales(design, mark)

@spec position_scales(map(), map()) :: [atom()]

The position scales a mark reads — x, y, angle, r — through its channels and its scales renames, in channel order (spec/14 §18.16, #373).

iex> design = %{frames: %{"main" => %{kind: :cartesian}}}
iex> Visualize.Chart.Builder.Scales.position_scales(design, %{type: :line, channels: %{y: "v", x: "t", series: "k"}})
["x", "y"]
iex> Visualize.Chart.Builder.Scales.position_scales(%{frames: %{"main" => %{kind: :polar}}}, %{type: :rose, channels: %{angle: "k", value: "v"}})
["angle", "r"]

unscaled(design, pool_types)

@spec unscaled(map(), pool_types()) :: [{[term()], {:unscaled, atom()}}]

The position channels bound to a field that names a scale the frame lacks and no type could be read for: {[:marks, i, :channels, ch], {:unscaled, field}} each, the builder's needs (spec/14 §18.16).

iex> design = Visualize.Chart.Migration.names(%{frames: %{main: %{kind: :cartesian}}, sources: %{s: %{fields: [:t, :v]}},
...>   marks: [%{type: :line, data: :s, channels: %{x: :t, y: :v}}]})
iex> Visualize.Chart.Builder.Scales.unscaled(design, %{})
[{[:marks, 0, :channels, :x], {:unscaled, "t"}}, {[:marks, 0, :channels, :y], {:unscaled, "v"}}]
iex> Visualize.Chart.Builder.Scales.unscaled(design, %{s: %{t: :number, v: :number}})
[]