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.
Files changed (188) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +900 -0
  4. data/doc/README.md +74 -0
  5. data/doc/bitfield.md +123 -0
  6. data/doc/charts.md +1370 -0
  7. data/doc/check.rb +75 -0
  8. data/doc/cli.md +483 -0
  9. data/doc/composition.md +599 -0
  10. data/doc/getting-started.md +121 -0
  11. data/doc/glyphs.md +134 -0
  12. data/doc/images.md +325 -0
  13. data/doc/library.md +239 -0
  14. data/doc/lint.md +270 -0
  15. data/doc/meter.md +210 -0
  16. data/doc/rule.md +51 -0
  17. data/doc/seq.md +169 -0
  18. data/doc/sparkline.md +63 -0
  19. data/doc/table.md +347 -0
  20. data/doc/targets.md +222 -0
  21. data/doc/text.md +84 -0
  22. data/doc/tree.md +108 -0
  23. data/doc/wave.md +122 -0
  24. data/examples/README.md +103 -0
  25. data/examples/ages.txt +9 -0
  26. data/examples/bad.csv +3 -0
  27. data/examples/boxes.txt +3 -0
  28. data/examples/broken.txt +4 -0
  29. data/examples/calibration.rb +12 -0
  30. data/examples/cities.csv +4 -0
  31. data/examples/cjk.rb +13 -0
  32. data/examples/comment.rb +9 -0
  33. data/examples/comment.sh +13 -0
  34. data/examples/cpu.txt +60 -0
  35. data/examples/dashboard.sh +25 -0
  36. data/examples/datasheet.rb +16 -0
  37. data/examples/disc.pgm +0 -0
  38. data/examples/disks.sh +18 -0
  39. data/examples/disks.txt +5 -0
  40. data/examples/dose.txt +7 -0
  41. data/examples/drift.txt +4 -0
  42. data/examples/ethernet.txt +3 -0
  43. data/examples/figure.c +5 -0
  44. data/examples/fit.rb +16 -0
  45. data/examples/floors.rb +21 -0
  46. data/examples/gallery.sh +20 -0
  47. data/examples/gauges.sh +31 -0
  48. data/examples/gradient.rb +16 -0
  49. data/examples/growth.txt +6 -0
  50. data/examples/hosts.csv +4 -0
  51. data/examples/icmp.txt +3 -0
  52. data/examples/ipv4.txt +12 -0
  53. data/examples/kinds.csv +4 -0
  54. data/examples/langs.csv +4 -0
  55. data/examples/lint.rb +16 -0
  56. data/examples/load.rb +14 -0
  57. data/examples/login.txt +10 -0
  58. data/examples/neg.csv +4 -0
  59. data/examples/normal.txt +200 -0
  60. data/examples/note.txt +2 -0
  61. data/examples/notes-fixed.md +17 -0
  62. data/examples/notes.md +17 -0
  63. data/examples/ohlc.txt +7 -0
  64. data/examples/os.csv +10 -0
  65. data/examples/outline.txt +8 -0
  66. data/examples/plot.txt +5 -0
  67. data/examples/refusals.rb +17 -0
  68. data/examples/report.sh +21 -0
  69. data/examples/rfc.sh +22 -0
  70. data/examples/samples.txt +7 -0
  71. data/examples/shapes.pgm +0 -0
  72. data/examples/sizes.tsv +3 -0
  73. data/examples/spi.txt +6 -0
  74. data/examples/status.sh +23 -0
  75. data/examples/stem.txt +12 -0
  76. data/examples/table-drift.txt +4 -0
  77. data/examples/temps.txt +31 -0
  78. data/examples/trig.sh +13 -0
  79. data/examples/trig.txt +102 -0
  80. data/examples/week.txt +8 -0
  81. data/examples/wide.txt +2 -0
  82. data/exe/tessella +6 -0
  83. data/lib/tessella/axis.rb +71 -0
  84. data/lib/tessella/bitfield.rb +194 -0
  85. data/lib/tessella/block.rb +56 -0
  86. data/lib/tessella/borders.rb +85 -0
  87. data/lib/tessella/canvas/area.rb +155 -0
  88. data/lib/tessella/canvas/ascii.rb +120 -0
  89. data/lib/tessella/canvas/block.rb +40 -0
  90. data/lib/tessella/canvas/braille.rb +30 -0
  91. data/lib/tessella/canvas/clip.rb +49 -0
  92. data/lib/tessella/canvas/density.rb +51 -0
  93. data/lib/tessella/canvas/dot.rb +27 -0
  94. data/lib/tessella/canvas/eighths.rb +43 -0
  95. data/lib/tessella/canvas/half.rb +51 -0
  96. data/lib/tessella/canvas/sextant.rb +36 -0
  97. data/lib/tessella/canvas.rb +168 -0
  98. data/lib/tessella/cell.rb +10 -0
  99. data/lib/tessella/chart.rb +211 -0
  100. data/lib/tessella/check.rb +52 -0
  101. data/lib/tessella/cli/bitfield.rb +121 -0
  102. data/lib/tessella/cli/chart.rb +615 -0
  103. data/lib/tessella/cli/chart_candle.rb +36 -0
  104. data/lib/tessella/cli/chart_heatmap.rb +21 -0
  105. data/lib/tessella/cli/chart_help.rb +85 -0
  106. data/lib/tessella/cli/chart_pie.rb +38 -0
  107. data/lib/tessella/cli/compose.rb +156 -0
  108. data/lib/tessella/cli/compose_options.rb +259 -0
  109. data/lib/tessella/cli/glyphs.rb +97 -0
  110. data/lib/tessella/cli/image.rb +250 -0
  111. data/lib/tessella/cli/input.rb +44 -0
  112. data/lib/tessella/cli/iso_time.rb +28 -0
  113. data/lib/tessella/cli/lint.rb +102 -0
  114. data/lib/tessella/cli/lint_options.rb +99 -0
  115. data/lib/tessella/cli/main.rb +121 -0
  116. data/lib/tessella/cli/meter.rb +80 -0
  117. data/lib/tessella/cli/rule.rb +57 -0
  118. data/lib/tessella/cli/seq.rb +117 -0
  119. data/lib/tessella/cli/sparkline.rb +72 -0
  120. data/lib/tessella/cli/table.rb +167 -0
  121. data/lib/tessella/cli/text.rb +78 -0
  122. data/lib/tessella/cli/tree.rb +100 -0
  123. data/lib/tessella/cli/wave.rb +148 -0
  124. data/lib/tessella/cli.rb +36 -0
  125. data/lib/tessella/color.rb +106 -0
  126. data/lib/tessella/compose.rb +243 -0
  127. data/lib/tessella/compose_fill.rb +176 -0
  128. data/lib/tessella/compose_grid.rb +36 -0
  129. data/lib/tessella/compose_shadow.rb +41 -0
  130. data/lib/tessella/error.rb +13 -0
  131. data/lib/tessella/frame.rb +170 -0
  132. data/lib/tessella/glyphs.rb +106 -0
  133. data/lib/tessella/image.rb +454 -0
  134. data/lib/tessella/image_octant.rb +46 -0
  135. data/lib/tessella/image_petscii.rb +116 -0
  136. data/lib/tessella/kinds/bar.rb +344 -0
  137. data/lib/tessella/kinds/box.rb +213 -0
  138. data/lib/tessella/kinds/box_upright.rb +110 -0
  139. data/lib/tessella/kinds/candlestick.rb +164 -0
  140. data/lib/tessella/kinds/columns.rb +259 -0
  141. data/lib/tessella/kinds/date_axis.rb +102 -0
  142. data/lib/tessella/kinds/error_bars.rb +71 -0
  143. data/lib/tessella/kinds/heatmap.rb +130 -0
  144. data/lib/tessella/kinds/line.rb +173 -0
  145. data/lib/tessella/kinds/multibar.rb +98 -0
  146. data/lib/tessella/kinds/multibar_columns.rb +130 -0
  147. data/lib/tessella/kinds/multibar_fine.rb +45 -0
  148. data/lib/tessella/kinds/multibar_rows.rb +93 -0
  149. data/lib/tessella/kinds/pie.rb +123 -0
  150. data/lib/tessella/kinds/pie_rim.rb +78 -0
  151. data/lib/tessella/kinds/sideways.rb +62 -0
  152. data/lib/tessella/kinds/stem.rb +134 -0
  153. data/lib/tessella/kinds/threshold.rb +73 -0
  154. data/lib/tessella/legend.rb +31 -0
  155. data/lib/tessella/lint/alphabet.rb +56 -0
  156. data/lib/tessella/lint/drift.rb +82 -0
  157. data/lib/tessella/lint/fences.rb +51 -0
  158. data/lib/tessella/lint/geometry.rb +120 -0
  159. data/lib/tessella/lint/geometry_checks.rb +224 -0
  160. data/lib/tessella/lint/lines.rb +115 -0
  161. data/lib/tessella/lint/mixing.rb +61 -0
  162. data/lib/tessella/lint/runner.rb +73 -0
  163. data/lib/tessella/lint/shadows.rb +120 -0
  164. data/lib/tessella/lint/target_rows.rb +66 -0
  165. data/lib/tessella/lint.rb +158 -0
  166. data/lib/tessella/meter.rb +133 -0
  167. data/lib/tessella/meter_upright.rb +43 -0
  168. data/lib/tessella/png.rb +456 -0
  169. data/lib/tessella/raster.rb +92 -0
  170. data/lib/tessella/rmagick_reader.rb +52 -0
  171. data/lib/tessella/rule.rb +59 -0
  172. data/lib/tessella/seq.rb +522 -0
  173. data/lib/tessella/sparkline.rb +57 -0
  174. data/lib/tessella/stats.rb +199 -0
  175. data/lib/tessella/table.rb +547 -0
  176. data/lib/tessella/table_cells.rb +225 -0
  177. data/lib/tessella/target.rb +105 -0
  178. data/lib/tessella/text.rb +76 -0
  179. data/lib/tessella/text_forms.rb +150 -0
  180. data/lib/tessella/tree.rb +221 -0
  181. data/lib/tessella/version.rb +5 -0
  182. data/lib/tessella/wave.rb +353 -0
  183. data/lib/tessella/width.rb +122 -0
  184. data/lib/tessella/width_class.rb +77 -0
  185. data/lib/tessella/width_invisible.rb +66 -0
  186. data/lib/tessella/width_invisible_high.rb +60 -0
  187. data/lib/tessella.rb +94 -0
  188. 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).