tablo

//:source-highlighter: highlight.js //:highlightjs-languages: crystal :source-highlighter: pygments //:source-highlighter: rouge :pygments-style: lightbulb //::rouge-style: gruvbox-light //:rouge-style: monokai_sublime //:rouge-style: desert :toc: :toclevels: 4 :icons: font :imagesdir: docs/assets/images

== History

Tablo is a port of https://github.com/matt-harvey/tabulo[Matt Harvey's Tabulo] Ruby gem to the Crystal Language.

The first version of Tablo (v0.10.1) was released on November 30, 2021, in the context of learning the Crystal language, which explains its relative limitations compared to Tabulo v2.7, the current version at that time.

So this version of Tablo (v1.0) is a complete rewrite of the library.

Compared to the first version, it offers extended capabilities, sometimes at the cost of a modified syntax. It also offers new features, such as the ability to add a Summary table, powered by user-defined functions (such as sum, mean, etc.), the ability to process any Enumerable data, as well as elaborate layout possibilities: grouped columns, different types of headers (title, subtitle, footer), linked or detached border lines, etc.

While overall, Tablo remains, in terms of its functionalities, broadly comparable, with a few exceptions, to the Tabulo v3.0 version of Matt Harvey, the source code, meanwhile, has been deeply redesigned.

== Features

  • Presents a DRY API that is column-based, not row-based, meaning header and body rows are automatically in sync
  • Lets you set fixed column widths, then either wrap or truncate the overflow
  • Alternatively, “pack” the table so that columns are auto-sized to their contents
  • Cell alignment is configurable, but has helpful content-based defaults (numbers right, strings left)
  • Tabulate any Enumerable: the underlying collection need not be an array
  • Step through your table a row at a time, printing as you go, without waiting for the underlying collection to load. In other words, have a streaming interface for free.
  • Add optional title, subtitle and footer to your table
  • The header row can be repeated at arbitrary intervals
  • Newlines within cell content are correctly handled
  • Multibyte Unicode characters are correctly handled (needs the "uniwidth" library)
  • Option to preserve whole words when wrapping content
  • Apply colors and other styling to table content and borders, without breaking the table
  • Easily transpose the table, so that rows are swapped with columns
  • Choose from several border configurations, including predefined ones such as Markdown, Ascii (default), and user-defined ones.
  • Adjacent columns can be capped by a group header
  • A summary table can be added to apply user-defined functions to numeric values of a column

== Installation and use

. Add the dependency to your shard.yml: +

dependencies: tablo: gitlab: hutou/tablo

. Run shards install . And insert the line

[source,crystal] require "tablo"

at the beginnning of your app.

== Tutorial

In this tutorial, we'll start with a very simple example, which we'll build on as we go along to gradually discover all the possibilities offered by the Tablo library.

Here's a first look at how to use Tablo to lay out a simple table of integers.

//[source,crystal] [%linenums,crystal]

require "tablo"

table = Tablo::Table.new([1, 2, 3]) do |t| t.add_column("itself", &.itself) t.add_column(2, header: "Double") { |n| n * 2 } t.add_column(:column_3, header: "String") { |n| ('@'.ord + n).chr.to_s * n }
end

puts table

or

[%linenums,crystal]

require "tablo"

table = Tablo::Table.new([1, 2, 3]) table.add_column("itself", &.itself) table.add_column(2, header: "Double") {|n| n * 2} table.add_column(:column_3, header: "String") { |n| ('@'.ord + n).chr.to_s * n }

puts table

output:

+--------------+--------------+--------------+ | itself | Double | String | +--------------+--------------+--------------+ | 1 | 2 | A | | 2 | 4 | BB | | 3 | 6 | CCC | +--------------+--------------+--------------+

A great deal of information can already be extracted from this simple example. Let's list them:

  • The only parameter required to create the table is the data source (the array of integers), but to produce a result, you obviously need to add columns.
  • Any number of columns can be defined, each requiring an identifier and a proc for extracting data from the source and, if necessary, modifying its type and value.
  • The column identifier can be of type String, Integer or Symbol. By default, the column header takes the value of the identifier, unless the optional header parameter is used.
  • Columns are the same width.
  • We can see two types of row: header and body.
  • Columns of numbers are aligned to the right, and columns of text to the left, for both headers and body.
  • Default borders use the classic Ascii type.

=== Borders

Each border type is defined by a string of exactly 16 characters, which is then converted into 16 strings of up to 1 character each. The definition string can contain any character, but two of them have a special meaning: during conversion, the uppercase E is replaced by an empty string, and the uppercase S character is replaced by a space (a simple space may also be used, of course). + Please note that using the capital E character may cause alignment difficulties.

The first 9 characters define the junction or intersection of horizontal and vertical border lines.

[%autowidth] |=== |Index | Description

| 0 | Top left corner | 1 | Top middle junction | 2 | Top right corner | 3 | Middle left junction | 4 | Middle middle intersection | 5 | Middle right junction | 6 | Bottom left corner | 7 | Bottom middle junction | 8 | Bottom right corner |===

The next three characters define vertical separators in data rows.

[%autowidth] |=== |Index | Description

| 9 | Left vertical separator | 10 | Middle vertical separator | 11 | Right vertical separator |===

And finally, the last four characters define the different types of horizontal border, depending on the type of data row or types of adjacent data rows (Row type will be the subject of the next section).

[%autowidth] |=== |Index | Description

| 12 | Title, subtitle, footer | 13 | Group | 14 | Header | 15 | Body |===

To change a table's border type, simply assign the desired definition to the border parameter when initializing the table. This can be done in two ways, either by assigning the 16-character string directly, or by assigning the name of one of the 7 predefined borders :

[%autowidth] |=== |BorderName::Ascii |"\++++\++++\+++++\|\|\|----" |BorderName::ReducedAscii | "ESEESEESEESE----" |BorderName::Modern | "┌┬┐├┼┤└┴┘│││────" |BorderName::ReducedModern | "ESEESEESEESE────" |BorderName::Markdown | "\_\__\|\|\|\___\|\|\|\__-_" |BorderName::Fancy | "╭┬╮├┼┤╰┴╯│:│─−-⋅" |BorderName::Blank | "EEEEEEEEEEEEEEEE" |===

So, for example, to set the ReducedAscii border type, I can either:

  • use the predefined border name (Tablo::BorderName::ReducedAscii or :reduced_ascii):

[%linenums,crystal,start=3]

table = Tablo::Table.new([1, 2, 3], border: Tablo::Border.new(:reduced_ascii)) do |t|

or

  • use its definition string:

[%linenums,crystal,start=3]

table = Tablo::Table.new([1, 2, 3], border: Tablo::Border.new("ESEESEESEESE----")) do |t|

Output:


    itself         Double   String       

         1              2   A            
         2              4   BB           
         3              6   CCC          

=== Row types

