tessella 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +900 -0
- data/doc/README.md +74 -0
- data/doc/bitfield.md +123 -0
- data/doc/charts.md +1370 -0
- data/doc/check.rb +75 -0
- data/doc/cli.md +483 -0
- data/doc/composition.md +599 -0
- data/doc/getting-started.md +121 -0
- data/doc/glyphs.md +134 -0
- data/doc/images.md +325 -0
- data/doc/library.md +239 -0
- data/doc/lint.md +270 -0
- data/doc/meter.md +210 -0
- data/doc/rule.md +51 -0
- data/doc/seq.md +169 -0
- data/doc/sparkline.md +63 -0
- data/doc/table.md +347 -0
- data/doc/targets.md +222 -0
- data/doc/text.md +84 -0
- data/doc/tree.md +108 -0
- data/doc/wave.md +122 -0
- data/examples/README.md +103 -0
- data/examples/ages.txt +9 -0
- data/examples/bad.csv +3 -0
- data/examples/boxes.txt +3 -0
- data/examples/broken.txt +4 -0
- data/examples/calibration.rb +12 -0
- data/examples/cities.csv +4 -0
- data/examples/cjk.rb +13 -0
- data/examples/comment.rb +9 -0
- data/examples/comment.sh +13 -0
- data/examples/cpu.txt +60 -0
- data/examples/dashboard.sh +25 -0
- data/examples/datasheet.rb +16 -0
- data/examples/disc.pgm +0 -0
- data/examples/disks.sh +18 -0
- data/examples/disks.txt +5 -0
- data/examples/dose.txt +7 -0
- data/examples/drift.txt +4 -0
- data/examples/ethernet.txt +3 -0
- data/examples/figure.c +5 -0
- data/examples/fit.rb +16 -0
- data/examples/floors.rb +21 -0
- data/examples/gallery.sh +20 -0
- data/examples/gauges.sh +31 -0
- data/examples/gradient.rb +16 -0
- data/examples/growth.txt +6 -0
- data/examples/hosts.csv +4 -0
- data/examples/icmp.txt +3 -0
- data/examples/ipv4.txt +12 -0
- data/examples/kinds.csv +4 -0
- data/examples/langs.csv +4 -0
- data/examples/lint.rb +16 -0
- data/examples/load.rb +14 -0
- data/examples/login.txt +10 -0
- data/examples/neg.csv +4 -0
- data/examples/normal.txt +200 -0
- data/examples/note.txt +2 -0
- data/examples/notes-fixed.md +17 -0
- data/examples/notes.md +17 -0
- data/examples/ohlc.txt +7 -0
- data/examples/os.csv +10 -0
- data/examples/outline.txt +8 -0
- data/examples/plot.txt +5 -0
- data/examples/refusals.rb +17 -0
- data/examples/report.sh +21 -0
- data/examples/rfc.sh +22 -0
- data/examples/samples.txt +7 -0
- data/examples/shapes.pgm +0 -0
- data/examples/sizes.tsv +3 -0
- data/examples/spi.txt +6 -0
- data/examples/status.sh +23 -0
- data/examples/stem.txt +12 -0
- data/examples/table-drift.txt +4 -0
- data/examples/temps.txt +31 -0
- data/examples/trig.sh +13 -0
- data/examples/trig.txt +102 -0
- data/examples/week.txt +8 -0
- data/examples/wide.txt +2 -0
- data/exe/tessella +6 -0
- data/lib/tessella/axis.rb +71 -0
- data/lib/tessella/bitfield.rb +194 -0
- data/lib/tessella/block.rb +56 -0
- data/lib/tessella/borders.rb +85 -0
- data/lib/tessella/canvas/area.rb +155 -0
- data/lib/tessella/canvas/ascii.rb +120 -0
- data/lib/tessella/canvas/block.rb +40 -0
- data/lib/tessella/canvas/braille.rb +30 -0
- data/lib/tessella/canvas/clip.rb +49 -0
- data/lib/tessella/canvas/density.rb +51 -0
- data/lib/tessella/canvas/dot.rb +27 -0
- data/lib/tessella/canvas/eighths.rb +43 -0
- data/lib/tessella/canvas/half.rb +51 -0
- data/lib/tessella/canvas/sextant.rb +36 -0
- data/lib/tessella/canvas.rb +168 -0
- data/lib/tessella/cell.rb +10 -0
- data/lib/tessella/chart.rb +211 -0
- data/lib/tessella/check.rb +52 -0
- data/lib/tessella/cli/bitfield.rb +121 -0
- data/lib/tessella/cli/chart.rb +615 -0
- data/lib/tessella/cli/chart_candle.rb +36 -0
- data/lib/tessella/cli/chart_heatmap.rb +21 -0
- data/lib/tessella/cli/chart_help.rb +85 -0
- data/lib/tessella/cli/chart_pie.rb +38 -0
- data/lib/tessella/cli/compose.rb +156 -0
- data/lib/tessella/cli/compose_options.rb +259 -0
- data/lib/tessella/cli/glyphs.rb +97 -0
- data/lib/tessella/cli/image.rb +250 -0
- data/lib/tessella/cli/input.rb +44 -0
- data/lib/tessella/cli/iso_time.rb +28 -0
- data/lib/tessella/cli/lint.rb +102 -0
- data/lib/tessella/cli/lint_options.rb +99 -0
- data/lib/tessella/cli/main.rb +121 -0
- data/lib/tessella/cli/meter.rb +80 -0
- data/lib/tessella/cli/rule.rb +57 -0
- data/lib/tessella/cli/seq.rb +117 -0
- data/lib/tessella/cli/sparkline.rb +72 -0
- data/lib/tessella/cli/table.rb +167 -0
- data/lib/tessella/cli/text.rb +78 -0
- data/lib/tessella/cli/tree.rb +100 -0
- data/lib/tessella/cli/wave.rb +148 -0
- data/lib/tessella/cli.rb +36 -0
- data/lib/tessella/color.rb +106 -0
- data/lib/tessella/compose.rb +243 -0
- data/lib/tessella/compose_fill.rb +176 -0
- data/lib/tessella/compose_grid.rb +36 -0
- data/lib/tessella/compose_shadow.rb +41 -0
- data/lib/tessella/error.rb +13 -0
- data/lib/tessella/frame.rb +170 -0
- data/lib/tessella/glyphs.rb +106 -0
- data/lib/tessella/image.rb +454 -0
- data/lib/tessella/image_octant.rb +46 -0
- data/lib/tessella/image_petscii.rb +116 -0
- data/lib/tessella/kinds/bar.rb +344 -0
- data/lib/tessella/kinds/box.rb +213 -0
- data/lib/tessella/kinds/box_upright.rb +110 -0
- data/lib/tessella/kinds/candlestick.rb +164 -0
- data/lib/tessella/kinds/columns.rb +259 -0
- data/lib/tessella/kinds/date_axis.rb +102 -0
- data/lib/tessella/kinds/error_bars.rb +71 -0
- data/lib/tessella/kinds/heatmap.rb +130 -0
- data/lib/tessella/kinds/line.rb +173 -0
- data/lib/tessella/kinds/multibar.rb +98 -0
- data/lib/tessella/kinds/multibar_columns.rb +130 -0
- data/lib/tessella/kinds/multibar_fine.rb +45 -0
- data/lib/tessella/kinds/multibar_rows.rb +93 -0
- data/lib/tessella/kinds/pie.rb +123 -0
- data/lib/tessella/kinds/pie_rim.rb +78 -0
- data/lib/tessella/kinds/sideways.rb +62 -0
- data/lib/tessella/kinds/stem.rb +134 -0
- data/lib/tessella/kinds/threshold.rb +73 -0
- data/lib/tessella/legend.rb +31 -0
- data/lib/tessella/lint/alphabet.rb +56 -0
- data/lib/tessella/lint/drift.rb +82 -0
- data/lib/tessella/lint/fences.rb +51 -0
- data/lib/tessella/lint/geometry.rb +120 -0
- data/lib/tessella/lint/geometry_checks.rb +224 -0
- data/lib/tessella/lint/lines.rb +115 -0
- data/lib/tessella/lint/mixing.rb +61 -0
- data/lib/tessella/lint/runner.rb +73 -0
- data/lib/tessella/lint/shadows.rb +120 -0
- data/lib/tessella/lint/target_rows.rb +66 -0
- data/lib/tessella/lint.rb +158 -0
- data/lib/tessella/meter.rb +133 -0
- data/lib/tessella/meter_upright.rb +43 -0
- data/lib/tessella/png.rb +456 -0
- data/lib/tessella/raster.rb +92 -0
- data/lib/tessella/rmagick_reader.rb +52 -0
- data/lib/tessella/rule.rb +59 -0
- data/lib/tessella/seq.rb +522 -0
- data/lib/tessella/sparkline.rb +57 -0
- data/lib/tessella/stats.rb +199 -0
- data/lib/tessella/table.rb +547 -0
- data/lib/tessella/table_cells.rb +225 -0
- data/lib/tessella/target.rb +105 -0
- data/lib/tessella/text.rb +76 -0
- data/lib/tessella/text_forms.rb +150 -0
- data/lib/tessella/tree.rb +221 -0
- data/lib/tessella/version.rb +5 -0
- data/lib/tessella/wave.rb +353 -0
- data/lib/tessella/width.rb +122 -0
- data/lib/tessella/width_class.rb +77 -0
- data/lib/tessella/width_invisible.rb +66 -0
- data/lib/tessella/width_invisible_high.rb +60 -0
- data/lib/tessella.rb +94 -0
- metadata +226 -0
data/README.md
ADDED
|
@@ -0,0 +1,900 @@
|
|
|
1
|
+
# tessella
|
|
2
|
+
|
|
3
|
+
Text figures for a monospace grid, in Unicode or pure ASCII: charts
|
|
4
|
+
(line, scatter, stairs, bar, histogram, box, stem and density plots,
|
|
5
|
+
on braille, block, dot, density or ASCII canvases, with scaled axes),
|
|
6
|
+
trees, tables, RFC-style bit-field diagrams, timing diagrams and
|
|
7
|
+
sequence diagrams, and the composition that frames them and sets
|
|
8
|
+
them side by side or stacked. The output is meant for documents
|
|
9
|
+
first, a terminal second: no trailing blank on any line,
|
|
10
|
+
no escape code unless colour is asked for, and a `--ascii` mode that
|
|
11
|
+
is ASCII from the first byte.
|
|
12
|
+
|
|
13
|
+
tessella has no runtime dependency: binning, percentiles and density
|
|
14
|
+
mapping are plain Ruby. The full guide, a page per module with real
|
|
15
|
+
renders and runnable [examples](examples/), is in [doc/](doc/README.md);
|
|
16
|
+
see [DESIGN.md](DESIGN.md) for the internal interfaces and the
|
|
17
|
+
decisions behind them.
|
|
18
|
+
|
|
19
|
+
> [!WARNING]
|
|
20
|
+
> Where a figure is read changes how wide its glyphs are: a CJK
|
|
21
|
+
> terminal draws box drawing two columns wide, and a browser may draw
|
|
22
|
+
> `▶` as an emoji or braille from a wider fallback font. Draw for the
|
|
23
|
+
> reader with `--target term|cjk|web`; see
|
|
24
|
+
> [Width targets](#width-targets).
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
tessella is not on rubygems.org, and it has no public remote. Install
|
|
29
|
+
it from a checkout of this repository:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
cd tessella
|
|
33
|
+
make install
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`make install` builds the gem and installs it for this user (`gem
|
|
37
|
+
install --user-install --local`). `make uninstall` removes it again.
|
|
38
|
+
Running `exe/tessella` directly out of a checkout, without installing,
|
|
39
|
+
also works.
|
|
40
|
+
|
|
41
|
+
## The library
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
require "tessella"
|
|
45
|
+
|
|
46
|
+
chart = Tessella.line(x = nil, y, name: nil, **opts) # also scatter,
|
|
47
|
+
# stairs, density
|
|
48
|
+
chart.add(x = nil, y, name: nil, color: nil, mark: nil) # another series
|
|
49
|
+
Tessella.stairs(x = nil, y, style: :post, **opts) # or style: :pre
|
|
50
|
+
Tessella.bar(labels, values, **opts)
|
|
51
|
+
Tessella.histogram(values, bins: nil, closed: :left, **opts)
|
|
52
|
+
Tessella.box(names, samples, **opts) # samples: Array of Arrays
|
|
53
|
+
Tessella.stem(values, scale: nil, **opts) # stem-and-leaf
|
|
54
|
+
|
|
55
|
+
chart.to_s # => String, already framed
|
|
56
|
+
chart.render(io = $stdout)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Options common to every kind, all optional: `title xlabel ylabel
|
|
60
|
+
width height xlim ylim canvas ascii color border xscale`, and for
|
|
61
|
+
line, scatter, stairs and density `yscale` too. Defaults:
|
|
62
|
+
width 40, height 15, canvas `:braille` (`:ascii` when `ascii: true`),
|
|
63
|
+
`color: false`, border by kind. Besides braille's 2x4 dots, `canvas:`
|
|
64
|
+
takes the solid mosaics: `:block`/`:quadrant` (2x2), `:half` (1x2),
|
|
65
|
+
`:sextant` (teletext's 2x3, `🬀`..`🬻`, Unicode 13) and `:octant`
|
|
66
|
+
(2x4, braille's resolution solid, Unicode 16, so only the newest
|
|
67
|
+
terminals draw it), as well as `:dot`, `:ascii` and `:density`. `xscale:` runs a series' x values
|
|
68
|
+
through a transform (`:ln`, `:log2`, `:log10`; `nil` the identity)
|
|
69
|
+
before limits and drawing, and `yscale:` a series' y values; the
|
|
70
|
+
axis then reads in transformed units (1 to 1000 under `:log10` is 0
|
|
71
|
+
to 3), as unicode_plot's does. An unknown option, canvas, xscale name, or
|
|
72
|
+
(for stairs) style raises `ArgumentError` naming it, as do a width or
|
|
73
|
+
height that is not a positive Integer, x and y of different lengths,
|
|
74
|
+
and a value that is not a finite number (nil, a String, NaN,
|
|
75
|
+
±Infinity, or what an `xscale:` makes of an x of 0 or below).
|
|
76
|
+
|
|
77
|
+
Dates on x: give line, scatter, stairs or density x values that are
|
|
78
|
+
times (a `Time`, or a `Date`/`DateTime`, a `Date` counting from
|
|
79
|
+
midnight UTC). They are plotted as seconds and the axis is labelled as
|
|
80
|
+
dates, in UTC, its format following the span (`%Y-%m-%d` for a day or
|
|
81
|
+
more, `%m-%d %H:%M` for an hour or more, `%H:%M:%S` below) or set by
|
|
82
|
+
`xdate: "%b %d"`; the axis spans the data exactly unless `xlim:`
|
|
83
|
+
(which may hold times) says otherwise, and takes no `xscale:`. On the
|
|
84
|
+
command line, `--xdate [FMT]` reads the x column as ISO 8601
|
|
85
|
+
(`2026-11-05`, `2026-11-05T14:30`, with `Z` or `+hh:mm`, UTC when
|
|
86
|
+
absent); a field that is not one is bad input.
|
|
87
|
+
|
|
88
|
+
Error bars: `yerr:` on line and scatter (`Tessella.line`,
|
|
89
|
+
`.scatter`, and `add`) is an error a point, the bar running from y -
|
|
90
|
+
e to y + e, or `[lows, highs]` for asymmetric ones; each point gets a
|
|
91
|
+
segment along the value axis (across, sideways), drawn after the line
|
|
92
|
+
or marks, and the axis limits take the bars in. Errors are in data
|
|
93
|
+
units and go through `yscale:` with y. On the command line `--yerr`
|
|
94
|
+
reads three columns, x, y and the error; a negative error in the
|
|
95
|
+
input is bad input (exit 1), naming its row.
|
|
96
|
+
|
|
97
|
+
A threshold: `threshold: N` on line, scatter and stairs draws a
|
|
98
|
+
dashed rule at N across the plot (down it, sideways), visible without
|
|
99
|
+
colour, and the value axis takes N in; under `color: true` the cells
|
|
100
|
+
beyond it take `above:` (`:red` by default) and the rest `below:`
|
|
101
|
+
(the series' own colour unless given). On the command line,
|
|
102
|
+
`--threshold N`, `--above C`, `--below C`.
|
|
103
|
+
|
|
104
|
+
`mark:` (on `add`, and on `Tessella.line`, `.scatter`, `.stairs` and
|
|
105
|
+
`.density` for the first series) draws that series as one glyph in
|
|
106
|
+
the cell of each point, over whatever the cell holds, instead of the
|
|
107
|
+
kind's own line or dots: `chart.add(xs, ys, name: "measured", mark:
|
|
108
|
+
"●")` marks measured points on a fitted curve. The glyph is one
|
|
109
|
+
character, ASCII under `ascii: true`; the legend shows it before the
|
|
110
|
+
series' name. Bar, histogram, box and stem take no `mark:`.
|
|
111
|
+
|
|
112
|
+
Line and stairs take `area: true`, which fills under each series
|
|
113
|
+
from the floor up, as btop draws its graphs: every pixel column is a
|
|
114
|
+
bar, so a braille cell holds two bars of 0 to 4 dots (a block cell two
|
|
115
|
+
of 0 to 2), and a peak narrower than a column still shows. The y axis
|
|
116
|
+
then starts at 0 unless `ylim:` says otherwise. Under `color: true`
|
|
117
|
+
the cells take `gradient:`'s colours by height, bottom first
|
|
118
|
+
(default `[:green, :yellow, :red]`, equal bands; `false` keeps the
|
|
119
|
+
series' colour); without colour they are plain glyphs. A colour, in
|
|
120
|
+
`gradient:` as anywhere tessella takes one, is a name (`:red`,
|
|
121
|
+
`:green`, `:yellow`, `:blue`, `:magenta`, `:cyan`, `:white`), a
|
|
122
|
+
256-colour index (`0..255`) or a truecolor `"#rrggbb"`; a gradient
|
|
123
|
+
whose stops are all `#rrggbb` blends smoothly from the bottom row to
|
|
124
|
+
the top one instead of in bands. `canvas: :eighths` (`--canvas
|
|
125
|
+
eighths`) fills by eighth blocks instead: one bar per character, but
|
|
126
|
+
8 steps a cell (`▁▂▃▄▅▆▇█`; sideways `▏▎▍▌▋▊▉█`), btop's other look.
|
|
127
|
+
It only fills, so it needs `area: true` and refuses `threshold:`,
|
|
128
|
+
whose dashes would read as bars.
|
|
129
|
+
|
|
130
|
+
Bar and histogram take three more options, all optional: `fill:` the
|
|
131
|
+
bar glyph (`█` by default, `#` under `ascii: true`), `eighths:` draw
|
|
132
|
+
the fractional last cell with `▏▎▍▌▋▊▉` (`true` by default, always
|
|
133
|
+
`false` under `ascii: true`), and `track:` the glyph filling the rest
|
|
134
|
+
of the bar's width (`·` by default, `.` under `ascii: true`; `nil` for
|
|
135
|
+
none).
|
|
136
|
+
|
|
137
|
+
Bar values may be negative. With at least one, the bar area splits at
|
|
138
|
+
a zero column into a negative and a positive part, each as wide as
|
|
139
|
+
its share of the values' span: negative bars grow left from zero,
|
|
140
|
+
positive ones right, and every value label follows the whole area.
|
|
141
|
+
A negative bar's fractional end is a half cell (`▐`), whole cells
|
|
142
|
+
under `ascii: true` or `eighths: false`:
|
|
143
|
+
|
|
144
|
+
```text
|
|
145
|
+
┌ ┐
|
|
146
|
+
a ┤█████████████████········· -6
|
|
147
|
+
b ┤·················█████████ 3
|
|
148
|
+
c ┤·························· 0
|
|
149
|
+
└ ┘
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Bar and histogram also take `orientation:` and `blocks:`.
|
|
153
|
+
`orientation: :vertical` draws columns rising from a floor, the value
|
|
154
|
+
axis on the left from 0 to the largest value; a bar chart's names go
|
|
155
|
+
under their columns when each fits, else its first and last name at
|
|
156
|
+
the two ends, and a histogram shows its axis's two ends. `blocks:` is
|
|
157
|
+
the glyph the bars are built of: `:eighths` (the default; `▁` to `█`
|
|
158
|
+
up a column, one column per character), `:braille` (one dot column
|
|
159
|
+
per bar, so two columns per character, 4 steps per cell; or, kept
|
|
160
|
+
horizontal, four bars per text row, each row named by the bars it
|
|
161
|
+
holds) or `:block`, also named `:quadrant` (the 2x2 quadrant glyphs,
|
|
162
|
+
as `canvas: :quadrant` for an area fill: two per character, 2
|
|
163
|
+
steps) or `:half` (half blocks, one bar per character: `▄`/`█` up a
|
|
164
|
+
column, 2 steps per cell, `▌`/`█` along a row, half a cell per step;
|
|
165
|
+
`canvas: :half` fills an area by halves); `:sextant` and `:octant`
|
|
166
|
+
pack three and four bars a text row (two columns per character
|
|
167
|
+
upright). All need values of
|
|
168
|
+
0 or more; `:braille` and `:block` refuse `ascii: true`, `fill:`,
|
|
169
|
+
`eighths:` and `track:`. On the command line: `--orientation
|
|
170
|
+
horizontal|vertical` and `--blocks
|
|
171
|
+
eighths|braille|block|quadrant|half|sextant|octant`.
|
|
172
|
+
|
|
173
|
+
```text
|
|
174
|
+
$ printf 'ruby 12\nc 30\nrust 7.5\ngo 18\n' | \
|
|
175
|
+
tessella chart --kind bar --orientation vertical --height 5 --width 20
|
|
176
|
+
┌───────────────────┐
|
|
177
|
+
30 │ ████ │
|
|
178
|
+
│ ████ │
|
|
179
|
+
│ ████ ████│
|
|
180
|
+
│████ ████ ▂▂▂▂ ████│
|
|
181
|
+
0 │████ ████ ████ ████│
|
|
182
|
+
└───────────────────┘
|
|
183
|
+
ruby c rust go
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Line, scatter, stairs and density take `orientation:` as well, the
|
|
187
|
+
direction their value axis runs: `:vertical` (the default) puts the
|
|
188
|
+
values up the vertical axis; `:horizontal` swaps the axes, values
|
|
189
|
+
along the horizontal one and x running up, so a horizontal chart of
|
|
190
|
+
`(x, y)` is the vertical chart of `(y, x)`. `xlim:`, `ylim:`,
|
|
191
|
+
`xlabel:` and `ylabel:` keep their meaning and follow their axis; an
|
|
192
|
+
`area:` fills from the left edge, its gradient running left to right.
|
|
193
|
+
|
|
194
|
+
Bars of several series: give `Tessella.bar` an Array per label, one
|
|
195
|
+
value a series, and name the series with `series:`. They are grouped
|
|
196
|
+
-- a bar a series under each label, a blank row between labels -- or,
|
|
197
|
+
with `stacked: true`, end to end in one bar labelled with its total;
|
|
198
|
+
both work upright too (`orientation: :vertical`). Series are told
|
|
199
|
+
apart by fill (`█ ▓ ▒ ░ ╳ ╱ ╲ ∴ ● ○ ■ ·`, `# = + @ * : . o % & ~ x`
|
|
200
|
+
in ASCII, so at most 12 series) and, under `color: true`, by colour;
|
|
201
|
+
the legend sits right of the frame, or wrapped to the frame's width
|
|
202
|
+
under it when there are more series than rows. Eighths end only the
|
|
203
|
+
`█` series, since they are solid -- except under `color: true`,
|
|
204
|
+
where stacked series meet to the eighth: the cell they share is an
|
|
205
|
+
eighths glyph inked in the first series' colour on the next one's
|
|
206
|
+
background (without colour its two halves could not be told apart,
|
|
207
|
+
so the boundary stays on a whole cell). Values must be 0 or more, and the
|
|
208
|
+
braille, block and half layouts draw one series only.
|
|
209
|
+
|
|
210
|
+
```text
|
|
211
|
+
$ printf 'q cpu io net\nq1 3 5 2\nq2 4 2 6\n' | \
|
|
212
|
+
tessella chart --kind bar --stacked --width 24
|
|
213
|
+
┌ ┐
|
|
214
|
+
q1 ┤█████▓▓▓▓▓▓▓▓▒▒▒▒··· 10
|
|
215
|
+
q2 ┤███████▓▓▓▒▒▒▒▒▒▒▒▒▒ 12
|
|
216
|
+
└ ┘
|
|
217
|
+
█ cpu ▓ io ▒ net
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
On the command line, a bar row with more than two columns is a label
|
|
221
|
+
and a value a series, named by the header; `--stacked` stacks them,
|
|
222
|
+
and a row of another width is bad input (exit 1).
|
|
223
|
+
|
|
224
|
+
`Tessella.heatmap(matrix, xlabels:, ylabels:)` draws a matrix as a
|
|
225
|
+
grid of cells, each `cell_width:` columns wide (2 by default, near
|
|
226
|
+
square in a terminal), shaded by value along ` ░▒▓█` (` .:-=+*#%@`
|
|
227
|
+
in ASCII), or, under `color: true`, as `█` coloured along a colormap
|
|
228
|
+
-- a viridis-like blend by default, or `colormap:` stops as any
|
|
229
|
+
gradient takes. Row names go on the left, column names under the
|
|
230
|
+
frame when each is narrower than a cell, and a key on the right gives
|
|
231
|
+
the range; `min:` and `max:` fix it, so maps drawn apart share one
|
|
232
|
+
scale. The matrix sets the size, so `width:` and `height:` are
|
|
233
|
+
refused. On the command line, `--kind heatmap` reads a row a label
|
|
234
|
+
then its values, a header naming the columns, with `--cell-width N`,
|
|
235
|
+
`--colormap C,C,...`, `--min A` and `--max B`.
|
|
236
|
+
|
|
237
|
+
```text
|
|
238
|
+
$ printf 'host mon tue wed thu\nalpha 1 3 5 2\nbeta 4 6 8 9\n' | \
|
|
239
|
+
tessella chart --kind heatmap --cell-width 4
|
|
240
|
+
┌────────────────┐
|
|
241
|
+
alpha │ ░░░░▒▒▒▒░░░░│ █ 9
|
|
242
|
+
beta │▒▒▒▒▓▓▓▓████████│ 1
|
|
243
|
+
└────────────────┘
|
|
244
|
+
mon tue wed thu
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
`Tessella.candlestick(x, open:, high:, low:, close:)` spreads the
|
|
248
|
+
candles over `width:`, each as wide as its share allows with a blank
|
|
249
|
+
column between (a column each, no gap, when they do not fit): the
|
|
250
|
+
wick `│` from low to high in the candle's middle column, the body
|
|
251
|
+
from open to close across the candle, `█` for an up candle (close at
|
|
252
|
+
or above open), `▒` for a down one, `─` when they share a row; under
|
|
253
|
+
`color: true` up is green and down red. The value axis is on the
|
|
254
|
+
left, the first and last x under the
|
|
255
|
+
frame -- dates when x holds times, formatted as a date axis's
|
|
256
|
+
(`xdate:`). A low above open or close, or a high below, is refused.
|
|
257
|
+
On the command line `--kind candlestick` reads `x open high low
|
|
258
|
+
close` rows, `--xdate` for ISO 8601 dates.
|
|
259
|
+
|
|
260
|
+
`Tessella.pie(labels, values)` draws the slices clockwise from the
|
|
261
|
+
top on a disc twice as wide as tall, so it reads round in a terminal,
|
|
262
|
+
each slice its own fill (the bars' twelve, so at most 12 slices)
|
|
263
|
+
and, under `color: true`, its own colour; the legend beside the disc
|
|
264
|
+
gives the shares in whole percents summing to 100 (under it, wrapped
|
|
265
|
+
to 40 columns, when the slices outnumber the rows). `donut: true` hollows the
|
|
266
|
+
middle into a ring. `rim: :smooth` (`--rim smooth`) draws the edge
|
|
267
|
+
of the disc, and of a donut's hole, to the shape: a cell it only
|
|
268
|
+
partly covers takes the glyph that best fits the coverage (`◢ ◣ ◤ ◥`,
|
|
269
|
+
halves, quadrants, quarter blocks, as `image --style petscii` picks
|
|
270
|
+
them), in its slice's colour; solid ink, so without colour the rim
|
|
271
|
+
reads as an outline. `:basic` does the same without Legacy
|
|
272
|
+
Computing's `🮊 🮂 🮅`, which older fonts lack; `:cells`, the default,
|
|
273
|
+
fills whole cells. `height:` (12 by default) sizes the disc;
|
|
274
|
+
`width:`, the limits, `canvas:` and `xscale:` are refused. Values
|
|
275
|
+
must be 0 or more, not all 0. On the command line `--kind pie` reads
|
|
276
|
+
`label value` rows, `--donut` for the ring:
|
|
277
|
+
|
|
278
|
+
```console
|
|
279
|
+
$ printf 'ruby,12\nc,30\nrust,7\n' | \
|
|
280
|
+
tessella chart --kind pie --donut --height 8
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
Box takes `orientation: :vertical` (`--orientation vertical`) too:
|
|
284
|
+
each box stands three columns wide, whiskers running up and down
|
|
285
|
+
(`╶┬╴` at the maximum, `┌┴┐` at Q3, `├─┤` the median, `└┬┘` at Q1,
|
|
286
|
+
`╶┴╴` at the minimum), the value axis on the left with its two ends
|
|
287
|
+
and middle, the names under the boxes; `ylim:` sets its range, and a
|
|
288
|
+
median sharing a row with a box edge wins it.
|
|
289
|
+
|
|
290
|
+
`Tessella.stem` refuses a scale that is not positive, and a plot
|
|
291
|
+
that would need more than 1000 stems, one line each (raise `scale:`).
|
|
292
|
+
|
|
293
|
+
### Composing figures
|
|
294
|
+
|
|
295
|
+
Any figure -- a chart, a String of lines, a `Block` -- composes into
|
|
296
|
+
one, measured in display columns (CJK counts two, colour codes none):
|
|
297
|
+
|
|
298
|
+
```ruby
|
|
299
|
+
plot = Tessella.bar(%w[ruby c], [12, 30], width: 20)
|
|
300
|
+
fig = Tessella.frame(plot, title: "Load", style: :rounded)
|
|
301
|
+
puts Tessella.beside(fig, Tessella.caption("note", text: "n"),
|
|
302
|
+
align: :middle)
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
`frame(fig, title:, style:, pad:)` boxes one figure (style `:light`,
|
|
306
|
+
`:heavy`, `:double`, `:rounded`, `:dashed` (`┄┆`), `:dashed_fine`
|
|
307
|
+
(`┈┊`), `:heavy_dashed` (`┅┇`), `:heavy_dashed_fine` (`┉┋`),
|
|
308
|
+
`:double_h` (`╒═╕`, double horizontals), `:double_v` (`╓─╖`, double
|
|
309
|
+
verticals) or `:ascii`; a name may say `-` for `_`; or a custom set: six
|
|
310
|
+
glyphs `tl tr bl br h v`, as in `style: "╓╖╙╜─║"`, or a
|
|
311
|
+
`Tessella::Borders::Set`); `beside(*figs, gap:,
|
|
312
|
+
align:)` sets figures side by side (align `:top`, `:middle`,
|
|
313
|
+
`:bottom`); `stack(*figs, gap:, rule:, center:)` sets them one under
|
|
314
|
+
another; `indent(fig, prefix:, suffix: nil)` prefixes every line (an empty
|
|
315
|
+
line gets the prefix without its trailing blank) and, with `suffix:`,
|
|
316
|
+
pads every row to the block's width and closes it with the suffix; `caption(fig, text:,
|
|
317
|
+
above:)` adds a centred line; `shadow(fig, glyph:)` adds a drop
|
|
318
|
+
shadow, `░` (`#` under `ascii: true`) one column right of every row
|
|
319
|
+
but the first and a bottom bar one row down reaching that column --
|
|
320
|
+
the geometry `tessella lint` checks; `fill(fig, pattern:)` puts a
|
|
321
|
+
pattern in every enclosed blank cell -- inside a frame, a box, any
|
|
322
|
+
closed region -- leaving text, lines and the outside alone: a name
|
|
323
|
+
from the registry (`light`, `medium`, `dark`, `solid`, `hatch`,
|
|
324
|
+
`backhatch`, `cross`, `grid`, `stipple`, `sparse`, `dotted`, `dots`,
|
|
325
|
+
`colons`, `hash`, `at`) or glyphs of one's own, tiled diagonally, all
|
|
326
|
+
of one width class (`colons` by default under `ascii: true`). A halo
|
|
327
|
+
keeps the pattern off text, so a label keeps its word shape:
|
|
328
|
+
`halo: true` (the default) leaves the cell each side of a text glyph
|
|
329
|
+
blank, `:around` the cells above and below too, `false` none; box
|
|
330
|
+
drawing and `+ - |` are not text. `frame(fig, fill:, halo:)` frames
|
|
331
|
+
and fills only the background the frame adds -- what floods in from
|
|
332
|
+
just inside its border -- never a box the content draws. Each takes `ascii:
|
|
333
|
+
true`, which draws ASCII glyphs and refuses non-ASCII content, and
|
|
334
|
+
returns a `Block`
|
|
335
|
+
(`lines`, `width`, `height`, `to_s`).
|
|
336
|
+
|
|
337
|
+
### Diagrams
|
|
338
|
+
|
|
339
|
+
Each diagram module reads the same small text formats the ascii-art
|
|
340
|
+
skill's scripts read, and returns a `Block`, so it composes like a
|
|
341
|
+
chart:
|
|
342
|
+
|
|
343
|
+
```ruby
|
|
344
|
+
Tessella.tree(outline, style: nil, ascii: false, root: nil)
|
|
345
|
+
Tessella.table(rows, header: true, style: nil, ascii: false,
|
|
346
|
+
align: nil, title: nil, width: nil, min_width: 4,
|
|
347
|
+
color: false)
|
|
348
|
+
Tessella::Table.parse(text, tsv: false) # CSV/TSV -> rows
|
|
349
|
+
Tessella.bitfield(fields, word: 32, octets: false, ruler: true,
|
|
350
|
+
abbrev: false)
|
|
351
|
+
Tessella.wave(text, period: 4, ascii: false, ruler: false)
|
|
352
|
+
Tessella.seq(text, ascii: false, numbered: false, footer: false)
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
- **tree**: an indented outline, one node per line, the indent unit
|
|
356
|
+
inferred from the first indented line; an optional `- `/`* `
|
|
357
|
+
bullet, and `label # note` notes aligned into one column. Styles
|
|
358
|
+
`:light`, `:heavy`, `:rounded`; several top-level lines draw as a
|
|
359
|
+
forest unless `root:` names one root for them.
|
|
360
|
+
- **table**: rows of Strings, the first the header unless `header:
|
|
361
|
+
false`; styles `:light`, `:heavy`, `:double`, `:rounded`, the
|
|
362
|
+
dashed four and `:double_h`/`:double_v` (with their mixed tees `╤ ╞
|
|
363
|
+
╪` and `╥ ╟ ╫`), `:ascii`,
|
|
364
|
+
`:minimal` (dividers only) and `:markdown` (a GitHub pipe table), or
|
|
365
|
+
a custom set of eleven glyphs, a frame's six then the tees `top_t
|
|
366
|
+
bot_t sep_l sep_t sep_r` (`"╒╕╘╛═│╤╧╞╪╡"`). A
|
|
367
|
+
column of numbers right-aligns; `align:` ("lrc", one letter per
|
|
368
|
+
column) overrides; it also takes an Array of `:left`, `:right`,
|
|
369
|
+
`:center`. `Table.parse` reads CSV (RFC 4180 quoting) or, with `tsv:
|
|
370
|
+
true`, tab-separated text. A cell holding `"\n"` draws on several
|
|
371
|
+
lines within its row, top-aligned. `width: N` fits the framed table
|
|
372
|
+
to N columns by narrowing its widest column a column at a time --
|
|
373
|
+
then whichever is widest, so wide columns converge and a narrow one
|
|
374
|
+
is never cut first -- and word-wrapping the narrowed cells (a word
|
|
375
|
+
too long breaks inside). `min_width:` is each column's floor (4 by
|
|
376
|
+
default), or an Array of one floor a column, so short identifiers
|
|
377
|
+
can stay whole while a long path takes the cut; with every column
|
|
378
|
+
at its floor, or its natural width when smaller, and still too
|
|
379
|
+
wide, the table is refused, naming the width it needs. ANSI colour in a cell takes
|
|
380
|
+
no column and survives padding and wrapping, closed and reopened on
|
|
381
|
+
each wrapped line; `color: true`
|
|
382
|
+
sets the header in bold. Markdown refuses multi-line cells and
|
|
383
|
+
`width:`.
|
|
384
|
+
- **bitfield**: `name:bits` lines or `[name, bits]` pairs, drawn as
|
|
385
|
+
RFC 2360 draws them, always in ASCII: a bit ruler, `+-+` ticks, each
|
|
386
|
+
name centred in its cell, a field longer than its row continued on
|
|
387
|
+
the next. `word:` is 8, 16, 32 or 64; `octets:` ticks per byte; a
|
|
388
|
+
name too wide for its cell is refused unless `abbrev:` truncates it,
|
|
389
|
+
and a non-ASCII name is refused.
|
|
390
|
+
- **wave**: WaveDrom-like signals, `name: wave` per line in `0 1 p n
|
|
391
|
+
x z = .`, bus labels after `|`, a lone `---` for a blank row; each
|
|
392
|
+
step is `period:` columns (2 at least), `ruler:` numbers the steps.
|
|
393
|
+
- **seq**: `participant A`, `A -> B: call`, `A --> B: reply`, `A ->>
|
|
394
|
+
B: async`, `A -> A: self`, `note over A[,B]: text`; columns widen
|
|
395
|
+
to fit every label, and `numbered:` / `footer:` number the messages
|
|
396
|
+
and repeat the participant boxes below. Heads are the full-size
|
|
397
|
+
`► ◄` for calls and replies, and a double `►► ◄◄` for async (`> <`
|
|
398
|
+
and `>> <<` under `ascii: true`).
|
|
399
|
+
|
|
400
|
+
Every one refuses bad input with an `ArgumentError` naming the line,
|
|
401
|
+
and under `ascii: true` draws ASCII glyphs and refuses non-ASCII text.
|
|
402
|
+
|
|
403
|
+
### Pictures
|
|
404
|
+
|
|
405
|
+
```ruby
|
|
406
|
+
Tessella.image(source, style: :ramp, width: 78, ramp: nil, gamma: 1.0,
|
|
407
|
+
threshold: 0.5, dither: :fs, invert: false,
|
|
408
|
+
edge_threshold: 0.3, ascii: false)
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
`source` is a `Tessella::Raster`, an IO, or a String of image bytes.
|
|
412
|
+
PGM/PPM and PNG are decoded with nothing installed; any other format
|
|
413
|
+
(JPEG, TIFF, WebP, ...) needs the `rmagick` gem, used when it is
|
|
414
|
+
installed and never required, and without it is refused with the
|
|
415
|
+
`magick IMAGE pgm:- | tessella image` pipe as the way round. Styles
|
|
416
|
+
trade tone for resolution: `ramp` and `blocks` give one tone per cell,
|
|
417
|
+
`half`, `quadrant`, `sextant`, `braille` and `octant` pack 2 to 8
|
|
418
|
+
pixels into a cell (`octant` is Unicode 16, drawn by the newest
|
|
419
|
+
terminals only, Ghostty and its like), `petscii` draws each cell
|
|
420
|
+
with the glyph whose shape best fits its 4x8 tones, in the spirit of
|
|
421
|
+
the Commodore character set -- halves, quadrants, quarter and
|
|
422
|
+
three-quarter blocks (`▊ ▆`, and Legacy Computing's `🮊 🮅` for the
|
|
423
|
+
right and upper ones), eighth-lines,
|
|
424
|
+
diagonal triangles `◢ ◣ ◤ ◥` and lines, shades `░ ▒ ▓`, line pieces,
|
|
425
|
+
discs `● ○` (threshold and dither do not apply to it) -- and `edges`
|
|
426
|
+
draws Sobel contours as `─ │ ╱ ╲`. `ramp:` is glyphs light to dense, or a name
|
|
427
|
+
from the glyph registry: `ramp` (` .:-=+*#%@`), `blocks`, `long` (a
|
|
428
|
+
70-glyph ASCII ramp, for finer tone) or `hatch`; a name wins over
|
|
429
|
+
the same letters. `dither:` is `:none`, `:fs` (Floyd-Steinberg),
|
|
430
|
+
`:atkinson` or `:bayer`; `invert: true` is for a light background.
|
|
431
|
+
`ascii: true` allows only the `ramp` style with an ASCII ramp. A
|
|
432
|
+
picture that renders blank is refused, naming the threshold to
|
|
433
|
+
lower.
|
|
434
|
+
|
|
435
|
+
### Linting
|
|
436
|
+
|
|
437
|
+
```ruby
|
|
438
|
+
report = Tessella.lint(source, width: 78, prefix: "", suffix: "",
|
|
439
|
+
web: false, forbid: [], no_wide: false, ascii: false,
|
|
440
|
+
allow_mixed: false, fences: false, geometry: false,
|
|
441
|
+
borders: [])
|
|
442
|
+
report.findings # => [#<data Tessella::Lint::Finding line=3, message="...">]
|
|
443
|
+
report.clean? # => true when there is none
|
|
444
|
+
Tessella.alphabet(source) # => "ascii", or "utf-8 box-drawing(12) ..."
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
`source` is a String or an IO. The lint checks a text figure for
|
|
448
|
+
what breaks it on someone else's screen: tabs, invisible glyphs,
|
|
449
|
+
spaces other than U+0020 (no-break, thin, ideographic, ...), trailing
|
|
450
|
+
blanks, lines past `width` once `prefix` is drawn in front and
|
|
451
|
+
`suffix` behind (a comment's closing ` -->` or ` */`),
|
|
452
|
+
invalid UTF-8, ASCII box structure mixed with Unicode box drawing in
|
|
453
|
+
one figure, a vertical one column off its anchor on a few rows, and a
|
|
454
|
+
drop shadow (`░`) missing from a row or misplaced. `web: true` adds
|
|
455
|
+
glyphs some web renderers draw two cells wide; `no_wide: true` makes
|
|
456
|
+
every wide glyph an error, and `ascii: true` every non-ASCII one.
|
|
457
|
+
`forbid:` takes sequences, or presets naming a comment syntax (`xml`,
|
|
458
|
+
`c-block`, `c-line`, `python-doc`, ...), and flags lines that would
|
|
459
|
+
end the comment the figure sits in. A line reading
|
|
460
|
+
`artlint: allow-mixed` (or `tessella: allow-mixed`), in any comment
|
|
461
|
+
syntax, exempts the figure after it from the mixing check.
|
|
462
|
+
|
|
463
|
+
`fences: true` lints each fenced block of a Markdown text on its own,
|
|
464
|
+
with the text's line numbers, so prose and tables are not read as
|
|
465
|
+
art. `geometry: true` also checks that a box's sides stay in their
|
|
466
|
+
columns down to its bottom edge, and that a vertical or an arrow head
|
|
467
|
+
hangs from something above it rather than from blank space, for every
|
|
468
|
+
box-drawing set (light, heavy, double, rounded, dashed, mixed
|
|
469
|
+
junctions) and ASCII `+-|` boxes; `borders:` adds custom sets (names,
|
|
470
|
+
`Borders::Set`s or six-glyph Strings, as `style:` takes them).
|
|
471
|
+
`Tessella.alphabet` measures a file before a figure goes into it: a
|
|
472
|
+
file that is pure ASCII should get ASCII art.
|
|
473
|
+
|
|
474
|
+
### Meters
|
|
475
|
+
|
|
476
|
+
```ruby
|
|
477
|
+
Tessella.meter(0.5) # [██████████··········] 50 %
|
|
478
|
+
Tessella.meter("73%", style: :star, title: "cpu", width: 8)
|
|
479
|
+
# cpu [★★★★★★☆☆] 73 %
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
A one-line progress bar or gauge, as the ascii-art skill's ui.md
|
|
483
|
+
writes one. `fraction` is 0..1 or a percentage String; `style:` a
|
|
484
|
+
meter pair from the registry (`arrow ▸▹`, `chevron`, `classic =`,
|
|
485
|
+
`rectangle ▮▯`, `wave ~_`, `blade ▰▱`, `button ⦿⦾`, `block █·`, the
|
|
486
|
+
default, which fills its last cell by eighths, `box ■□`, `circle ●○`,
|
|
487
|
+
`star ★☆`) or a fill and a track glyph of one's own, refused when the
|
|
488
|
+
two differ in width class (the bar would change length as it fills
|
|
489
|
+
in a CJK terminal). `width:` (20), `title:`, `label: false` (no
|
|
490
|
+
percentage), `brackets: false`; `ascii: true` draws `[===== ]`.
|
|
491
|
+
On the command line: `tessella meter [--style S] [--width N] [--title
|
|
492
|
+
T] [--title-width N] [--no-label] [--no-brackets] [--ascii]
|
|
493
|
+
FRACTION`; `title_width:` (`--title-width`) pads the title, so a
|
|
494
|
+
column of meters with titles of different lengths lines up.
|
|
495
|
+
`orientation: :vertical` (`--orientation vertical`) stands the bar
|
|
496
|
+
up: a column of `height:` cells (8; `width:` is then refused) filled
|
|
497
|
+
from the bottom, the block style topped by eighths (`▁` to `▇`),
|
|
498
|
+
between `─` caps (`-` in ASCII, none under `brackets: false`), the
|
|
499
|
+
title above and the percentage below, centred on the bar; `compose
|
|
500
|
+
beside` sets several side by side.
|
|
501
|
+
|
|
502
|
+
`Tessella.grid(*figures, columns: 2, gap: 2, row_gap: 1, align:
|
|
503
|
+
:top)` sets figures out in rows of `columns`, a dashboard of charts,
|
|
504
|
+
meters and sparklines: each row side by side, the rows stacked; a
|
|
505
|
+
short last row stays short. `tessella compose grid [--columns N]
|
|
506
|
+
[--gap N] [--row-gap N] [--align A] BLOCK...`.
|
|
507
|
+
|
|
508
|
+
`Tessella.rule(width: 78, ornament: nil, style: :light)` is a
|
|
509
|
+
divider: a line in a rule style (`light ─`, `heavy ━`, `double ═`,
|
|
510
|
+
`dashed ┄`, `ascii -`), an ornament centred in it by display width:
|
|
511
|
+
`Tessella.rule(width: 24, ornament: "✦")` is `────────── ✦
|
|
512
|
+
───────────`. On the command line: `tessella rule [--width N]
|
|
513
|
+
[--ornament T] [--style S] [--ascii]`.
|
|
514
|
+
|
|
515
|
+
### Text styles
|
|
516
|
+
|
|
517
|
+
```ruby
|
|
518
|
+
Tessella.text("Hello", style: :double_struck) # => "ℍ𝕖𝕝𝕝𝕠"
|
|
519
|
+
Tessella.text("ℍ𝕖𝕝𝕝𝕠", style: :plain) # => "Hello"
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
Letters and digits in a Unicode style: `bold`, `italic`,
|
|
523
|
+
`bold-italic`, `script`, `bold-script`, `fraktur`, `bold-fraktur`,
|
|
524
|
+
`double-struck`, `sans`, `sans-bold`, `sans-italic`,
|
|
525
|
+
`sans-bold-italic`, `monospace`, `small-caps`, `superscript`,
|
|
526
|
+
`subscript`, `circled`, `negative-circled`, `parenthesized`,
|
|
527
|
+
`squared`, `negative-squared`, `fullwidth`; `:plain` turns any of
|
|
528
|
+
them back. The forms come from Unicode's own data, the gaps in the
|
|
529
|
+
mathematical alphabets filled from Letterlike Symbols (`ℎ`, `ℬ`,
|
|
530
|
+
`ℂ`, ...). A character a style has no form for -- digits outside
|
|
531
|
+
bold, double-struck, sans, sans-bold and monospace, a small-cap `x`,
|
|
532
|
+
most subscript letters -- passes through unchanged; `strict: true`
|
|
533
|
+
refuses the text instead, naming them. Capitals keep their case in
|
|
534
|
+
small caps; squared and the negative styles have capitals only.
|
|
535
|
+
Every style is non-ASCII (`ascii: true` refuses it), and `fullwidth`
|
|
536
|
+
is two columns a glyph. These are for a decorative line, not
|
|
537
|
+
documentation prose: screen readers and search see other letters.
|
|
538
|
+
|
|
539
|
+
```text
|
|
540
|
+
tessella text --style S [--strict] [TEXT ...] (stdin if none)
|
|
541
|
+
tessella text --plain [TEXT ...]
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
A letter or digit with no form is named on stderr; under `--strict`
|
|
545
|
+
the command prints nothing and exits 1.
|
|
546
|
+
|
|
547
|
+
### Sparklines
|
|
548
|
+
|
|
549
|
+
```ruby
|
|
550
|
+
Tessella.sparkline([1, 5, 22, 13, 53, 2, 8]) # ▁▂▄▃█▁▂
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
One cell a value, eight steps from the lowest to the highest (`▁` to
|
|
554
|
+
`█`, or `_.-:=+*#` under `ascii: true`). `width:` averages the values
|
|
555
|
+
into that many cells; `min:` and `max:` fix the range, so several
|
|
556
|
+
lines share one scale. `tessella sparkline [--width N] [--min A]
|
|
557
|
+
[--max B] [--ascii] [VALUE ...]` reads the values given, or stdin
|
|
558
|
+
(split on blanks and commas).
|
|
559
|
+
|
|
560
|
+
### Glyph sets
|
|
561
|
+
|
|
562
|
+
```ruby
|
|
563
|
+
Tessella::Width.class_of("▒") # => :A
|
|
564
|
+
Tessella::Glyphs.fetch(:meter, :arrow) # => a GlyphSet: glyphs "▸▹"
|
|
565
|
+
Tessella::Glyphs.fetch(:ramp, :blocks).mixed? # => true (░ N, ▒▓█ A)
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
Every glyph has a width class, as the ascii-art skill's palette names
|
|
569
|
+
them: N, narrow everywhere; A, East Asian Ambiguous (one column in
|
|
570
|
+
Western terminals, two in CJK-configured ones); W, always two.
|
|
571
|
+
`Tessella::Glyphs` names the sets the library draws with -- border
|
|
572
|
+
styles, fill patterns, meter fill/track pairs, rule lines, tone ramps,
|
|
573
|
+
arrow heads (`full` `►◄▲▼`, `small` `▸◂▴▾`, `emoji` `▶◀`)
|
|
574
|
+
-- and computes each set's classes from Unicode 15.1, never by hand.
|
|
575
|
+
A set spanning two classes is *mixed*: fine in Western terminals, but
|
|
576
|
+
it changes length in CJK ones. Each set also lists the width targets
|
|
577
|
+
it is safe for (`GlyphSet#targets`, `Glyphs.targets(glyphs)`): `term`
|
|
578
|
+
always, `cjk` with no ambiguous glyph, `web` with no wide or
|
|
579
|
+
emoji-capable one.
|
|
580
|
+
|
|
581
|
+
```text
|
|
582
|
+
tessella glyphs [KIND [NAME]] # every set, a kind, or one set's glyphs
|
|
583
|
+
tessella glyphs --target cjk # only the sets safe for a target
|
|
584
|
+
tessella glyphs --check GLYPHS # the classes of a set of one's own;
|
|
585
|
+
# exit 1 when they are mixed
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
### Width targets
|
|
589
|
+
|
|
590
|
+
> [!WARNING]
|
|
591
|
+
> **A text figure lines up only where its glyphs are as wide as
|
|
592
|
+
> tessella counted, and that is not the same everywhere.** Unicode
|
|
593
|
+
> gives some glyphs no fixed width: box drawing, block elements,
|
|
594
|
+
> `· ● ★`, Greek and accented Latin are *East Asian Ambiguous*, one
|
|
595
|
+
> column in a Western terminal and two in one set up for Chinese,
|
|
596
|
+
> Japanese or Korean. A browser adds two risks of its own: it may
|
|
597
|
+
> draw an emoji-capable glyph (`▶ ☀ ✔`) as a two-column colour emoji,
|
|
598
|
+
> and it draws any glyph its monospace font lacks from a fallback font
|
|
599
|
+
> at that font's width (in Chrome on GitHub's code fonts braille is
|
|
600
|
+
> about 1.2 columns, `▶` and `◆` two). A figure drawn for one of these
|
|
601
|
+
> places shears in another: a frame that does not close, a column of
|
|
602
|
+
> bars that drifts, a chart whose right edge wanders. Choose the
|
|
603
|
+
> target where the figure will be read.
|
|
604
|
+
|
|
605
|
+
tessella draws for three targets:
|
|
606
|
+
|
|
607
|
+
| Target | Read in | What tessella does |
|
|
608
|
+
|--------|---------|--------------------|
|
|
609
|
+
| `term` (default) | a Western terminal or editor | ambiguous glyphs count one column, wide ones (CJK, emoji, an emoji sequence) two |
|
|
610
|
+
| `cjk` | a terminal set up for CJK | ambiguous glyphs count two; structure defaults to ASCII, text is kept |
|
|
611
|
+
| `web` | a browser: a README or docs on GitHub | U+FE0E after emoji-capable glyphs; charts in quadrant blocks, not braille; `■` for `◆` |
|
|
612
|
+
|
|
613
|
+
```ruby
|
|
614
|
+
Tessella.target = :cjk # for the rest of the fiber
|
|
615
|
+
Tessella::Target.with(:web) { Tessella.table(rows).to_s }
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
On the command line: `tessella --target term|cjk|web COMMAND ...`,
|
|
619
|
+
or `TESSELLA_TARGET` (`--target` wins; an empty variable is `term`).
|
|
620
|
+
|
|
621
|
+
**`cjk`.** Every module's `ascii:` defaults to `true`, since ASCII is
|
|
622
|
+
one column everywhere: the structure is drawn in `+ - |`, while text
|
|
623
|
+
stays as given (CJK labels, `é`) and is measured with ambiguous
|
|
624
|
+
glyphs as two. `ascii: false` (`--no-ascii`) still asks for Unicode
|
|
625
|
+
structure: a frame and a table then keep every inner width a whole
|
|
626
|
+
number of border glyphs, so they close in a CJK terminal (and look a
|
|
627
|
+
column wide here and there in a Western one); other figures' Unicode
|
|
628
|
+
structure will not line up there.
|
|
629
|
+
|
|
630
|
+
**`web`.** U+FE0E, the text-presentation selector, after an
|
|
631
|
+
emoji-capable glyph keeps it a text glyph, but it fixes the glyph's
|
|
632
|
+
form, not its width: in a browser only glyphs the monospace font
|
|
633
|
+
carries line up. Measured in Chrome on GitHub's code fonts, box
|
|
634
|
+
drawing, quadrant and half blocks, eighths and shades are exactly one
|
|
635
|
+
column; so under `web` a chart defaults to quadrant blocks
|
|
636
|
+
(`--canvas braille` still asks for braille), and arrows should end in
|
|
637
|
+
`▸ ◂`, not `▶ ◀`. A sequence diagram's `► ◄` heads, chosen for their
|
|
638
|
+
look, are not emoji-capable, but a font without them gives them a
|
|
639
|
+
fallback width too.
|
|
640
|
+
|
|
641
|
+
`tessella lint --target T` checks a figure, drawn or hand-made, with
|
|
642
|
+
the target's widths, and `tessella glyphs --target T` lists the glyph
|
|
643
|
+
sets safe for it. [doc/targets.md](doc/targets.md) has renders of
|
|
644
|
+
each.
|
|
645
|
+
|
|
646
|
+
### The command line from Ruby
|
|
647
|
+
|
|
648
|
+
`require "tessella"` loads the library alone. The command classes
|
|
649
|
+
(`Tessella::CLI`, `Tessella::ChartCLI`, ...) and `optparse` come with
|
|
650
|
+
`require "tessella/cli"`, which loads the library too.
|
|
651
|
+
|
|
652
|
+
## The `tessella` command
|
|
653
|
+
|
|
654
|
+
```text
|
|
655
|
+
tessella COMMAND [options] [file]
|
|
656
|
+
tessella chart [options] [file]
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
The first argument names the command; `tessella --help` lists them:
|
|
660
|
+
`chart`, `compose`, `tree`, `table`, `bitfield`, `wave`, `seq`,
|
|
661
|
+
`image` and `lint`, each with its own `--help`. A file that cannot
|
|
662
|
+
be read is bad input (exit 1) in every command, a bad option a usage
|
|
663
|
+
error (exit 2).
|
|
664
|
+
`tessella --version` prints the version. No command, or an unknown
|
|
665
|
+
one, is a usage error (exit 2).
|
|
666
|
+
|
|
667
|
+
Reads one file argument or stdin (blank lines and `#` comments
|
|
668
|
+
skipped), draws one chart, and prints it. A line holding a comma
|
|
669
|
+
splits on commas, one holding a tab on tabs, and any other line on
|
|
670
|
+
runs of spaces, so a CSV or tab-separated label may hold spaces.
|
|
671
|
+
Columns by kind: line/scatter/stairs/density take one column of y
|
|
672
|
+
(against 1..n), two columns of x y, or more columns as x and one
|
|
673
|
+
series per further column; bar takes label and value, and refuses a
|
|
674
|
+
row with a third column (bad input, exit 1); histogram takes
|
|
675
|
+
one column of samples; box takes one or more columns, one box per
|
|
676
|
+
column; stem takes one column, or two for the back-to-back form. A
|
|
677
|
+
first row that is not all numbers (for bar, a first row whose value
|
|
678
|
+
field is not a number) names the columns.
|
|
679
|
+
|
|
680
|
+
Three renders, byte for byte as printed.
|
|
681
|
+
|
|
682
|
+
Two named series, from a header row `x sin cos` and 101 rows of data
|
|
683
|
+
generated on the spot. Drawn here for the web (`--target web`), where
|
|
684
|
+
the chart takes quadrant blocks: a browser draws braille, the default
|
|
685
|
+
canvas in a terminal, from a fallback font wider than a column, so a
|
|
686
|
+
braille chart's rows would not line up on this page.
|
|
687
|
+
|
|
688
|
+
```text
|
|
689
|
+
$ ruby -e 'puts "x sin cos"; 0.step(10, 0.1) { puts [it.round(1),
|
|
690
|
+
Math.sin(it), Math.cos(it)].join(" ") }' | \
|
|
691
|
+
tessella --target web chart --title trig --width 30 --height 6
|
|
692
|
+
trig
|
|
693
|
+
┌──────────────────────────────┐
|
|
694
|
+
1 │▀▜▄▀▀▀▚▖ ▗▀▀▀▚▟▀▀▀▚ │ sin
|
|
695
|
+
│ ▟▀▙ ▝▖ ▟▘ ▞▀▄ ▀▖ │ cos
|
|
696
|
+
│▞ ▝▚ ▝▖ ▞▘ ▗▞ ▝▖ ▜▖ │
|
|
697
|
+
│▀▀▀▀▀█▀▀▀▀▛▀▀█▀▀▀▀▛▀▀▀▀▀▛▀▀▀▜▛│
|
|
698
|
+
│ ▙ ▝▖▞ ▗▛ ▝▄ ▀│
|
|
699
|
+
-1 │ ▜▄▄▄▛▚▄▄▟▀ ▝▚▄▄▄│
|
|
700
|
+
└──────────────────────────────┘
|
|
701
|
+
0 10
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
A two-column CSV, `langs.csv`, holding `lang,loc`, `ruby,12`, `c,30`
|
|
705
|
+
and `rust,7.5`, drawn as a bar chart with the house defaults
|
|
706
|
+
(`eighths` on, `·` track):
|
|
707
|
+
|
|
708
|
+
```text
|
|
709
|
+
$ tessella chart --kind bar --width 30 langs.csv
|
|
710
|
+
┌ ┐
|
|
711
|
+
ruby ┤██████████▍··············· 12
|
|
712
|
+
c ┤██████████████████████████ 30
|
|
713
|
+
rust ┤██████▌··················· 7.5
|
|
714
|
+
└ ┘
|
|
715
|
+
```
|
|
716
|
+
|
|
717
|
+
The same chart under `--ascii`:
|
|
718
|
+
|
|
719
|
+
```text
|
|
720
|
+
$ tessella chart --kind bar --ascii --width 30 langs.csv
|
|
721
|
+
+ +
|
|
722
|
+
ruby |##########................ 12
|
|
723
|
+
c |########################## 30
|
|
724
|
+
rust |#######................... 7.5
|
|
725
|
+
+ +
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
### Options
|
|
729
|
+
|
|
730
|
+
Common to every kind: `--kind
|
|
731
|
+
line|scatter|stairs|density|bar|histogram|box|stem|heatmap|candlestick|pie`
|
|
732
|
+
(default `line`),
|
|
733
|
+
`--width N`, `--height N`, `--title S`, `--xlabel S`, `--ylabel S`,
|
|
734
|
+
`--xlim A,B`, `--ylim A,B`,
|
|
735
|
+
`--canvas
|
|
736
|
+
braille|block|quadrant|half|sextant|octant|dot|ascii|density|eighths`,
|
|
737
|
+
`--ascii`, `--color`, `--xscale ln|log2|log10` (and `--yscale` and
|
|
738
|
+
`--xdate [FMT]` for line, scatter, stairs and density, and `--yerr`
|
|
739
|
+
for line and scatter).
|
|
740
|
+
|
|
741
|
+
Kind-specific: `--bins N` and `--closed left|right` (histogram only);
|
|
742
|
+
`--style post|pre` (stairs only); `--stacked` (bar only); `--blocks
|
|
743
|
+
eighths|braille|block|quadrant|half` (bar and histogram only);
|
|
744
|
+
`--orientation horizontal|vertical`
|
|
745
|
+
(every kind but stem: the direction the value axis runs, bars and
|
|
746
|
+
boxes horizontal by default, the plotted kinds vertical); `--area`
|
|
747
|
+
and `--gradient C,C,...`
|
|
748
|
+
(line and stairs only; each `C` a name, an index `0..255` or `#rrggbb`,
|
|
749
|
+
or `none` for the series' colour); `--scale N` (stem only); `--fill G`,
|
|
750
|
+
`--no-eighths` and `--track G|none` (bar and histogram only, as above).
|
|
751
|
+
`--help` and `--version` print and exit. An option given to a kind
|
|
752
|
+
that does not take it is a usage error (exit 2) naming both; a bad
|
|
753
|
+
value for one a kind does take surfaces as the chart's own
|
|
754
|
+
`ArgumentError`, also exit 2. Bad input (a non-number where a number
|
|
755
|
+
is due, a missing field) exits 1, naming the row and the field; so
|
|
756
|
+
does a value the chart cannot draw (one past Float's range, a negative
|
|
757
|
+
pie slice, a candle whose low and high do not hold it, an x of 0 or
|
|
758
|
+
below under a log `--xscale`), raised as `Tessella::ValueError`, an
|
|
759
|
+
`ArgumentError`. Input that is not valid UTF-8 is bad input in every
|
|
760
|
+
command, naming its line.
|
|
761
|
+
|
|
762
|
+
### Colour
|
|
763
|
+
|
|
764
|
+
Colour is off by default: `--color` on the command line, `color:
|
|
765
|
+
true` in the library, turns on ANSI escapes for the chart's cells;
|
|
766
|
+
the frame, labels and legend stay plain. The default is off because
|
|
767
|
+
the first use of tessella's output is documents, where an escape code
|
|
768
|
+
is garbage.
|
|
769
|
+
|
|
770
|
+
### ASCII mode
|
|
771
|
+
|
|
772
|
+
`--ascii` (`ascii: true`) switches the canvas and the frame to
|
|
773
|
+
ASCII from the first byte: `+ - |` for the frame, `#` for bars, and an
|
|
774
|
+
ASCII glyph set for the plotted canvas instead of Unicode braille,
|
|
775
|
+
block or dot cells — no part draws Unicode and lets a later pass
|
|
776
|
+
rewrite it. Every drawing command takes `--no-ascii` too: under
|
|
777
|
+
`--target cjk`, whose default is ASCII, it asks for Unicode
|
|
778
|
+
structure (a frame or table then closes in a CJK terminal; see
|
|
779
|
+
"Width targets").
|
|
780
|
+
|
|
781
|
+
### `tessella compose`
|
|
782
|
+
|
|
783
|
+
```text
|
|
784
|
+
tessella compose frame [--title T] [--style S] [--pad N] BLOCK
|
|
785
|
+
tessella compose beside [--gap N] [--align A] BLOCK...
|
|
786
|
+
tessella compose stack [--gap N] [--rule C] [--center] BLOCK...
|
|
787
|
+
tessella compose indent --prefix STR [--suffix STR] BLOCK
|
|
788
|
+
tessella compose caption --text T [--above] BLOCK
|
|
789
|
+
tessella compose shadow [--glyph G] BLOCK
|
|
790
|
+
tessella compose fill [--pattern P] [--halo sides|around|none] BLOCK
|
|
791
|
+
(frame takes --fill P and --halo; --no-halo)
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
Each BLOCK is a file, or `-` for stdin (once). `--style` takes a
|
|
795
|
+
style's name or six glyphs, as `style:` does. Every subcommand also
|
|
796
|
+
takes `--[no-]ascii` and `--max-width N`. Bad input -- a missing file, an
|
|
797
|
+
empty block, a tab, a non-ASCII byte under `--ascii`, output wider
|
|
798
|
+
than `--max-width` -- exits 1 naming the block and line; a usage
|
|
799
|
+
mistake (an unknown style or align, `--style` other than ascii with
|
|
800
|
+
`--ascii`, a wrong number of blocks) exits 2.
|
|
801
|
+
|
|
802
|
+
```text
|
|
803
|
+
$ tessella chart --kind bar --width 20 langs.csv > plot.txt
|
|
804
|
+
$ tessella compose frame --title Load plot.txt
|
|
805
|
+
```
|
|
806
|
+
|
|
807
|
+
### The diagram commands
|
|
808
|
+
|
|
809
|
+
```text
|
|
810
|
+
tessella tree [--style S] [--ascii] [--root NAME] [file]
|
|
811
|
+
tessella table [--tsv] [--no-header] [--style S|GLYPHS] [--ascii]
|
|
812
|
+
[--align SPEC] [--title T] [file]
|
|
813
|
+
tessella bitfield [--word N] [--octets] [--no-ruler] [--abbrev] [file]
|
|
814
|
+
tessella wave [--period N] [--ascii] [--ruler] [file]
|
|
815
|
+
tessella seq [--ascii] [--numbered] [--footer] [file]
|
|
816
|
+
tessella image [--style S] [--width N] [--dither M] [--ramp STR]
|
|
817
|
+
[--gamma F] [--threshold F] [--edge-threshold F]
|
|
818
|
+
[--invert] [--ascii] [file]
|
|
819
|
+
```
|
|
820
|
+
|
|
821
|
+
Each reads its file or stdin and takes `--max-width N`, refusing
|
|
822
|
+
(exit 1) a figure wider than N columns. Bad input exits 1 naming the
|
|
823
|
+
line; a usage mistake exits 2.
|
|
824
|
+
|
|
825
|
+
```text
|
|
826
|
+
$ printf 'A -> B: request\nB --> A: ok\n' | tessella seq
|
|
827
|
+
```
|
|
828
|
+
|
|
829
|
+
### `tessella lint`
|
|
830
|
+
|
|
831
|
+
```text
|
|
832
|
+
tessella lint [--width N] [--prefix STR] [--suffix STR] [--ruler]
|
|
833
|
+
[--forbid SEQ]...
|
|
834
|
+
[--target T] [--web] [--no-wide] [--ascii]
|
|
835
|
+
[--allow-mixed]
|
|
836
|
+
[--fences] [--geometry] [--border GLYPHS]...
|
|
837
|
+
[file ...]
|
|
838
|
+
tessella lint --alphabet [file ...]
|
|
839
|
+
```
|
|
840
|
+
|
|
841
|
+
The command form of `Tessella.lint`, and the ascii-art skill's
|
|
842
|
+
`artlint.rb` under a new name, with its options and output kept:
|
|
843
|
+
findings go to stderr as `FILE:LINE: message` (`stdin` for stdin),
|
|
844
|
+
one per line; exit 0 when every input is clean, 1 on any finding or
|
|
845
|
+
an unreadable file (the other files are still linted), 2 on a usage
|
|
846
|
+
mistake. `--ruler` prints each line's width and the columns of its
|
|
847
|
+
verticals on stdout, which must agree down a figure. `--alphabet`
|
|
848
|
+
prints `FILE: ascii` or `FILE: utf-8 ...` on stdout, lints nothing
|
|
849
|
+
and exits 0. Its `--paranoid` is gone: `--ascii` is the flag.
|
|
850
|
+
`--target term|cjk|web` lints for where the figure is read (the
|
|
851
|
+
current target by default; see "Width targets"): widths are that
|
|
852
|
+
target's, `web` is `--web` and accepts a glyph followed by U+FE0E,
|
|
853
|
+
and `cjk` names ambiguous glyphs (`é`, `·`, `●`) on aligned rows.
|
|
854
|
+
|
|
855
|
+
```text
|
|
856
|
+
$ printf 'a\tb\n┌──┐ \n│ab │\n└──┘\n' | tessella lint --geometry
|
|
857
|
+
stdin:1: tab character
|
|
858
|
+
stdin:2: trailing whitespace
|
|
859
|
+
stdin:3: side of box opened at line 2 col 1 is off at col 4
|
|
860
|
+
stdin:3: connector '│' at col 5 hangs from ' '
|
|
861
|
+
```
|
|
862
|
+
|
|
863
|
+
## Relation to unicode_plot
|
|
864
|
+
|
|
865
|
+
tessella is a full rewrite, not a fork or a wrapper, of
|
|
866
|
+
[unicode_plot](https://github.com/red-data-tools/unicode_plot) (the
|
|
867
|
+
gem this project drew its chart kinds and canvases from). unicode_plot
|
|
868
|
+
serves as a test oracle only — every kind and canvas is compared
|
|
869
|
+
against it under test — and no code is copied from it; the ASCII
|
|
870
|
+
canvas's glyph rule in particular is original. Under unicode_plot's
|
|
871
|
+
own bar settings (`fill: "■"` for bars, `"▇"` for histograms, no
|
|
872
|
+
eighths, no track), tessella's output is byte-for-byte identical to
|
|
873
|
+
unicode_plot's.
|
|
874
|
+
|
|
875
|
+
A few places depart from unicode_plot on purpose: the density canvas
|
|
876
|
+
honours sizes below 5 (unicode_plot clamps to 5); histogram labels
|
|
877
|
+
survive bins of width 10 or more (unicode_plot crashes there); and an
|
|
878
|
+
axis extreme prints `10`, not `10.0`, while a plotted value keeps its
|
|
879
|
+
own form; stem leaves are exact (`0.3` at scale 1 is leaf 3, where
|
|
880
|
+
unicode_plot prints 2); constant data of 1e16 or more draws (it
|
|
881
|
+
crashes unicode_plot); negative bars draw (unicode_plot refuses
|
|
882
|
+
them); and labels in wide characters (CJK) are measured in display
|
|
883
|
+
columns. See [DESIGN.md](DESIGN.md) for why.
|
|
884
|
+
|
|
885
|
+
## Development
|
|
886
|
+
|
|
887
|
+
```bash
|
|
888
|
+
make check # preflight: every file parses, RuboCop clean
|
|
889
|
+
make tests # run the suite; ORACLE=path/to/gems also runs the
|
|
890
|
+
# unicode_plot comparisons (unicode_plot is never a
|
|
891
|
+
# runtime or install dependency)
|
|
892
|
+
make bundle # write dist/tessella.rb, the library as one file
|
|
893
|
+
```
|
|
894
|
+
|
|
895
|
+
See [DESIGN.md](DESIGN.md) for the module layout and interfaces, and
|
|
896
|
+
`make help` for every target.
|
|
897
|
+
|
|
898
|
+
## License
|
|
899
|
+
|
|
900
|
+
MIT. See [LICENSE](LICENSE).
|