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
@@ -0,0 +1,199 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tessella
4
+ # Binning, percentiles, and the value transforms a chart's scale can
5
+ # apply before drawing (DESIGN.md, Stats and Axis).
6
+ module Stats
7
+ # edges ascending; counts.size == edges.size - 1; every value
8
+ # lands in exactly one bin (DESIGN.md).
9
+ Histogram = Data.define(:edges, :counts, :closed)
10
+
11
+ TRANSFORMS = {
12
+ ln: ->(x) { Math.log(x) },
13
+ log2: ->(x) { Math.log2(x) },
14
+ log10: ->(x) { Math.log10(x) }
15
+ }.freeze
16
+ IDENTITY = ->(x) { x }.freeze
17
+
18
+ # bins: nil picks the count enumerable-statistics' Array#histogram
19
+ # picks for the same data (Sturges' rule); closed: :left means
20
+ # [a, b), :right (a, b].
21
+ def self.histogram(values, bins: nil, closed: :left)
22
+ left = left_closed?(closed)
23
+ return empty_histogram(left) if values.empty?
24
+
25
+ edges = histogram_edges(values, bins, left)
26
+ counts = bin_counts(values, edges, left)
27
+ Histogram.new(edges: edges, counts: counts,
28
+ closed: left ? :left : :right)
29
+ end
30
+
31
+ # ps in 0..100, linear interpolation between order statistics
32
+ # (the definition enumerable-statistics uses).
33
+ def self.percentiles(values, ps)
34
+ sorted = values.sort
35
+ ps.map { percentile(sorted, it) }
36
+ end
37
+
38
+ def self.transform(name)
39
+ return IDENTITY if name.nil? || name == :identity
40
+
41
+ TRANSFORMS.fetch(name) do
42
+ raise ArgumentError, "unknown transform: #{name.inspect}"
43
+ end
44
+ end
45
+
46
+ def self.left_closed?(closed)
47
+ return true if closed == :left
48
+ return false if closed == :right
49
+
50
+ raise ArgumentError,
51
+ "closed must be :left or :right, got #{closed.inspect}"
52
+ end
53
+
54
+ def self.empty_histogram(left)
55
+ Histogram.new(edges: [0.0], counts: [],
56
+ closed: left ? :left : :right)
57
+ end
58
+
59
+ def self.percentile(sorted, p)
60
+ raise ArgumentError, "percentile out of bounds" if
61
+ p.negative? || p > 100
62
+
63
+ n = sorted.size
64
+ return sorted.first.to_f if n == 1
65
+
66
+ d = (n - 1) * p / 100.0
67
+ i = d.floor
68
+ interpolate(sorted, i, d - i, n)
69
+ end
70
+
71
+ def self.interpolate(sorted, i, f, n)
72
+ return sorted[i].to_f if f.zero? || i == n - 1
73
+
74
+ ((sorted[i] * (1 - f)) + (sorted[i + 1] * f)).to_f
75
+ end
76
+
77
+ def self.histogram_edges(values, bins, left)
78
+ nbins = bins || sturges(values.size)
79
+ raise ArgumentError, "bins must be >= 1, got #{nbins}" if
80
+ nbins < 1
81
+
82
+ lo, hi = values.minmax.map(&:to_f)
83
+ Edges.calculate(lo, hi, nbins, left)
84
+ end
85
+
86
+ def self.sturges(n) = n.zero? ? 1 : (Math.log2(n).ceil + 1)
87
+
88
+ def self.bin_counts(values, edges, left)
89
+ counts = Array.new(edges.size - 1, 0)
90
+ values.each do |v|
91
+ bi = bin_index(edges, v.to_f, left)
92
+ counts[bi] += 1 if bi.between?(0, counts.size - 1)
93
+ end
94
+ counts
95
+ end
96
+
97
+ def self.bin_index(edges, x, left)
98
+ inside = left ? ->(e) { e <= x } : ->(e) { e < x }
99
+ lo = -1
100
+ hi = edges.size
101
+ while hi - lo > 1
102
+ mid = lo + ((hi - lo) / 2)
103
+ inside.call(edges[mid]) ? lo = mid : hi = mid
104
+ end
105
+ left ? lo : hi - 1
106
+ end
107
+
108
+ # The edges unicode_plot's Array#histogram computes: a bin width
109
+ # rounded to a nice step (1/2/5 x a power of ten), then widened
110
+ # on both sides until it covers the data (closed-side dependent).
111
+ module Edges
112
+ State = Struct.new(:start, :step, :divisor, :len)
113
+
114
+ def self.calculate(lo, hi, nbins, left)
115
+ state = state_for(lo, hi, nbins)
116
+ extend!(state, lo, hi, left)
117
+ build(state)
118
+ end
119
+
120
+ def self.state_for(lo, hi, nbins)
121
+ return State.new(hi, unit_step(hi), 1.0, 1) if lo == hi
122
+
123
+ bw = (hi - lo) / nbins
124
+ lbw = Math.log10(bw)
125
+ return wide(lo, hi, bw, lbw) if lbw >= 0
126
+
127
+ narrow(lo, hi, bw, lbw)
128
+ end
129
+
130
+ # A constant's one bin is [v, v + 1), as unicode_plot's; from
131
+ # about 1e16 up v + 1 == v, so the width grows by tens until
132
+ # the bin holds v.
133
+ def self.unit_step(v)
134
+ step = 1.0
135
+ step *= 10 while v + step == v
136
+ step
137
+ end
138
+
139
+ def self.wide(lo, hi, bw, lbw)
140
+ base = 10.0**lbw.floor
141
+ step = base * multiplier(bw / base)
142
+ start = step * (lo / step).floor
143
+ State.new(start, step, 1.0, ((hi - start) / step).ceil)
144
+ end
145
+
146
+ def self.narrow(lo, hi, bw, lbw)
147
+ base = 10.0**-lbw.floor
148
+ divisor = base / multiplier(bw * base)
149
+ start = (lo * divisor).floor
150
+ State.new(start, 1.0, divisor,
151
+ ((hi * divisor) - start).ceil)
152
+ end
153
+
154
+ def self.multiplier(r)
155
+ return 1 if r <= 1.1
156
+ return 2 if r <= 2.2
157
+ return 5 if r <= 5.5
158
+
159
+ 10
160
+ end
161
+
162
+ def self.extend!(state, lo, hi, left)
163
+ shrink!(state, lo, left)
164
+ grow!(state, hi, left)
165
+ end
166
+
167
+ def self.shrink!(state, lo, left)
168
+ if left
169
+ state.start -= state.step while
170
+ lo < state.start / state.divisor
171
+ else
172
+ state.start -= state.step while
173
+ lo <= state.start / state.divisor
174
+ end
175
+ end
176
+
177
+ def self.grow!(state, hi, left)
178
+ if left
179
+ state.len += 1 while at(state, state.len) <= hi
180
+ else
181
+ state.len += 1 while at(state, state.len) < hi
182
+ end
183
+ end
184
+
185
+ def self.at(state, len)
186
+ (state.start + ((len - 1) * state.step)) / state.divisor
187
+ end
188
+
189
+ def self.build(state)
190
+ edges = []
191
+ state.len.times do
192
+ edges << (state.start / state.divisor)
193
+ state.start += state.step
194
+ end
195
+ edges
196
+ end
197
+ end
198
+ end
199
+ end