==== Header and Body The Header and Body data row types form the basis of table formatting. Other types can be optionally added to establish the final layout: the Group row type and Heading row types (Title, SubTitle and Footer).

==== Group Adjacent columns can share a common header, above the column headers themselves. This common header constitutes a Group row type.

To create a Group row, simply define a common header after each set of adjacent columns to be grouped.

[%linenums,crystal]

require "tablo"

table = Tablo::Table.new([1, 2, 3]) do |t| t.add_column("itself", &.itself) t.add_column(2, header: "Double") {|n| n * 2} t.add_group("Numbers") t.add_column(:column_3, header: "String") { |n| ('@'.ord + n).chr.to_s * n }
t.add_group("Text") end

puts table

output:

+-----------------------------+--------------+ | Numbers | Text | +--------------+--------------+--------------+ | itself | Double | String | +--------------+--------------+--------------+ | 1 | 2 | A | | 2 | 4 | BB | | 3 | 6 | CCC | +--------------+--------------+--------------+

By default, Group headers are centered, but their alignment can be modified globally at table initialization time with the group_alignment parameter, or locally for a given group with the alignment parameter.

[%linenums,crystal,start=3]

table = Tablo::Table.new([1, 2, 3], group_alignment: Tablo::Justify::Left) do |t|

Output:

| Numbers | Text |

or

[%linenums,crystal,start=8]

t.add_group("Text", alignment: Tablo::Justify::Left)

Output:

| Numbers | Text |

Note that the group header can be empty, and that an empty group header is automatically created if the last column group is not specified.

Group and header are intimately linked and only separated by a horizontal line. For custom rendering, this line can be omitted by setting the Table omit_group_header_rule parameter to true.

[%linenums,crystal]

require "tablo"

table = Tablo::Table.new([1, 2, 3], omit_group_header_rule: true) do |t| t.add_column("itself", &.itself) t.add_column(2, header: "Double") {|n| n * 2} t.add_group("Numbers") t.add_column(:column_3, header: "String") { |n| ('@'.ord + n).chr.to_s * n }
t.add_column(:column_4, header: "Boolean") {|n| n.even?} t.add_group("Other data types") end

puts table

Output:

+-----------------------------+-----------------------------+ | Numbers | Other data types | | itself | Double | String | Boolean | +--------------+--------------+--------------+--------------+ | 1 | 2 | A | false | | 2 | 4 | BB | true | | 3 | 6 | CCC | false | +--------------+--------------+--------------+--------------+

==== Headings

A formatted table can optionally include a title, subtitle and footer. Each of these elements is of type Title, SubTitle or Footer, inherited from the abstract class Heading (see API).

By default, when the table is initialized, their value is nil, so nothing is displayed.

To display a title (or subtitle or footer), simply specify its value when initializing the table.

[%linenums,crystal,start=3]

table = Tablo::Table.new([1, 2, 3], title: Tablo::Title.new("Data types alignment")) do |t|

Output:

                  Data types alignment                    

+-----------------------------+-----------------------------+ | Numbers | Other data types | +--------------+--------------+--------------+--------------+ | itself | Double | String | Boolean | +--------------+--------------+--------------+--------------+ | 1 | 2 | A | false | | 2 | 4 | BB | true | | 3 | 6 | CCC | false | +--------------+--------------+--------------+--------------+

These elements can also be framed, possibly with line breaks before and after (defined in the Frame struct initializer as line_breaks_before and line_breaks_after with a value of 0).

The number of line breaks between adjacent elements is equal to the highest value between the current element's line_breaks_after and the next element's line_breaks_before.

[%linenums,crystal,start=3]

table = Tablo::Table.new([1, 2, 3], title: Tablo::Title.new("Data types alignment", frame: Tablo::Frame.new(line_breaks_before: 0, line_breaks_after: 2))) do |t|

Output:


+-----------------------------------------------------------+ | Data types alignment | +-----------------------------------------------------------+

+-----------------------------+-----------------------------+ | Numbers | Other data types | +--------------+--------------+--------------+--------------+ | itself | Double | String | Boolean | +--------------+--------------+--------------+--------------+ | 1 | 2 | A | false | | 2 | 4 | BB | true | | 3 | 6 | CCC | false | +--------------+--------------+--------------+--------------+

In summary, we have 6 types of data rows :

[%autowidth] |=== | Type | Description

| Header | Always displayed, unless header_frequency: is nil + or masked_headers: is true | Body | Always displayed | Group | Optional | Title | Optional | Subtitle| Optional | Footer | Optional

|===

=== Rules

Between the different types of rows, there are also different types of separator lines, whose format varies according to the types of rows they separate.

In the case of framed rows, for example, there may be a single dividing line, making the rows linked, or on the contrary, there may first be a closing line for the top row, possibly followed by line breaks before the opening line of the bottom row.

These horizontal rules are formatted by the horizontal_rule method of class Border.

=== Display frequency and repeated title

An important parameter in table initialization is header_frequency:

  • By default, it is set to 0, i.e. rows of data other than body are displayed only once, at the beginning for titles and headers, at the end for the footer.

[%linenums,crystal,start=3]

table = Tablo::Table.new([1, 2, 3], header_frequency: 0, title: Tablo::Title.new("Data types alignment", frame: Tablo::Frame.new(0, 2)), subtitle: Tablo::SubTitle.new("Only Booleans are centered by default"), footer: Tablo::Footer.new("End of page")) do |t|

Output:


+-----------------------------------------------------------+ | Data types alignment | +-----------------------------------------------------------+

        Only Booleans are centered by default            

+-----------------------------+-----------------------------+ | Numbers | Other data types | +--------------+--------------+--------------+--------------+ | itself | Double | String | Boolean | +--------------+--------------+--------------+--------------+ | 1 | 2 | A | false | | 2 | 4 | BB | true | | 3 | 6 | CCC | false | +--------------+--------------+--------------+--------------+ End of page

  • If set to nil, only body rows are displayed.

[%linenums,crystal,start=3]

