Visualize.Data.Table (Visualize v0.8.1)

Copy Markdown View Source

Tabular data as the row list the generators consume.

Every generator takes a list of rows. rows/1 turns the other shapes data arrives in — a column-oriented map, a keyword list of columns, an Nx.Tensor, an Explorer DataFrame or anything else implementing the Table.Reader protocol — into that list, and passes a list of rows through untouched (spec/08 §6).

Visualize.Data.Table.rows(%{t: [1, 2, 3], v: [10, 20, 30]})
# => [%{t: 1, v: 10}, %{t: 2, v: 20}, %{t: 3, v: 30}]

Sources that name their columns with strings (Explorer does) yield rows keyed by strings; accessor/1 reads a column by either spelling, so :v reaches "v" and the other way round. time_columns/1 names the columns whose every value is temporal, for a time scale to be inferred from.

The table package is optional, like Nx: without it lists, column maps and tensors still work, and a struct source raises UndefinedFunctionError at Table.to_rows/1 (D-52).

Summary

Types

A column name, as the source spells it.

A row: a map keyed by column name, or a tuple in column order.

Anything rows/1 accepts: a list of rows, a keyword list or map of equal-length columns, an Nx.Tensor of rank 1 or 2, a Visualize.Data.Delivery, or a struct implementing Table.Reader.

Functions

The accessor fn row -> get(row, name) end, for a shape's x/y setters.

Every named column's type, read whole (spec/08 §6.3, spec/14 §2.3, #369): all non-nil values temporal is :time, all numbers :number, all strings :text, all atoms or booleans :category; a mixed or empty column is left out. A suggestion for a tool, never something the layer reads at render.

The value of column name in row, by either spelling of the name.

The key of row that spells name: name itself when it is a key, else the key of the other spelling, nil when neither is.

A tensor's columns under names, in order: the column map rows/1 reads (spec/08 §6.4, D-214).

The rows of a tabular source, as a list.

Whether a value is a point in time: a DateTime, Date or NaiveDateTime, or a string in ISO 8601 form that one of those parses (DateTime.from_iso8601/1 first, then NaiveDateTime.from_iso8601/1, then Date.from_iso8601/1).

The names of the columns whose values are all temporal.

Types

name()

@type name() :: atom() | String.t()

A column name, as the source spells it.

row()

@type row() :: map() | tuple()

A row: a map keyed by column name, or a tuple in column order.

source()

@type source() ::
  [row()] | %{required(name()) => [term()]} | [{name(), [term()]}] | struct()

Anything rows/1 accepts: a list of rows, a keyword list or map of equal-length columns, an Nx.Tensor of rank 1 or 2, a Visualize.Data.Delivery, or a struct implementing Table.Reader.

Functions

accessor(fun)

@spec accessor(name() | (row() -> term())) :: (row() -> term())

The accessor fn row -> get(row, name) end, for a shape's x/y setters.

A function is returned unchanged, so a generator can normalize any accessor form through this one call.

column_types(source)

@spec column_types(source()) :: %{
  required(name()) => :time | :number | :category | :text
}

Every named column's type, read whole (spec/08 §6.3, spec/14 §2.3, #369): all non-nil values temporal is :time, all numbers :number, all strings :text, all atoms or booleans :category; a mixed or empty column is left out. A suggestion for a tool, never something the layer reads at render.

iex> rows = [%{t: ~U[2024-01-01 00:00:00Z], v: 1, k: :a, s: "x", m: 1},
...>         %{t: ~U[2024-01-02 00:00:00Z], v: 2.5, k: :b, s: "y", m: "two"}]
iex> Visualize.Data.Table.column_types(rows)
%{t: :time, v: :number, k: :category, s: :text}
iex> Visualize.Data.Table.column_types([])
%{}

get(row, name)

@spec get(map(), name()) :: term()

The value of column name in row, by either spelling of the name.

Map.get(row, name) when the key is present; otherwise an atom name is tried as its string and a string name as the atom key spelled the same. nil when neither is a key. No atom is ever created. A tuple row has no names and raises BadMapError.

key(row, name)

@spec key(map(), name()) :: name() | nil

The key of row that spells name: name itself when it is a key, else the key of the other spelling, nil when neither is.

A design names a field by its string (spec/14 §1.4, D-195) and a host's rows are often keyed by atoms: the atom key a string finds is remembered for the calling process, so a frame that reads the same field from every row scans one row's keys, not all of them. No atom is ever created or looked up by name — the remembered key is one a row held.

iex> Visualize.Data.Table.key(%{cpu: 1}, "cpu")
:cpu
iex> Visualize.Data.Table.key(%{"cpu" => 1}, :cpu)
"cpu"
iex> Visualize.Data.Table.key(%{cpu: 1}, "mem")
nil

named(tensor, names)

@spec named(source(), [name()]) ::
  {:ok, source()}
  | {:error,
     {:width, non_neg_integer(), non_neg_integer()} | {:rank, non_neg_integer()}}

A tensor's columns under names, in order: the column map rows/1 reads (spec/08 §6.4, D-214).

Column i of a rank-2 tensor is Enum.at(names, i), and a rank-1 tensor is one column. A tensor whose width is not length(names) is {:error, {:width, length(names), width}}, one of another rank {:error, {:rank, rank}}; nothing raises. Any other source is {:ok, source}, the same term, so every source can pass through.

iex> Visualize.Data.Table.named([%{x: 1}], ["x"])
{:ok, [%{x: 1}]}

rows(tensor)

@spec rows(source()) :: [row()]

The rows of a tabular source, as a list.

  • A list of rows is returned as-is: no copy, no traversal.
  • A keyword list whose values are equal-length columns is a list of columns; the rows are maps keyed by the keys. (table reads a list the same way, columns before rows.) A column is a list or a rank-1 Nx.Tensor (D-214).
  • A map whose values are equal-length columns is a map of columns, rows keyed likewise. Any other plain map raises ArgumentError.
  • An Nx.Tensor of rank 2 yields one tuple per row, in column order — the shape the default Visualize.Shape.Line accessors read; rank 1 yields its values. Any other rank raises ArgumentError. A tuple has no names: named/2 gives a tensor's columns some.
  • A Visualize.Data.Delivery is the rows of the source it carries (spec/08 §10.4).
  • Any other struct is read through Table.to_rows/1: rows keyed by the reader's column names as it spells them. A struct with no Table.Reader impl raises Protocol.UndefinedError; a reader answering :none raises ArgumentError.

The empty list, an empty map and an empty keyword list all yield [].

temporal?(string)

@spec temporal?(term()) :: boolean()

Whether a value is a point in time: a DateTime, Date or NaiveDateTime, or a string in ISO 8601 form that one of those parses (DateTime.from_iso8601/1 first, then NaiveDateTime.from_iso8601/1, then Date.from_iso8601/1).

time_columns(source)

@spec time_columns(source()) :: [name()]

The names of the columns whose values are all temporal.

The candidate names are every key of every map row, sorted by term order (a map's own key order is unspecified); a row without the key contributes nil. A column is temporal when it has at least one value that is not nil and every value is nil or satisfies temporal?/1. Detection reads the whole column, not a first row: every row of every column is visited and every string is parsed, so the cost is the size of the table. A source whose rows are tuples has no names and yields [].