Tablet (tablet v0.1.0)
View SourceA tiny tabular data renderer
This module renders tabular data as text for output to the console or any where else. Give it data in either of the following common tabular data shapes:
# List of matching maps (atom or string keys)
data = [
%{"id" => 1, "name" => "Puck"},
%{"id" => 2, "name" => "Nick Bottom"}
]
# List of matching key-value lists
data = [
[{"id", 1}, {"name", "Puck"}],
[{"id", 2}, {"name", "Nick Bottom"}]
]
Then call Tablet.puts/2
:
Tablet.puts(data)
#=> id name
#=> 1 Puck
#=> 2 Nick Bottom
While this shows a table with minimal styling, it's possible to create fancier tables with colors, borders and more.
Here are some of Tablet's features:
Kino.DataTable
-inspired API for ease of switching between Livebook and console output- Automatic column sizing
- Multi-column wrapping for tables with many rows and few columns
- Data eliding for long strings
- Customizable data formatting and styling
- Unicode support for emojis and other wide characters
IO.ANSI.ansidata/0
throughout- Small. No runtime dependencies.
While seemingly an implementation detail, Tablet's use of IO.ANSI.ansidata/0
allows a lot of flexibility in adding color and style to rendering. See IO.ANSI
and the section below to learn more about this cool feature if you haven't used
it before.
Example
Here's a more involved example:
iex> data = [
...> %{planet: "Mercury", orbital_period: 88},
...> %{planet: "Venus", orbital_period: 224.701},
...> %{planet: "Earth", orbital_period: 365.256},
...> %{planet: "Mars", orbital_period: 686.971}
...> ]
iex> formatter = fn
...> :__header__, :planet -> {:ok, "Planet"}
...> :__header__, :orbital_period -> {:ok, "Orbital Period"}
...> :orbital_period, value -> {:ok, "#{value} days"}
...> _, _ -> :default
...> end
iex> Tablet.render(data, keys: [:planet, :orbital_period], formatter: formatter)
...> |> IO.ANSI.format(false)
...> |> IO.chardata_to_string()
"Planet Orbital Period
" <>
"Mercury 88 days
" <>
"Venus 224.701 days
" <>
"Earth 365.256 days
" <>
"Mars 686.971 days
"
Note that normally you'd call IO.ANSI.format/2
without passing false
to
get colorized output and also call IO.puts/2
to write to a terminal.
Data formatting and column headers
Tablet naively converts data values and constructs column headers to
IO.ANSI.ansidata/0
. This may not be what you want. To customize this,
pass a 2-arity function using the :formatter
option. That function takes
the key and value as arguments and should return {:ok, ansidata}
. The
special key :__header__
is passed when constructing header row. Return
:default
to use the default conversion.
Styling
Various table output styles are supported by supplying a :style
function.
The following are included:
compact/3
- a minimal table style with underlined headers (default)markdown/3
- GitHub-flavored markdown table style
Ansidata
Tablet takes advantage of IO.ANSI.ansidata/0
everywhere. This makes it
easy to apply styling, colorization, and other transformations. However,
it can be hard to read. It's highly recommended to either call simplify/1
to
simplify the output for review or to call IO.ANSI.format/2
and then
IO.puts/2
to print it.
In a nutshell, IO.ANSI.ansidata/0
lets you create lists of strings to
print and intermix atoms like :red
or :blue
to indicate where ANSI escape
sequences should be inserted if supported. Tablet actually doesn't know what
any of the atoms means and passes them through. Elixir's IO.ANSI
module
does all of the work. If fact, if you find IO.ANSI
too limited, then you
could use an alternative like bunt and
include atoms like :chartreuse
which its formatter will understand.
Summary
Types
Column width values
Row-oriented data
Data formatter callback function
An atom or string key that identifies a data column
One row of data represented as a list of column ID, data tuples
One row of data represented in a map
Styling callback function
Styling context
Table renderer state
Functions
Trim or pad ansidata
Print a table to the console
Render a table as IO.ANSI.ansidata/0
Convenience function for simplifying ansidata
Calculate the visual length of an ansidata string
Types
@type column_width() :: pos_integer() | :default | :minimum | :expand
Column width values
Column widths may be passed via the :column_widths
options. The following
values may also be specified:
:default
- use the:default_column_width
. This is the same as not specifying the column width:minimum
- make the column minimally fit the widest data element:expand
- expand the column so that the table is as wide as the console
When multiple keys have the :expand
, they'll be allocated equal space.
@type data() :: [matching_map()] | [matching_key_value_list()]
Row-oriented data
@type formatter() :: (key(), any() -> {:ok, IO.ANSI.ansidata()} | :default)
Data formatter callback function
This function is used for conversion of tabular data to IO.ANSI.ansidata/0
.
The special key :__header__
is passed when formatting the column titles.
The callback should return {:ok, ansidata}
or :default
.
An atom or string key that identifies a data column
One row of data represented as a list of column ID, data tuples
One row of data represented in a map
@type style_function() :: (t(), styling_context(), [IO.ANSI.ansidata()] -> IO.ANSI.ansidata())
Styling callback function
Tablet makes calls to the styling function for each line in the table
starting with the header, then the rows (1 to N), and finally the footer. The
second parameter is the styling_context/0
. Users can supply additional
context via the :context
option when rendering the tables. This is the
means by which users can inform the styling function of potentially important
things like locale.
The third parameter is a list of IO.ANSI.ansidata/0
values. When
rendering multi-column tables (:wrap_across
set to greater than 1), each
item in the list corresponds to a set of columns. If your styling function
doesn't care about multi-column tables, then call List.flatten/1
on the
parameter.
The return value is always IO.ANSI.ansidata/0
. It should contain a final
new line since Tablet
doesn't add anything. Multiple lines can be returned
if borders or more room for text is needed.
When writing styling functions, it's recommended to pattern matching on the
context. Most of the time, you'll just need to know whether you're in the
:header
section or dealing with data rows. The context contains enough
information to do more complicated things like match on even or odd lines and
more if needed.
@type styling_context() :: %{ :line => pos_integer() | :header | :footer, :n => non_neg_integer(), optional(atom()) => any() }
Styling context
The context is a simple map with two fields that Tablet adds for conveying
the line that it's on. The key to remember is that the word "line" doesn't
necessarily represent one line of output. It's common for the :header
line
to output multiple lines for borders or titles. Each numbered line may result
in multiple lines after styling.
@type t() :: %Tablet{ column_widths: %{required(key()) => column_width()}, context: map(), data: [matching_map()], default_column_width: column_width(), formatter: formatter(), keys: nil | [key()], name: IO.ANSI.ansidata(), style: atom() | style_function(), total_width: non_neg_integer(), wrap_across: pos_integer() }
Table renderer state
Fields:
:data
- data rows:column_widths
- a map of keys to their desired column widths. Seecolumn_width/0
.:context
- user-provided context for styling the table:keys
- a list of keys to include in the table for each record. The order is reflected in the rendered table. Optional:default_column_width
- column width to use when unspecified in:column_widths
. Defaults to:minimum
:formatter
- a function to format the data in the table. The default is to convert everything to strings.:name
- the name or table title. This can be anyIO.ANSI.ansidata/0
value.:style
- one of the built-in styles or a function to style the table. The default is:compact
.:total_width
- the width of the console for use when expanding columns. The default is 0 to autodetect.:wrap_across
- the number of columns to wrap across in multi-column mode. The default is 1.
Functions
@spec left_trim_pad(IO.ANSI.ansidata(), pos_integer()) :: IO.ANSI.ansidata()
Trim or pad ansidata
This function is useful for styling output to fit data into a cell.
Print a table to the console
Call this to quickly print tabular data to the console.
This supports all of the options from render/2
.
Additional options:
:ansi_enabled?
- force ANSI output. If unset, the terminal setting is used.
@spec render( data(), keyword() ) :: IO.ANSI.ansidata()
Render a table as IO.ANSI.ansidata/0
This formats tabular data and returns it in a form that can be run through
IO.ANSI.format/2
for expansion of ANSI escape codes and then written to
an IO device.
Options:
:column_widths
- a map of keys to their desired column widths. Seecolumn_width/0
.:context
- optional context to be passed tyo the styling function:data
- tabular data:default_column_width
- default column width in characters:formatter
- if passing non-ansidata, supply a function to apply custom formatting:keys
- a list of keys to include in the table for each record. The order is reflected in the rendered table. Optional:name
- the name or table title. This can be anyIO.ANSI.ansidata/0
value. Not used by default style.:style
- seet:style/0
for details on styling tables:total_width
- the total width of the table if any of the:column_widths
is:expand
. Defaults to the console width if needed.:wrap_across
- the number of columns to wrap across in multi-column mode
@spec simplify(IO.ANSI.ansidata()) :: IO.ANSI.ansidata()
Convenience function for simplifying ansidata
This is useful when debugging or checking output for unit tests. It flattens the list, combines strings, and removes redundant ANSI codes.
@spec visual_length(IO.ANSI.ansidata()) :: non_neg_integer()
Calculate the visual length of an ansidata string
This function has simplistic logic to account for Unicode characters that typically render in the space of two characters when using a fixed width font.