table = Tablo::Table.new([1, 2, 3], header_frequency: nil,

Output:

+--------------+--------------+--------------+--------------+ | 1 | 2 | A | false | | 2 | 4 | BB | true | | 3 | 6 | CCC | false | +--------------+--------------+--------------+--------------+

  • If set to n (positive), group or column headers are repeated every n rows, as are footers, but titles and subtitles are not repeated.

[%linenums,crystal,start=3]

table = Tablo::Table.new([1, 2, 3], header_frequency: 2,

output:


+-----------------------------------------------------------+ | Data types alignment | +-----------------------------------------------------------+

        Only Booleans are centered by default            

+-----------------------------+-----------------------------+ | Numbers | Other data types | +--------------+--------------+--------------+--------------+ | itself | Double | String | Boolean | +--------------+--------------+--------------+--------------+ | 1 | 2 | A | false | | 2 | 4 | BB | true | +--------------+--------------+--------------+--------------+ End of page
+-----------------------------+-----------------------------+ | Numbers | Other data types | +--------------+--------------+--------------+--------------+ | itself | Double | String | Boolean | +--------------+--------------+--------------+--------------+ | 3 | 6 | CCC | false | | | | | | +--------------+--------------+--------------+--------------+ End of page

However, if the title repeated parameter is set to true, we obtain title and subtitle repetition.

[%linenums,crystal,start=3]

table = Tablo::Table.new([1, 2, 3], header_frequency: 2, title: Tablo::Title.new("Data types alignment", frame: Tablo::Frame.new(0, 2), repeated: true),

output:


+-----------------------------------------------------------+ | Data types alignment | +-----------------------------------------------------------+

        Only Booleans are centered by default            

+-----------------------------+-----------------------------+ | Numbers | Other data types | +--------------+--------------+--------------+--------------+ | itself | Double | String | Boolean | +--------------+--------------+--------------+--------------+ | 1 | 2 | A | false | | 2 | 4 | BB | true | +--------------+--------------+--------------+--------------+ End of page
+-----------------------------------------------------------+ | Data types alignment | +-----------------------------------------------------------+

        Only Booleans are centered by default            

+-----------------------------+-----------------------------+ | Numbers | Other data types | +--------------+--------------+--------------+--------------+ | itself | Double | String | Boolean | +--------------+--------------+--------------+--------------+ | 3 | 6 | CCC | false | | | | | | +--------------+--------------+--------------+--------------+ End of page

=== Extracting, Formatting and Styling

At the heart of Tablo's operation lies the Cell, a data structure containing all the elements required for display.

A cell, whether fed by data extracted from the source or directly from the code, can span several lines. Even if it initially occupies a single line, reducing the column width can result in a cell being displayed over several lines.

You can limit the number of lines displayed by using the header_wrap or body_wrap parameters when initializing the table (These 2 parameters are global to the table, and cannot be set on individual columns). If the whole cell content cannot be displayed due to this restriction, a special character (tilde by default) is inserted in the right-hand padding area of the last line of the cell (unless right padding is set to 0 for the column).

Note here the use of the row_divider_frequency parameter to separate body rows

[%linenums,crystal]

require "tablo"

table = Tablo::Table.new(["abc", "def\nghi\njkl\nmno\npqr", "xyz"], border: Tablo::Border.new("+++++++++|||---."), header_wrap: 2, body_wrap: 3, row_divider_frequency: 1) do |t| t.add_column("A\nfour\nlines\ncell", &.itself) end

puts table

output:

+--------------+ | A | | four ~| +--------------+ | abc | +..............+ | def | | ghi | | jkl ~| +..............+ | xyz | +--------------+

In addition, to have greater control over the line break, we can use the wrap_mode parameter to choose between Rune (Roughly equivalent to a character) and Word when cutting a line.

To use Tablo with non-Romanic languages, it is mandatory to use the naqviz/uni_char_width shard so that the width of each grapheme is correctly managed, without impacting alignment.

To do this, you need to:

. Add the dependencies to your shard.yml: + [source,yaml]

dependencies: tablo: gitlab: hutou/tablo uniwidth: github: naqvis/uni_char_width

. Run shards install . And insert the lines

[source,crystal] require "tablo" require "uniwidth"

at the beginning of your app.

==== Extracting

The cell value attribute contains the raw data.

If directly given as argument to Headings or Group, the cell is a TextCell as it is not related to source data.

If extracted from the source (body rows), the cell is of type DataCell, and the corresponding column header is also a DataCell (as it depends on the type of body value for alignment).

The cell_data attribute, specific to the DataCell type, provides access to the cell's coordinates (row_index and column_index), as well as the body_value. This information is used to activate conditional formatting and styling.

[source,crystal] struct CellData getter body_value, row_index, column_index def initialize(@body_value : CellType, @row_index : Int32, @column_index : Int32) end end

The type of value is Tablo::CellType, which is simply defined as an empty module restriction type:

[source,crystal] module Tablo::CellType end

This module is already included in all Crystal main scalar types. To use a (non or less used) scalar type or a user defined class or struct, it is mandatory to include it by reopening the class or struct.

For example, to allow a cell value to contain an array, we could do :

[source,crystal] class Array include Tablo::CellType end

For example:

[%linenums,crystal]

require "tablo"

table = Tablo::Table.new([[1, 2], [3, 4]]) do |t| t.add_column("itself") { |n| n } end

puts table

output:

+--------------+ | itself | +--------------+ | [1, 2] | | [3, 4] | +--------------+

==== Formatting

Formatting consists in applying a transformation to the raw data (the value) to obtain a character string ready to be displayed. The simplest transformation (which is also the one applied by default) is simply a call to the to_s method.

Using a proc formatter allows you to customize formatting in a variety of ways, from using sprintf formatting strings for numeric values to various String methods for text and specific Tablo::Util methods for both.

A formatter proc can take four forms : the first two apply equally to TextCell and DataCell and are applied unconditionally on value.

The first form expects one parameter (value) and the second two: value and width (column width).

Here is an example of the first form:

[%linenums,crystal]

require "tablo"

table = Tablo::Table.new([1, 2, 3]) do |t| t.add_column("itself", &.itself) t.add_column(2, header: "Double") { |n| n * 2 } t.add_column(3, header: "Float", header_formatter: ->(value : Tablo::CellType) { value.as(String).upcase }, body_formatter: ->(value : Tablo::CellType) { "%.3f" % value.as(Float) }) { |n| n ** 0.5 } end

puts table

output:

+--------------+--------------+--------------+ | itself | Double | FLOAT | <1> +--------------+--------------+--------------+ | 1 | 2 | 1.000 | | 2 | 4 | 1.414 | | 3 | 6 | 1.732 | +--------------+--------------+--------------+

<1> Note that the FLOAT column is aligned to the right, as its alignment is governed by the type of value, which is a float.

If the formatting were done directly at the data extraction level, value would be of type String and column would be aligned to the left.

[%linenums,crystal]

require "tablo"

table = Tablo::Table.new([1, 2, 3]) do |t| t.add_column("itself", &.itself) t.add_column(2, header: "Double") { |n| n * 2 } t.add_column(3, header: "Float", header_formatter: ->(value : Tablo::CellType) { value.as(String).upcase }) { |n| "%.3f" % (n ** 0.5) } end

puts table

output:

+--------------+--------------+--------------+ | itself | Double | FLOAT | +--------------+--------------+--------------+ | 1 | 2 | 1.000 | | 2 | 4 | 1.414 | | 3 | 6 | 1.732 | +--------------+--------------+--------------+

To illustrate the 2nd form, we will use the Tablo::Util.stretch method, which can be useful on groups or headings.

[%linenums,crystal]

require "tablo"

table = Tablo::Table.new([1, 2, 3]) do |t| t.add_column("itself", &.itself) t.add_column(2, header: "Double") { |n| n * 2 } t.add_group("Numbers", formatter: ->(value : Tablo::CellType, width : Int32) { Tablo::Util.stretch(value.as(String), width, ' ') }) t.add_column(:column_3, header: "String") { |n| ('@'.ord + n).chr.to_s * n } t.add_column(:column_4, header: "Boolean") { |n| n.even? } t.add_group("Other data types") end

puts table

output:

+-----------------------------+-----------------------------+ | N u m b e r s | Other data types | +--------------+--------------+--------------+--------------+ | itself | Double | String | Boolean | +--------------+--------------+--------------+--------------+ | 1 | 2 | A | false | | 2 | 4 | BB | true | | 3 | 6 | CCC | false | +--------------+--------------+--------------+--------------+

Form 3 and form 4 apply only on DataCell cell types, as they use the cell_data parameter to conditionnally format the value.

Here is an exemple of form 3 with another method from Tablo::Util, which use the column_index as formatting condition.

[%linenums,crystal]

require "tablo"

table = Tablo::Table.new([-30.00001, -3.14159, 0.0, 1.470001, 5.78707, 10.0], body_formatter: ->(value : Tablo::CellType, cell_data : Tablo::CellData) { case cell_data.column_index when 1 then Tablo::Util.dot_align(value.as(Float), 4, Tablo::Util::DotAlign::Empty) when 2 then Tablo::Util.dot_align(value.as(Float), 4, Tablo::Util::DotAlign::Blank) when 3 then Tablo::Util.dot_align(value.as(Float), 4, Tablo::Util::DotAlign::Dot) when 4 then Tablo::Util.dot_align(value.as(Float), 4, Tablo::Util::DotAlign::DotZero) else value.as(Float).to_s end }) do |t| t.add_column("itself", &.itself) t.add_column("1 - Empty", &.itself) t.add_column("2 - Blank", &.itself) t.add_column("3 - Dot", &.itself) t.add_column("4 - DotZero", &.itself) end

puts table

output:

+--------------+--------------+--------------+--------------+--------------+ | itself | 1 - Empty | 2 - Blank | 3 - Dot | 4 - DotZero | +--------------+--------------+--------------+--------------+--------------+ | -30.00001 | -30 | -30 | -30. | -30.0 | | -3.14159 | -3.1416 | -3.1416 | -3.1416 | -3.1416 | | 0.0 | | 0 | 0. | 0.0 | | 1.470001 | 1.47 | 1.47 | 1.47 | 1.47 | | 5.78707 | 5.7871 | 5.7871 | 5.7871 | 5.7871 | | 10.0 | 10 | 10 | 10. | 10.0 | +--------------+--------------+--------------+--------------+--------------+

Incidentally, this last example displays all the formatting possibilities of the Tablo::Util.dot_align method.

Compared to the third form, form 4 also allows the use of the width value. + Its usefulness seems less obvious, however.

Overview of the 4 different forms of formatter procs:

[%autowidth] |=== | Forms of formatter procs | Parameter and types, in order

| 1st form | value : Tablo::CellType + used by: TextCell or DataCell | 2nd form | value : Tablo::CellType, width : Int32 + used by: TextCell or DataCell | 3rd form | value : Tablo::CellType, cell_data : Tablo::CellData + used by: DataCell | 4th form | value : Tablo::CellType, cell_data : Tablo::CellData, width : Int32 + used by: DataCell |===

==== Styling

When it comes to terminal styling, the possibilities are limited, especially as they depend on the terminal's capabilities. There are therefore 2 complementary ways of proceeding:

  • play with the mode (underlined, bold, italic...)
  • use color

This can be done using ANSI code sequences, or preferably, using the colorize module of the standard library.

In this section, we'll be using color, altered characters and graphic borders with the Fancy border type. Output will therefore be presented as SVG images, so as to guarantee perfect rendering, whatever the medium used for display.

For styling, there are 5 forms of procs.

The first uses only the (formatted) content as a parameter, and therefore does not allow conditional styling.

Let's look at a simple example, with yellow borders and blue headers.

[%linenums,crystal]

require "tablo" require "colorize"

table = Tablo::Table.new([1, 2, 3], border: Tablo::Border.new(:fancy, styler: ->(border_char : String) { border_char.colorize(:yellow).to_s }), header_styler: ->(content : String) { content.colorize(:blue).to_s }) do |t| t.add_column("itself", &.itself) t.add_column(2, header: "Double") { |n| n * 2 } t.add_column(:column_3, header: "String") { |n| ('@'.ord + n).chr.to_s * n } t.add_column(:column_4, header: "Boolean") { |n| n.even? } end

puts table

image:styling_first_form.svg[width="560",caption="Styling first form:"]

Cool! Let's do now some conditional styling, painting in bold green all values greater than 2 in all numeric columns and underlining the true boolean value in fourth column: this is the third form of styling.

Just add, at the table level before header_styler, the following lines :

[%linenums,crystal,start=8]

body_styler: ->(value : Tablo::CellType, content : String) { case value when Int32 value > 2 ? content.colorize.fore(:green).mode(:bold).to_s : content else value == true ? content.colorize.mode(:underline).to_s : content end },

image:styling_third_form.svg[width="560",caption="Styling third form:"]

Let's end with a final example, with a black-and-white look: how do you display rows alternately in light gray (with a bit of italics) and dark gray to make them easier to read?

This would be the 4th form.

[%linenums,crystal]

require "tablo" require "colorize"

table = Tablo::Table.new([1, 2, 3, 4, 5], title: Tablo::Title.new("My black and white fancy table", frame: Tablo::Frame.new), footer: Tablo::Footer.new("End of data", frame: Tablo::Frame.new), border: Tablo::Border.new(:fancy, ->(border_char : String) { border_char.colorize(:light_gray).to_s }), body_styler: ->(_value : Tablo::CellType, cell_data : Tablo::CellData, content : String) { if cell_data.row_index.even? "\e[3m" + content.colorize(:light_gray).to_s + "\e[0m" <1> else content.colorize.fore(:dark_gray).mode(:bold).to_s end }, header_styler: ->(content : String) { content.colorize.mode(:bold).to_s }) do |t| t.add_column("itself", &.itself) t.add_column(2, header: "Double") { |n| n * 2 } t.add_column(:column_3, header: "String") { |n| ('@'.ord + n).chr.to_s * n } t.add_column(:column_4, header: "Boolean") { |n| n.even? } end

puts table

<1> From version 1.10 onwards, Crystal does support italic mode, and the use of ANSI sequences is given here simply as an example.

image:styling_fourth_form.svg[width="560",caption="Styling fourth form:"]

Overview of the 5 different forms of styler procs:

[%autowidth] |=== | Forms of styler procs | Parameter and types, in order

| 1st form | (formatted) content : String + used by: Border, TextCell or DataCell | 2nd form | (formatted) content : String, line_index : Int32 + used by: TextCell | 3rd form | value : Tablo::CellType, (formatted) content : String + used by: DataCell | 4th form | value : Tablo::CellType, cell_data : Tablo::CellData, (formatted) content : String + used by: DataCell | 5th form | value : Tablo::CellType, cell_data : Tablo::CellData, (formatted) content : String, line_index : Int32 + used by: DataCell |===

=== Packing

In the previous examples, the notion of column width was used. For a better understanding, the diagram below highlights the structure of a column.

image:column_layout.svg[width="560"]

As we saw at the start of this tutorial, by default, all columns have the same width, i.e. 12 characters.

Of course, this value can be modified globally when initializing the table, or individually when defining columns. The same applies to left and right padding, as well as to the padding character (a space, by default).

The border width is 1 character maximum, but can be 0 (i.e. no border) if the letter E is used in the border definition string.

The pack method is a welcome aid to table formatting. It accepts 3 parameters, all optional:

  • width: total width required for the formatted table. If no width is given and if the value of parameter Config.terminal_capped_width is true, the value of width is read from the size of the terminal, otherwise its value is nil and in that case, only starting_widths == AutoSized has an effect.

  • starting_widths : column widths taken as starting point for resizing, possible values are :

** Current : resizing starts from columns current width

** Initial : current values are reset to their initial values, at column definition time

** AutoSized : current values are set to their 'best fit' values, ie they are automatically adapted to their largest content

  • except: column or array of columns excluded from being resized, identified by their label

The following examples will illustrate the behaviour of the different parameters values, starting from the 'standard' one, with all column widths to their default value : 12 characters.

[%linenums,crystal]

require "tablo"

data = [ [1, "Box", "Orange", "Elephant", "Mont St Michel"], ] table = Tablo::Table.new(data) do |t| t.add_column("Primes") { |n| n[0].as(Int32) } t.add_column(2, header: "Things") { |n| n[1].as(String) } t.add_column(:fruits, header: "Fruits") { |n| n[2].as(String) } t.add_column(3, header: "Animals") { |n| n[3].as(String) } t.add_column("Famous\nSites") { |n| n[4].as(String) } end

puts table puts "table width = #{table.total_table_width}"

Table standard output, using default width values, without any packing:

puts table +--------------+--------------+--------------+--------------+--------------+ | Primes | Things | Fruits | Animals | Famous | | | | | | Sites | +--------------+--------------+--------------+--------------+--------------+ | 1 | Box | Orange | Elephant | Mont St | | | | | | Michel | +--------------+--------------+--------------+--------------+--------------+ table width = 76

Using default pack parameters (ie: none !), we get an optimal packing

puts table.pack +--------+--------+--------+----------+----------------+ | Primes | Things | Fruits | Animals | Famous | | | | | | Sites | +--------+--------+--------+----------+----------------+ | 1 | Box | Orange | Elephant | Mont St Michel | +--------+--------+--------+----------+----------------+ table width = 56

But using pack with same table width (56) on initial widths values gives a significantly poorer result

puts table.pack(56, starting_widths: Tablo::StartingWidths::Initial) +----------+----------+----------+----------+----------+ | Primes | Things | Fruits | Animals | Famous | | | | | | Sites | +----------+----------+----------+----------+----------+ | 1 | Box | Orange | Elephant | Mont St | | | | | | Michel | +----------+----------+----------+----------+----------+ table width = 56

This is due to the way Tablo reduces or increases column size. See the description of the algorithm in the API section for Table.pack.

Using the width parameter, any table width can be obtained, by reducing or increasing the width of each column progressively to reach the desired table width

puts table.pack(30) +-----+-----+-----+-----+----+ | Pri | Thi | Fru | Ani | Fa | | mes | ngs | its | mal | mo | | | | | s | us | | | | | | Si | | | | | | te | | | | | | s | +-----+-----+-----+-----+----+ | 1 | Box | Ora | Ele | Mo | | | | nge | pha | nt | | | | | nt | St | | | | | | Mi | | | | | | ch | | | | | | el | +-----+-----+-----+-----+----+ table width = 30

or

puts table.pack(90) +----------------+-----------------+-----------------+-----------------+-----------------+ | Primes | Things | Fruits | Animals | Famous | | | | | | Sites | +----------------+-----------------+-----------------+-----------------+-----------------+ | 1 | Box | Orange | Elephant | Mont St Michel | +----------------+-----------------+-----------------+-----------------+-----------------+ table width = 90

There is, however, a limit to the reduction: each column must be able to accommodate at least one character. Here, we're asking for a table width of 15, but the minimum size to respect this rule is 21!

puts table.pack(15) +---+---+---+---+---+ | P | T | F | A | F | | r | h | r | n | a | | i | i | u | i | m | | m | n | i | m | o | | e | g | t | a | u | | s | s | s | l | s | | | | | s | S | | | | | | i | | | | | | t | | | | | | e | | | | | | s | +---+---+---+---+---+ | 1 | B | O | E | M | | | o | r | l | o | | | x | a | e | n | | | | n | p | t | | | | g | h | S | | | | e | a | t | | | | | n | M | | | | | t | i | | | | | | c | | | | | | h | | | | | | e | | | | | | l | +---+---+---+---+---+ table width = 21

If, with the parameter starting_widths == Startingwidths::AutoSized by default (set by Config.starting_widths), the pack method automatically adapts the width of columns to their largest content (body or header) before resizing, this requires you to go through the entire source dataset, which can be costly in terms of performance in some cases.

This behavior can be avoided, but possibly with a loss of quality, by changing the value of starting_widths to Current or Initial. In this case, however, a value for width must be supplied, either directly or by reading the terminal size, otherwise pack will become a non-operation.

Finally, using the except parameter, you can temporarily freeze the size of one or more columns at their current value, so that they are excluded from resizing.

table.pack puts table.pack(80,except: [2, :fruits]) +-------------------+--------+--------+-------------------+--------------------+ | Primes | Things | Fruits | Animals | Famous | | | | | | Sites | +-------------------+--------+--------+-------------------+--------------------+ | 1 | Box | Orange | Elephant | Mont St Michel | +-------------------+--------+--------+-------------------+--------------------+ table width = 80

Note that when a column is created using the add_column method, the width is stored in 2 different attributes: the current value and the initial value (never subsequently modified).

The pack method simply modifies the current column width value, and the to_s method then takes care of the layout, and we can save the table in its output layout in a string with :

[source,crystal] formatted_table = table.to_s

It is therefore equivalent to write :

[source,crystal] table.pack puts table

or

[source,crystal] puts table.pack

To sum up:

[autowidth] |=== | Type of call | Results (with StartingWidths::AutoSized as default and no columns excluded)

| table.pack a|

  • Automatically adapts columns to their largest content
  • Modifies current values of column width | table.pack(40) a|
  • Automatically adapts columns to their largest content +
  • Modifies current values of column width +
  • Reduces or increases column widths to meet total table size requirements | table.pack(starting_widths: StartingWidths::Current) a|
  • No-op | table.pack(40, starting_widths: StartingWidths::Current) a|
  • Reduces or increases column widths to meet total table size requirements | table.pack(starting_widths: StartingWidths::Initial) a|
  • Only resets current column values to their initial values, no packing is done | table.pack(40, starting_widths: StartingWidths::Initial) a|
  • Resets current column values to their initial values
  • Reduces or increases column widths to meet total table size requirements |===

=== Summary

The Tablo library offers a very basic yet useful summary method. This method must be considered as experimental however, as it obviously needs improvement. So, its usage may change in the future.

At present, it can be used to perform calculations on individual columns of data, and, often at the cost of some code duplication, to perform calculations between columns. Clearly not a DRY API!

Here's an example of how it works now and what it can do for you:

[%linenums,crystal]

require "tablo"

record Entry, product : String, price : Int32 invoice = [ Entry.new("Laptop", 98000), Entry.new("Printer", 15499), Entry.new("Router", 9900), Entry.new("Accessories", 6450), ]

tbl = Tablo::Table.new(invoice, omit_last_rule: true, title: Tablo::Title.new("Invoice")) do |t| t.add_column("Product", &.product) t.add_column("Price", body_formatter: ->(value : Tablo::CellType) { "%.2f" % (value.as(Int32) / 100) }, &.price) t.add_column(:tax, header: "Tax (20%)", body_formatter: ->(value : Tablo::CellType) { "%.2f" % (value.as(Int32) / 100) }) { |n| n.price * 20 // 100 } end

tbl.summary( { "Product" => { proc: [ {1, ->(_ary : Array(Tablo::CellType)) { "Total excl.".as(Tablo::CellType) }}, {2, ->(_ary : Array(Tablo::CellType)) { "VAT (Total)".as(Tablo::CellType) }}, {3, ->(_ary : Array(Tablo::CellType)) { "Total incl.".as(Tablo::CellType) }}, ], }, "Price" => { body_formatter: ->(value : Tablo::CellType) { value.nil? ? "" : "%.2f" % (value.as(Int32) / 100) }, proc: [ {1, ->(ary : Array(Tablo::CellType)) { (ary.map &.as(Int32)).sum.as(Tablo::CellType) }}, {3, ->(ary : Hash(Tablo::LabelType, Array(Tablo::CellType))) { sum_price = (ary["Price"].map &.as(Int32)).sum sum_tax = (ary[:tax].map &.as(Int32)).sum (sum_price + sum_tax).as(Tablo::CellType) }}, ], }, :tax => { body_formatter: ->(value : Tablo::CellType) { value.nil? ? "" : "%.2f" % (value.as(Int32) / 100) }, proc: {2, ->(ary : Array(Tablo::CellType)) { (ary.map &.as(Int32)).sum.as(Tablo::CellType) }}, }, }, masked_headers: true )

puts tbl puts tbl.summary

Output:

                 Invoice                   

+--------------+--------------+--------------+ | Product | Price | Tax (20%) | +--------------+--------------+--------------+ | Laptop | 980.00 | 196.00 | | Printer | 154.99 | 30.99 | | Router | 99.00 | 19.80 | | Accessories | 64.50 | 12.90 | +--------------+--------------+--------------+ | Total excl. | 1298.49 | | | VAT (Total) | | 259.69 | | Total incl. | 1558.18 | | +--------------+--------------+--------------+

Let's take a closer look at the source code.

The first part - the creation of the main table - calls for no particular comment (except perhaps the use of a more realistic data source than the previous arrays of integers!)

Calling the Summary method, with its 2 parameters, creates a new Table instance, and calling the same method without arguments returns this same instance, ready to be displayed.

The first parameter (summary_def) defines all the calculations to be performed on the data, as well as their layout.

The type of summary_def is : Hash(LabelType, NamedTuple(...)). The hash key is therefore a column identifier (LabelType is an alias of String | Symbol | Int32).

The NamedTuple may have up to 8 entries :

[%autowidth] |=== | Hash key | Type of hash value

| header | String | header_alignment | Justify | header_formatter | DataCellFormatter | header_styler | DataCellStyler | body_alignment | Justify | body_formatter | DataCellFormatter | body_styler| DataCellStyler | proc | SummaryProcs |===

The latter - SummaryProcs- is a tad complex and can take several forms. Basically, it is a tuple of 2 elements :

  • An Int32, which indicates the position (line) of the proc result in the column
  • A proc or an array of procs, which performs the calculation and expects as parameter either an Array(CellType) or a Hash(LabelType, Array(CellType))

The second parameter (options) ...

Outch!

Looking again at the source code, we see that :

  • The :tax column has 2 entries, of type Tuple:

** body_formatter: and its associated proc which performs the conversion from cents to currency units, and checks that the cell is not nil (this is necessary, as of the 3 summary lines, the 1st and 3rd are not fed).

** proc: a tuple defined by the number of the summary line to be fed, and the proc performing the calculation. The latter takes as parameter a CellType array (the "Tax (20%)" data column), converts it to an array of integers before summing and converting the result to CellType.

//// A summary table can be created from the data in the main table. It will have the same number of columns, and one or more functions can be applied to each of these columns, particularly for numerical values: statistical functions (sum, average, maximum, etc.) or any other type of calculation.

Let's take a simple example to illustrate the syntax involved. [source,crystal]

require "tablo"

table = Tablo::Table.new([1, 2, 3]) do |t| t.add_column("itself", &.itself) t.add_column(2, header: "Double") { |n| n * 2 } t.add_column(:column_3, header: "String") { |n| n.even?.to_s } end puts table

table.summary( { 2 => { header: "Sum", proc: {1, ->(ary : Tablo::SourcesCurrentColumn) { (ary.select(&.is_a?(Number)) .map &.as(Number)).sum.as(Tablo::CellType) }}, }, }) puts table.summary

[literal]

+--------------+--------------+--------------+ | itself | Double | String | +--------------+--------------+--------------+ | 1 | 2 | false | | 2 | 4 | true | | 3 | 6 | false | +--------------+--------------+--------------+ +--------------+--------------+--------------+ | | Sum | | +--------------+--------------+--------------+ | | 12.0 | | +--------------+--------------+--------------+

The first parameter expected by the summary method is a hash, and its type is Hash(LabelType, NamedTuple(...)). The named tuple allowed keys are :

  • header
  • header_alignment
  • header_formatter
  • header_styler
  • body_alignment
  • body_formatter
  • body_styler
  • proc

typeof(summary_def) # => Hash(Int32, NamedTuple(header: String, proc: Tuple(Int32, Proc(Array(Tablo::CellType), Tablo::CellType))))

////

//// === Transpose &&&&&&&&&&&&&&&&&&&& to be continued &&&&&&&&&&&&&&&&&&&&&&

////

== Complete API

=== Tablo internals Table definition is largely based on default values, which can be modified via named parameters if required.

Most of the parameters defined when initializing a table are taken over by default, if appropriate, when creating columns.

Tablo features a column-based rather than row-based API, which means that header and body lines are automatically synchronized.

At the heart of Tablo's operation lies the cell, a data structure containing all the elements required for display. There are 2 different types of cell:

  • Those containing source or source-dependent data, the DataCell type (for Header and Body rows)

  • Text cells, the TextCell type (for Group and Headings)

They differ mainly in 2 exclusive attribute types:

  • RowType for TextCell cells

  • CellData for DataCell cells

Both have the value attribute, which contains the raw data extracted from source. Its type is Tablo::CellType

=== class Table

The Table class is Tablo's main class. Its initialization defines the main parameters governing the overall operation of the Tablo library, in particular the data source and column definitions.

==== Method initialize

Returns an instance of Table(T)

[%autowidth] |=== | Used constants | Default value

| DEFAULT_HEADING_ALIGNMENT | Justify::Center | DEFAULT_FORMATTER | -&gt;(c : CellType) { c.to_s } | DEFAULT_STYLER | -&gt;(s : String) { s } | DEFAULT_DATA_DEPENDENT_STYLER | -&gt;(_c : CellType, s : String) { s } |===

[%autowidth] //[cols="1,2,3"] |=== | Parameters | Type and default value | Comments

3+^| Mandatory positional parameter

|sources | Enumerable(T) | Can be any Enumerable data type + (Range is currently (Crystal 1.9.2) not correctly supported in this context: use Range.to_a instead)

3+^| Optional named parameters, with default values

| title, subtitle, footer | Title, SubTitle and Footer, respectively initialized by Title.new, SubTitle.new and Footer.new | Initializing these classes without any argument set their value to nil, so there is nothing to display

| border | Border = Config.border | Default Config.border sets border type to BorderName::Ascii. Other BorderName are ReducedAscii, Modern, ReducedModern, Markdown, Fancy and Blank. Border type may also be initialized directly by a string of 16 characters

| group_alignment | Justify = DEFAULT_HEADING_ALIGNMENT |

| group_formatter | TextCellFormatter = DEFAULT_FORMATTER |

| group_styler | TextCellStyler = DEFAULT_STYLER |

| header_alignment | Justify? = nil | With nil as default, alignment depends on the type of the related body cell value

| header_formatter | DataCellFormatter = DEFAULT_FORMATTER |

| header_styler | DataCellStyler = DEFAULT_DATA_DEPENDENT_STYLER |

| body_alignment | Justify? = nil | With nil as default, alignment depends on the type of the body cell value

| body_formatter | DataCellFormatter = DEFAULT_FORMATTER |

| body_styler | DataCellStyler = DEFAULT_DATA_DEPENDENT_STYLER |

| left_padding | Int32 = 1 | Permitted range of values governed by Config.padding_width_range in the check_padding method (raises InvalidValue runtime exception if value not in range)

| right_padding | Int32 = 1 | Permitted range of values governed by Config.padding_width_range in the check_padding method (raises InvalidValue runtime exception if value not in range)

| padding_character | String = " " | The check_padding_character auxiliairy method ensures the padding_character string size is only one, raises an InvalidValue runtime exception otherwise

| truncation_indicator| String = "~" | The check_truncation_indicator auxiliairy method ensures the truncation_indicator string size is only one, raises an InvalidValue runtime exception otherwise

| width | Int32 = 12 | Global width value for columns. Permitted range of values governed by Config.column_width_range in the check_width auxiliary method (raises InvalidValue runtime exception unless value in range)

| header_frequency | Int32? = 0 a|

  • If set to 0, rows of data other than body are displayed only once, at the beginning for titles and headers, at the end for the footer.
  • If set to n (positive), group or column headers are repeated every n rows, as are footers, but titles and subtitles are not repeated (unless title repeated attribute is set to true)
  • If set to nil, only body rows are displayed.

Permitted range of values governed by Config.header_frequency_range in the check_header_frequency auxiliary method (raises InvalidValue runtime exception unless value in range or nil)

| row_divider_frequency | Int32? = nil | Permitted range of values governed by Config.row_divider_frequency_range in the check_row_divider_frequency auxiliary method (raises InvalidValue runtime exception unless value in range or nil)

| wrap_mode | WrapMode = WrapMode::Word a| WrapMode enum defines 2 modes :

  • Rune : long lines can be cut between characters (graphemes)
  • Word : long lines can be cut between words only

| header_wrap | Int32? = nil | Permitted range of values governed by Config.header_wrap_range in the check_header_wrap auxiliary method (raises InvalidValue runtime exception unless value in range or nil)

| body_wrap | Int32? = nil | Permitted range of values governed by Config.body_wrap_range in the check_body_wrap auxiliary method (raises InvalidValue runtime exception unless value in range or nil)

| masked_headers| Bool = false | If true, groups and column headers are not displayed (this does not prevent display of title, subtitle and footer)

| omit_group_header_rule | Bool = false | If true, the rule between Group and Header rows is not displayed. This is useful for headers custom rendering.

| omit_last_rule | Bool = false | If true, the closing rule of table is not displayed. This is useful for custom rendering (and notably for Detail and Summary tables joining) |===

==== Method add_column

Returns an instance of Column(T)

[%autowidth] |=== | Parameters | Type and default value | Comments

3+^| Mandatory positional parameter

|label | LabelType | The label identifies the column (LabelType is an alias of Int32 \| Symbol \| String)

3+^| Optional named parameters, with default values

| header | String = label.to_s | Can be an empty string

| header_alignment | Justify? = header_alignment | By default, inherits from table header_alignment initializer

| header_formatter | DataCellFormatter = header_formatter | By default, inherits from table header_formatter initializer

| header_styler | DataCellStyler = header_styler | By default, inherits from table header_styler initializer

| body_alignment | Justify? = body_alignment | By default, inherits from table body_alignment initializer

| body_formatter | DataCellFormatter = body_formatter | By default, inherits from table body_formatter initializer

| body_styler | DataCellStyler = body_styler | By default, inherits from table body_styler initializer

| left_padding | Int32 = left_padding | By default, inherits from table left_padding initializer

| right_padding | Int32 = right_padding | By default, inherits from table right_padding initializer

| padding_character | String = padding_character | By default, inherits from table padding_character initializer

| width | Int32 = width | By default, inherits from table width initializer

| truncation_indicator | String = truncation_indicator | By default, inherits from table truncation_indicator initializer

| wrap_mode | WrapMode = wrap_mode | By default, inherits from table wrap_mode initializer

| &extractor | (T \| Int32) -&gt; CellType | Captured block for extracting data from source |===

==== Method add_group

Returns an instance of TextCell

[%autowidth] |=== | Parameters | Type and default value | Comments

3+^| Mandatory positional parameter

|label | LabelType | The label identifies the group.

3+^| Optional named parameters, with default values

| header | String = label.to_s | Can be an empty string

| alignment | Justify = group_alignment | By default, inherits from table group_alignment initializer

| formatter | TextCellFormatter = group_formatter | By default, inherits from table group_formatter initializer

| styler | TextCellFormatter = group_styler | By default, inherits from table group_styler initializer

| padding_character | String = padding_character | By default, inherits from table padding_character initializer

| truncation_indicator | String = truncation_indicator | By default, inherits from table truncation_indicator initializer

| wrap_mode | WrapMode = wrap_mode | By default, inherits from table wrap_mode initializer |===

==== Method reset_sources Replaces existing data source with a new one and returns it + (This could be seen as a hack to do some special form of pagination !)

[%autowidth] |=== | Parameters | Type and default value | Comments

3+^| Mandatory positional parameter

| src | Enumerable(T) | The new data source must also be an Enumerable(T) |===

==== Method to_s

Returns the table as a formatted string

//// ==== Method each

Returns successive formatted rows, with all corresponding headers and footers,
according to the parameters defined.

In fact,
[source,crystal]
----
table.each do |r|
puts r
end
----
is the same as

[source,crystal]
----
puts table
----

==== Method `pack`

Returns `self` (the current Table instance) after modifying its column widths


[cols="1,2,3"]
|===
| Parameters | Type and default value | Comments

3+^| _All named parameters are optional, with default values_


| `width`   |   `Int32?` = `nil` |  The requested total table width.

If `width` is `nil` and `Config.terminal_capped_width` is `true` (and output
        not redirected), `width` finally takes the value of the terminal size.

| `starting_widths`   |   `StartingWidths = Config.starting_widths` a|
`Starting_widths` allows you to specify the starting point for resizing :
  • either from the current column width value (StartingWidths::Current)

  • or from its initial value (StartingWidths::Initial)

or ignore it and directly perform optimized resizing (StartingWidths::AutoSized)

| `except`   |   `Except?` = `nil` | Column identifier or array of column
identifiers to be excluded from packing (`Except` is an alias of `LabelType \|
        Array(LabelType)`)
|===

===== Description of the packing algorithm
The resizing algorithm is actually quite simple.

if the final value of the  `width` parameter is not `nil`, it first compares
the table's current width with the requested width, to determine whether this
is a reduction or an increase in size. Then, depending on the case, either the
widest column is reduced, or the narrowest increased, in steps of 1, until the
requested table width is reached.

This explains why the final result of resizing depends on the starting column
widths.

==== Method `summary`

==== Method `horizontal_rule`

==== Method `transpose`

==== Method `total_table_width`


=== abstract class Heading : concrete classes Title, SubTitle and Footer

                             `Title`, `SubTitle` and `Footer` structs are inherited from the `Heading`
                             abstract struct.

                             ==== abstract struct Heading

                             ===== Method `framed?`

                             Returns `true` if `Title`, `SubTitle` or `Footer` contents are framed, `false`
                             otherwise.

                             ==== struct Frame
                             The `Frame` struct is used to add a frame to `Title`, `SubTitle` or `Footer`.

                             ===== Method `initialize`

                             Returns an instance of `Frame`

                             [cols="1,2,3"]
                             |===
                             | Parameters | Type and default value | Comments
                             | `line_breaks_before` |  `Int32` = 0 | The count of line breaks to emit
                             before displaying framed contents
                             | `line_breaks_after` |  `Int32` = 0 | The count of line breaks to emit after
                             displaying framed contents
                             |===

                             The effective count of line breaks is the maximum value of
                             `line_breaks_before` and `line_breaks_after` for adjacent rows.

                             ==== struct Title

                             Attributes `value`, `frame`, `alignment` and `repeated` are declared as
                             properties, and can be modified after initialization

                             ===== Method `initialize`

                             Returns an instance of `Title`

                             [cols="1,2,3"]
                             |===
                             | Parameters | Type and default value | Comments
                             | `value` |  `CellType?` = `nil` | The title contents (may not be an empty string).
                             No display if `nil`
                             | `frame` | `Frame?` = `nil` | Defines a frame around title contents. If
                             `nil`, no frame.
                             | `repeated` | `Bool` = `false` | If `true`, force title (and subtitle) to
                             be repeated at header frequency
                             | `alignment` | `Justify` =  `DEFAULT_HEADING_ALIGNMENT` |
                             | `formatter` | `TextCellFormatter` = `DEFAULT_FORMATTER` |
                             | `styler` | `TextCellStyler` = `DEFAULT_STYLER` |
                             |===


                             ==== struct SubTitle

                             Attributes `value`, `frame` and `alignment` are declared as properties, and
                             can be modified after initialization

                             ===== Method `initialize`

                             Returns an instance of `SubTitle`

                             [cols="1,2,3"]
                             |===
                             | Parameters | Type and default value | Comments
                             | `value` |  `CellType?` = `nil` | The title contents (may not be an empty string).
                             No display if `nil`
                             | `frame` | `Frame?` = `nil` | Defines a frame around title contents
                             | `alignment` | `Justify` =  `DEFAULT_HEADING_ALIGNMENT` |
                             | `formatter` | `TextCellFormatter` = `DEFAULT_FORMATTER` |
                             | `styler` | `TextCellStyler` = `DEFAULT_STYLER` |
                             |===

                             ==== struct Footer

                             Attributes `value`, `frame`, `alignment` and `page_break` are declared as
                             properties, and can be modifiedafter initialization

                             ===== Method `initialize`

                             Returns an instance of `Footer`

                             [cols="1,2,3"]
                             |===
                             | Parameters | Type and default value | Comments
                             | `value` |  `CellType?` = `nil` | The title contents (may not be an empty string).
                             No display if `nil`
                             | `frame` | `Frame?` = `nil` | Defines a frame around title contents
                             | `page_break` | `Bool` = `false` |  If `true`, emit a page break
                             after footer
                             | `alignment` | `Justify` =  `DEFAULT_HEADING_ALIGNMENT` |
                             | `formatter` | `TextCellFormatter` = `DEFAULT_FORMATTER` |
                             | `styler` | `TextCellStyler` = `DEFAULT_STYLER` |
                             |===

                             ////
                             == Overview


                             To give you a taste of both the richness of the layout, here's an example that's somewhat contrived, but interesting to study.

                             image::docs/assets/images/overview.svg[width="400",caption="Figure 8:"]

{empty} +

and the corresponding source code.

[source,crystal]

include::docs/assets/sources/overview.cr[]

////

Repository

tablo

Owner
Statistic
  • 0
  • 0
  • 20
  • 0
  • 4
  • 11 months ago
  • August 9, 2023
License

Links
Synced at

Thu, 21 Nov 2024 14:54:12 GMT

Languages