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

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.

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 colour scale a category → :ordinal, a number → :sequential. A channel with no evidence derives nothing; a scale already declared is never in the result.

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 = %{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}]}

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 colour 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 = %{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}}}, %{})
%{}

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 = %{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}})
[]