tty-sparkline 0.1.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/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +490 -0
- data/lib/tty-sparkline.rb +3 -0
- data/lib/tty/sparkline.rb +330 -0
- data/lib/tty/sparkline/version.rb +7 -0
- metadata +100 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 72e8765643f86155bdfe16df48b7342424e37d91758eead94bc23a981c4fc092
|
4
|
+
data.tar.gz: '094ae4841f7784550040d5e2036ffb222e8412bc0f617e7e9374440b4c6c6148'
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 660020d0cd95f23f1739d64f2e17bada95fd8db104746c1be3fa0e28ddbef97e82f9e857c108da117555bd555a43204f9cb3e5facd7ab4a1f70122752a1b1ac2
|
7
|
+
data.tar.gz: 1035f0cd07ccea2f1a918d504bab82398b3b5581a1274536440b7d4005814d02435c0267bb38e6742299983bf562d3a6c7d00c5a7560fddb0fa11ac31f272d9c
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2021 Piotr Murach (piotrmurach.com)
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,490 @@
|
|
1
|
+
<div align="center">
|
2
|
+
<a href="https://ttytoolkit.org"><img width="130" src="https://github.com/piotrmurach/tty/raw/master/images/tty.png" alt="TTY Toolkit logo" /></a>
|
3
|
+
</div>
|
4
|
+
|
5
|
+
# TTY::Sparkline
|
6
|
+
|
7
|
+
[][gem]
|
8
|
+
[][gh_actions_ci]
|
9
|
+
[][appveyor]
|
10
|
+
[][codeclimate]
|
11
|
+
[][coverage]
|
12
|
+
[][inchpages]
|
13
|
+
|
14
|
+
[gem]: https://badge.fury.io/rb/tty-sparkline
|
15
|
+
[gh_actions_ci]: https://github.com/piotrmurach/tty-sparkline/actions?query=workflow%3ACI
|
16
|
+
[appveyor]: https://ci.appveyor.com/project/piotrmurach/tty-sparkline
|
17
|
+
[codeclimate]: https://codeclimate.com/github/piotrmurach/tty-sparkline/maintainability
|
18
|
+
[coverage]: https://coveralls.io/github/piotrmurach/tty-sparkline
|
19
|
+
[inchpages]: https://inch-ci.org/github/piotrmurach/tty-sparkline
|
20
|
+
|
21
|
+
> Sparkline charts for terminal applications.
|
22
|
+
|
23
|
+
**TTY::Sparkline** provides sparkline drawing component for [TTY](https://github.com/piotrmurach/tty) toolkit.
|
24
|
+
|
25
|
+

|
26
|
+
|
27
|
+
## Installation
|
28
|
+
|
29
|
+
Add this line to your application's Gemfile:
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
gem "tty-sparkline"
|
33
|
+
```
|
34
|
+
|
35
|
+
And then execute:
|
36
|
+
|
37
|
+
$ bundle install
|
38
|
+
|
39
|
+
Or install it yourself as:
|
40
|
+
|
41
|
+
$ gem install tty-sparkline
|
42
|
+
|
43
|
+
## Contents
|
44
|
+
|
45
|
+
* [1. Usage](#1-usage)
|
46
|
+
* [2. Interface](#2-interface)
|
47
|
+
* [2.1 new](#21-new)
|
48
|
+
* [2.2 append](#22-append)
|
49
|
+
* [2.3 render](#23-render)
|
50
|
+
* [3. Configuration](#3-configuration)
|
51
|
+
* [3.1 :top](#31-top)
|
52
|
+
* [3.2 :left](#32-left)
|
53
|
+
* [3.3 :height](#33-height)
|
54
|
+
* [3.4 :width](#34-width)
|
55
|
+
* [3.5 :min](#35-min)
|
56
|
+
* [3.6 :max](#36-max)
|
57
|
+
* [3.7 :bars](#37-bars)
|
58
|
+
* [3.8 :buffer_size](#38-buffer_size)
|
59
|
+
* [3.9 :non_numeric](#39-non_numeric)
|
60
|
+
|
61
|
+
## 1. Usage
|
62
|
+
|
63
|
+
To display a sparkline chart, first, create an instance with some data:
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
sparkline = TTY::Sparkline.new(1..8)
|
67
|
+
```
|
68
|
+
|
69
|
+
Then invoke `render` to generate the chart:
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
sparkline.render
|
73
|
+
# => "▁▂▃▄▅▆▇█"
|
74
|
+
```
|
75
|
+
|
76
|
+
To display the sparkline in the terminal, print it like so:
|
77
|
+
|
78
|
+
```ruby
|
79
|
+
print sparkline.render
|
80
|
+
```
|
81
|
+
|
82
|
+
This will result in the following output:
|
83
|
+
|
84
|
+
```
|
85
|
+
▁▂▃▄▅▆▇█
|
86
|
+
```
|
87
|
+
|
88
|
+
A sparkline can be positioned anywhere within the terminal screen using [:top](#31-top) and [:left](#32-left) keywords:
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
sparkline = TTY::Sparkline.new(1..8, top: 10, left: 50)
|
92
|
+
```
|
93
|
+
|
94
|
+
A sparkline can have custom size given by the [:height](#33-height) and [:width](#34-width) keywords:
|
95
|
+
|
96
|
+
```ruby
|
97
|
+
sparkline = TTY::Sparkline.new(1..8, height: 5, width: 100)
|
98
|
+
```
|
99
|
+
|
100
|
+
To restrict data values use [:min](#35-min) and [:max](#36-max):
|
101
|
+
|
102
|
+
```ruby
|
103
|
+
sparkline = TTY::Sparkline.new(1..8, min: 0, max: 5)
|
104
|
+
```
|
105
|
+
|
106
|
+
## 2. Interface
|
107
|
+
|
108
|
+
### 2.1 new
|
109
|
+
|
110
|
+
To render a new sparkline chart, first, create an instance:
|
111
|
+
|
112
|
+
```ruby
|
113
|
+
sparkline = TTY::Sparkline.new
|
114
|
+
```
|
115
|
+
|
116
|
+
If you have a static set of data items you can provide them during initialisation with the `:data` keyword that accepts both `Array` and `Range` types:
|
117
|
+
|
118
|
+
```ruby
|
119
|
+
sparkline = TTY::Sparkline.new(1..8)
|
120
|
+
sparkline = TTY::Sparkline.new([1, 2, 3, 4, 5, 6, 7, 8])
|
121
|
+
```
|
122
|
+
|
123
|
+
However, if you don't know data upfront you can [append](#22-append) it after instantiation:
|
124
|
+
|
125
|
+
```ruby
|
126
|
+
sparkline << 1 << 2 << 3 << 4
|
127
|
+
```
|
128
|
+
|
129
|
+
### 2.2 append
|
130
|
+
|
131
|
+
After a sparkline is created, you can further add more data items using the `push` method:
|
132
|
+
|
133
|
+
```ruby
|
134
|
+
sparkline.push(1).push(2)
|
135
|
+
sparkline.push(3, 4)
|
136
|
+
```
|
137
|
+
|
138
|
+
The `push` is also aliased as `append` and `<<` operator:
|
139
|
+
|
140
|
+
```ruby
|
141
|
+
sparkline.append(5).append(6)
|
142
|
+
sparkline << 7 << 8
|
143
|
+
```
|
144
|
+
|
145
|
+
Then calling `render` will result in the following:
|
146
|
+
|
147
|
+
```ruby
|
148
|
+
sparkline.render
|
149
|
+
# => "▁▂▃▄▅▆▇█"
|
150
|
+
````
|
151
|
+
|
152
|
+
This method is particularly useful if you intend to stream data. When streaming it's advisable to specify the maximum [width](#34-width) and position that the chart is going to span:
|
153
|
+
|
154
|
+
```ruby
|
155
|
+
sparkline = TTY::Sparkline.new(left: 0, top: 0, width: 50)
|
156
|
+
````
|
157
|
+
|
158
|
+
And then you can stream data, for example, from some service like so:
|
159
|
+
|
160
|
+
```ruby
|
161
|
+
loop do
|
162
|
+
# fetch next value(s) from another service etc.
|
163
|
+
|
164
|
+
sparkline << value # send value(s) to the chart
|
165
|
+
print sparkline.render # render the chart out to the console
|
166
|
+
end
|
167
|
+
```
|
168
|
+
|
169
|
+
### 2.3 render
|
170
|
+
|
171
|
+
Once you have a sparkline instance with some data:
|
172
|
+
|
173
|
+
```ruby
|
174
|
+
sparkline = TTY::Sparkline.new(1..8)
|
175
|
+
```
|
176
|
+
|
177
|
+
To show the sparkline chart use the `render` method like so:
|
178
|
+
|
179
|
+
```ruby
|
180
|
+
sparkline.render
|
181
|
+
# => "▁▂▃▄▅▆▇█"
|
182
|
+
```
|
183
|
+
|
184
|
+
To display the chart in the terminal, you need to print it:
|
185
|
+
|
186
|
+
```ruby
|
187
|
+
print sparkline.render
|
188
|
+
```
|
189
|
+
|
190
|
+
This will result in the following output:
|
191
|
+
|
192
|
+
```
|
193
|
+
▁▂▃▄▅▆▇█
|
194
|
+
```
|
195
|
+
|
196
|
+
The render method also accepts a block that provides access to the current value, bar character, column and row.
|
197
|
+
|
198
|
+
For example, to insert a space between each bar, we could do the following:
|
199
|
+
|
200
|
+
```ruby
|
201
|
+
sparkline.render do |val, bar, col, row|
|
202
|
+
col == 7 ? bar : "#{bar} "
|
203
|
+
end
|
204
|
+
```
|
205
|
+
|
206
|
+
That would result in:
|
207
|
+
|
208
|
+
```ruby
|
209
|
+
# => "▁ ▂ ▃ ▄ ▅ ▆ ▇ █"
|
210
|
+
```
|
211
|
+
|
212
|
+
The block form is useful for applying colours, for example, to mark the lowest or the highest value.
|
213
|
+
|
214
|
+
For instance, you can use [pastel](https://github.com/piotrmurach/pastel) to colour the maximum cyan, minimum red and all the other bars green:
|
215
|
+
|
216
|
+
|
217
|
+
```ruby
|
218
|
+
pastel = Pastel.new
|
219
|
+
|
220
|
+
sparkline.render do |val, bar, col, row|
|
221
|
+
if val == 8
|
222
|
+
pastel.cyan(bar)
|
223
|
+
elsif val == 1
|
224
|
+
pastel.red(bar)
|
225
|
+
else
|
226
|
+
pastel.green(bar)
|
227
|
+
end
|
228
|
+
end
|
229
|
+
```
|
230
|
+
|
231
|
+
## 3. Configuration
|
232
|
+
|
233
|
+
### 3.1 `:top`
|
234
|
+
|
235
|
+
By default, a sparkline will not be positioned. To position it against the top of the terminal screen use `:top` keyword. For example, to place a sparkline 10 lines down from the top of the screen do:
|
236
|
+
|
237
|
+
```ruby
|
238
|
+
TTY::Sparkline.new(top: 10)
|
239
|
+
```
|
240
|
+
|
241
|
+
Add [:left](#32-left) to position against the left of the terminal screen.
|
242
|
+
|
243
|
+
To dynamically position a sparkline within a terminal window consider using [tty-screen](https://github.com/piotrmurach/tty-screen) for gathering screen size. For example, to centre align against the vertical axis do:
|
244
|
+
|
245
|
+
```ruby
|
246
|
+
TTY::Sparkline.new(top: TTY::Screen.height / 2)
|
247
|
+
```
|
248
|
+
|
249
|
+
### 3.2 `:left`
|
250
|
+
|
251
|
+
By default, a sparkline will not be positioned. To position it against the left side of the terminal screen use `:left` keyword. For example, to place a sparkline `50` columns away from the left side of the terminal window do:
|
252
|
+
|
253
|
+
```ruby
|
254
|
+
TTY::Sparkline.new(left: 50)
|
255
|
+
```
|
256
|
+
|
257
|
+
Add [:top](#31-top) to position against the top of the terminal screen.
|
258
|
+
|
259
|
+
To dynamically position a sparkline within a terminal window consider using [tty-screen](https://github.com/piotrmurach/tty-screen) for gathering screen size. For example, to centre align against horizontal axis do:
|
260
|
+
|
261
|
+
```ruby
|
262
|
+
TTY::Sparkline.new(left: TTY::Screen.width / 2)
|
263
|
+
```
|
264
|
+
|
265
|
+
### 3.3 `:height`
|
266
|
+
|
267
|
+
By default, a sparkline will be rendered within a single terminal line. To change a chart to span more than one line use `:height` keyword. For example to display a sparkline on `3` lines do:
|
268
|
+
|
269
|
+
```ruby
|
270
|
+
TTY::Sparkline.new(1..8, height: 3)
|
271
|
+
```
|
272
|
+
|
273
|
+
Then rendering the sparkline in the console:
|
274
|
+
|
275
|
+
```ruby
|
276
|
+
print sparkline.render
|
277
|
+
````
|
278
|
+
|
279
|
+
We would get the following output:
|
280
|
+
|
281
|
+
```
|
282
|
+
▃▆
|
283
|
+
▂▅███
|
284
|
+
▁▄▇█████
|
285
|
+
```
|
286
|
+
|
287
|
+
### 3.4 `:width`
|
288
|
+
|
289
|
+
By default, a sparkline will be rendered in as many terminal columns as there are data items. To restrict the chart to a limited number of columns use the `:width` keyword. For example, to display a sparkline up to a maximum of `5` columns do:
|
290
|
+
|
291
|
+
```ruby
|
292
|
+
TTY::Sparkline.new(1..8, width: 5)
|
293
|
+
```
|
294
|
+
|
295
|
+
Then by rendering the sparkline in the console:
|
296
|
+
|
297
|
+
```ruby
|
298
|
+
print sparkline.render
|
299
|
+
```
|
300
|
+
|
301
|
+
We would generate the following limited output:
|
302
|
+
|
303
|
+
```
|
304
|
+
▄▅▆▇█
|
305
|
+
```
|
306
|
+
|
307
|
+
This option can be combined with the [:height](#33-height):
|
308
|
+
|
309
|
+
```ruby
|
310
|
+
TTY::Sparkline.new(1..8, height: 3, width: 5)
|
311
|
+
```
|
312
|
+
|
313
|
+
The result of rendering the above sparkline would be as follows:
|
314
|
+
|
315
|
+
```
|
316
|
+
▃▆
|
317
|
+
▂▅███
|
318
|
+
█████
|
319
|
+
```
|
320
|
+
|
321
|
+
### 3.5 `:min`
|
322
|
+
|
323
|
+
By default, the minimum value will be calculated from the entire data set. For example, given the following data:
|
324
|
+
|
325
|
+
```ruby
|
326
|
+
sparkline = TTY::Sparkline.new([100, 75, 100, 50, 80])
|
327
|
+
```
|
328
|
+
|
329
|
+
When displayed in the console, you will see the following:
|
330
|
+
|
331
|
+
```
|
332
|
+
█▄█▁▅
|
333
|
+
```
|
334
|
+
|
335
|
+
You will notice that the value of `50` looks like it's almost zero. This is because the sparkline automatically scales bars against the minimum value to make the differences between values more visible.
|
336
|
+
|
337
|
+
To change this, you can provide a custom minimum value using the `:min` keyword:
|
338
|
+
|
339
|
+
```ruby
|
340
|
+
sparkline = TTY::Sparkline.new([100, 75, 100, 50, 80], min: 0)
|
341
|
+
```
|
342
|
+
|
343
|
+
This time the display will show `50` as a more prominent value at the cost of making the difference between other values less striking:
|
344
|
+
|
345
|
+
```
|
346
|
+
█▆█▄▆
|
347
|
+
```
|
348
|
+
|
349
|
+
### 3.6 `:max`
|
350
|
+
|
351
|
+
By default, the maximum value will be calculated from the entire data set. For example, given the following data:
|
352
|
+
|
353
|
+
```ruby
|
354
|
+
sparkline = TTY::Sparkline.new([100, 75, 300, 50, 80])
|
355
|
+
```
|
356
|
+
|
357
|
+
When displayed in the console, you will see the following:
|
358
|
+
|
359
|
+
```
|
360
|
+
▂▁█▁▁
|
361
|
+
```
|
362
|
+
|
363
|
+
You will notice that the value of `300` makes all the remaining numbers look like insignificant zeros. This is because the sparkline automatically scales bars against the maximum value to make the differences between values more prominent.
|
364
|
+
|
365
|
+
To change this, you can provide a custom maximum value using the `:max` keyword:
|
366
|
+
|
367
|
+
```ruby
|
368
|
+
sparkline = TTY::Sparkline.new([100, 75, 300, 50, 80], max: 100)
|
369
|
+
```
|
370
|
+
|
371
|
+
This time the display will limit values and reduce `300` to the height of `100` making all the remaining values more visible.
|
372
|
+
|
373
|
+
```
|
374
|
+
█▄█▁▅
|
375
|
+
```
|
376
|
+
|
377
|
+
### 3.7 `:bars`
|
378
|
+
|
379
|
+
There are eight bar types used to display values in a sparkline chart. These can be changed with the `:bars` keyword to a different set of characters:
|
380
|
+
|
381
|
+
```ruby
|
382
|
+
sparkline = TTY::Sparkline.new(1..8, bars: %w[_ - = ^])
|
383
|
+
```
|
384
|
+
|
385
|
+
Then rendering the chart will output:
|
386
|
+
|
387
|
+
```ruby
|
388
|
+
sparkline.render
|
389
|
+
# => "___--==^"
|
390
|
+
```
|
391
|
+
|
392
|
+
### 3.8 `:buffer_size`
|
393
|
+
|
394
|
+
By default, the amount of data values that can be [appended](#22-append) to a sparkline is limited to `16K`. This helps to keep the memory size down when streaming data. You can change it with the `:buffer_size` keyword.
|
395
|
+
|
396
|
+
For example, to keep the last 5 data values do:
|
397
|
+
|
398
|
+
```ruby
|
399
|
+
sparkline = TTY::Sparkline.new(buffer_size: 5, min: 0)
|
400
|
+
```
|
401
|
+
|
402
|
+
Then exceeding the number of values:
|
403
|
+
|
404
|
+
```ruby
|
405
|
+
sparkline.push(1, 2, 3, 4, 5, 6, 7, 8)
|
406
|
+
```
|
407
|
+
|
408
|
+
Will cause the render to truncate the output:
|
409
|
+
|
410
|
+
```ruby
|
411
|
+
sparkline.render
|
412
|
+
# => "▄▅▆▇█"
|
413
|
+
````
|
414
|
+
|
415
|
+
### 3.9 `:non_numeric`
|
416
|
+
|
417
|
+
To instruct a sparkline on how to deal with non-numeric data use the `:non_numeric` keyword.
|
418
|
+
|
419
|
+
Below are the possible values:
|
420
|
+
|
421
|
+
* `:empty` - shows empty spaces (default)
|
422
|
+
* `:ignore` - skips displaying anything
|
423
|
+
* `:minimum` - shows the smallest bar
|
424
|
+
|
425
|
+
Given data with some non-numeric values like `"foo"`, `nil` and `""`:
|
426
|
+
|
427
|
+
```ruby
|
428
|
+
data = [1, 2, "foo", 4, nil, 6, "", 8]
|
429
|
+
```
|
430
|
+
|
431
|
+
When you don't specify the `:non_numeric` keyword, it will be set to `:empty` by default:
|
432
|
+
|
433
|
+
```ruby
|
434
|
+
sparkline = TTY::Sparkline.new(data)
|
435
|
+
```
|
436
|
+
|
437
|
+
This will then display any non-numeric values as empty spaces:
|
438
|
+
|
439
|
+
```ruby
|
440
|
+
sparkline.render
|
441
|
+
# => "▁▂ ▄ ▆ █"
|
442
|
+
```
|
443
|
+
|
444
|
+
If you want to ignore displaying any non-numeric values use `:ignore`:
|
445
|
+
|
446
|
+
```ruby
|
447
|
+
sparkline = TTY::Sparkline.new(data, non_numeric: :ignore)
|
448
|
+
```
|
449
|
+
|
450
|
+
When rendered, this will result in:
|
451
|
+
|
452
|
+
```ruby
|
453
|
+
sparkline.render
|
454
|
+
# => "▁▂▄▆█"
|
455
|
+
````
|
456
|
+
|
457
|
+
You can also convert any non-numeric values to the smallest bar with `:minimum`:
|
458
|
+
|
459
|
+
```ruby
|
460
|
+
sparkline = TTY::Sparkline.new(data, non_numeric: :minimum)
|
461
|
+
```
|
462
|
+
|
463
|
+
This will render the following:
|
464
|
+
|
465
|
+
```ruby
|
466
|
+
sparkline.render
|
467
|
+
# => "▁▂▁▄▁▆▁█"
|
468
|
+
```
|
469
|
+
|
470
|
+
## Development
|
471
|
+
|
472
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
473
|
+
|
474
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
475
|
+
|
476
|
+
## Contributing
|
477
|
+
|
478
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/piotrmurach/tty-sparkline. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/piotrmurach/tty-sparkline/blob/master/CODE_OF_CONDUCT.md).
|
479
|
+
|
480
|
+
## License
|
481
|
+
|
482
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
483
|
+
|
484
|
+
## Code of Conduct
|
485
|
+
|
486
|
+
Everyone interacting in the TTY::Sparkline project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/piotrmurach/tty-sparkline/blob/master/CODE_OF_CONDUCT.md).
|
487
|
+
|
488
|
+
## Copyright
|
489
|
+
|
490
|
+
Copyright (c) 2021 Piotr Murach. See LICENSE for further details.
|
@@ -0,0 +1,330 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "tty-cursor"
|
4
|
+
|
5
|
+
require_relative "sparkline/version"
|
6
|
+
|
7
|
+
module TTY
|
8
|
+
# Responsible for drawing sparkline in a terminal
|
9
|
+
#
|
10
|
+
# @api public
|
11
|
+
class Sparkline
|
12
|
+
class Error < StandardError; end
|
13
|
+
|
14
|
+
BARS = %w[▁ ▂ ▃ ▄ ▅ ▆ ▇ █].freeze
|
15
|
+
|
16
|
+
EMPTY = ""
|
17
|
+
|
18
|
+
MAX_BUFFER_SIZE = 2**14
|
19
|
+
|
20
|
+
NEWLINE = "\n"
|
21
|
+
|
22
|
+
NON_NUMERIC_CONVERSIONS = %i[empty ignore minimum].freeze
|
23
|
+
|
24
|
+
SPACE = " "
|
25
|
+
|
26
|
+
# The top position
|
27
|
+
#
|
28
|
+
# @return [Integer]
|
29
|
+
#
|
30
|
+
# @api public
|
31
|
+
attr_reader :top
|
32
|
+
|
33
|
+
# The left position
|
34
|
+
#
|
35
|
+
# @return [Integer]
|
36
|
+
#
|
37
|
+
# @api public
|
38
|
+
attr_reader :left
|
39
|
+
|
40
|
+
# The chart maximum width in terminal columns
|
41
|
+
#
|
42
|
+
# @return [Integer]
|
43
|
+
#
|
44
|
+
# @api public
|
45
|
+
attr_reader :width
|
46
|
+
|
47
|
+
# The chart height in terminal lines
|
48
|
+
#
|
49
|
+
# @return [Integer]
|
50
|
+
#
|
51
|
+
# @api public
|
52
|
+
attr_reader :height
|
53
|
+
|
54
|
+
# The drawing cursor
|
55
|
+
#
|
56
|
+
# @return [TTY::Cursor]
|
57
|
+
#
|
58
|
+
# @api public
|
59
|
+
attr_reader :cursor
|
60
|
+
|
61
|
+
# The custom minimum value used for scaling bars
|
62
|
+
#
|
63
|
+
# @return [Numeric]
|
64
|
+
#
|
65
|
+
# @api public
|
66
|
+
attr_accessor :min
|
67
|
+
|
68
|
+
# The custom maximum value used for scaling bars
|
69
|
+
#
|
70
|
+
# @return [Numeric]
|
71
|
+
#
|
72
|
+
# @api public
|
73
|
+
attr_accessor :max
|
74
|
+
|
75
|
+
# Create a Sparkline instance
|
76
|
+
#
|
77
|
+
# @param [Array<Numeric>] data
|
78
|
+
# the data to chart
|
79
|
+
# @param [Integer] top
|
80
|
+
# the top position
|
81
|
+
# @param [Integer] left
|
82
|
+
# the left position
|
83
|
+
# @param [Integer] height
|
84
|
+
# the height in terminal lines
|
85
|
+
# @param [Integer] width
|
86
|
+
# the maximum width in terminal columns
|
87
|
+
# @param [Numeric] min
|
88
|
+
# the custom minimum value
|
89
|
+
# @param [Numeric] max
|
90
|
+
# the custom maximum value
|
91
|
+
# @param [Array<String>] bars
|
92
|
+
# the bars used for display
|
93
|
+
# @param [Integer] buffer_size
|
94
|
+
# the maximum buffer size
|
95
|
+
# @param [Symbol] non_numeric
|
96
|
+
# the replacement for a non-numeric value
|
97
|
+
#
|
98
|
+
# @api public
|
99
|
+
def initialize(data = [], top: nil, left: nil, height: 1, width: nil,
|
100
|
+
min: nil, max: nil, bars: BARS, buffer_size: MAX_BUFFER_SIZE,
|
101
|
+
non_numeric: :empty)
|
102
|
+
check_minmax(min, max) if min && max
|
103
|
+
check_non_numeric(non_numeric)
|
104
|
+
|
105
|
+
@data = Array(data).dup
|
106
|
+
@cached_data_size = @data.size
|
107
|
+
@top = top
|
108
|
+
@left = left
|
109
|
+
@height = height
|
110
|
+
@width = width
|
111
|
+
@min = min
|
112
|
+
@max = max
|
113
|
+
@bars = bars
|
114
|
+
@num_of_bars = bars.size
|
115
|
+
@buffer_size = buffer_size
|
116
|
+
@non_numeric = non_numeric
|
117
|
+
@filter = ->(value) { value.is_a?(::Numeric) }
|
118
|
+
@cursor = TTY::Cursor
|
119
|
+
end
|
120
|
+
|
121
|
+
# Append value(s)
|
122
|
+
#
|
123
|
+
# @example
|
124
|
+
# sparkline.push(1, 2, 3, 4)
|
125
|
+
#
|
126
|
+
# @example
|
127
|
+
# sparkline << 1 << 2 << 3 << 4
|
128
|
+
#
|
129
|
+
# @param [Array<Numeric>] nums
|
130
|
+
#
|
131
|
+
# @return [self]
|
132
|
+
#
|
133
|
+
# @api public
|
134
|
+
def push(*nums)
|
135
|
+
@data.push(*nums)
|
136
|
+
@cached_data_size += nums.size
|
137
|
+
|
138
|
+
if (overflow = @cached_data_size - @buffer_size) > 0
|
139
|
+
@data.shift(overflow)
|
140
|
+
@cached_data_size -= overflow
|
141
|
+
end
|
142
|
+
|
143
|
+
self
|
144
|
+
end
|
145
|
+
alias append push
|
146
|
+
alias << push
|
147
|
+
|
148
|
+
# The number of values
|
149
|
+
#
|
150
|
+
# @return [Integer]
|
151
|
+
#
|
152
|
+
# @api public
|
153
|
+
def size
|
154
|
+
@cached_data_size
|
155
|
+
end
|
156
|
+
|
157
|
+
# Render data as a sparkline chart
|
158
|
+
#
|
159
|
+
# @example
|
160
|
+
# sparkline.render
|
161
|
+
#
|
162
|
+
# @param [Integer] min
|
163
|
+
# the minimum value to display
|
164
|
+
# @param [Integer] max
|
165
|
+
# the maximum value to display
|
166
|
+
#
|
167
|
+
# @return [String]
|
168
|
+
# the rendered sparkline chart
|
169
|
+
#
|
170
|
+
# @api public
|
171
|
+
def render(min: nil, max: nil)
|
172
|
+
return EMPTY if @data.empty?
|
173
|
+
|
174
|
+
buffer = []
|
175
|
+
calc_min, calc_max = data_minmax(min, max)
|
176
|
+
check_minmax(calc_min, calc_max)
|
177
|
+
|
178
|
+
height.times do |y|
|
179
|
+
buffer << position(y) if position?
|
180
|
+
@data[data_range].each.with_index do |value, x|
|
181
|
+
bar_index = clamp_and_scale(value, calc_min, calc_max)
|
182
|
+
bar = convert_to_bar(bar_index, height - 1 - y)
|
183
|
+
bar = yield(value, bar, x, y) if block_given?
|
184
|
+
buffer << bar
|
185
|
+
end
|
186
|
+
buffer << NEWLINE unless y == height - 1
|
187
|
+
end
|
188
|
+
|
189
|
+
buffer.join
|
190
|
+
end
|
191
|
+
|
192
|
+
private
|
193
|
+
|
194
|
+
# Find the minimum and maximum value in the data
|
195
|
+
#
|
196
|
+
# @param [Integer] min
|
197
|
+
# the custom minimum value
|
198
|
+
# @param [Integer] max
|
199
|
+
# the custom maximum value
|
200
|
+
#
|
201
|
+
# @return [Array<Numeric, Numeric>]
|
202
|
+
#
|
203
|
+
# @api private
|
204
|
+
def data_minmax(min, max)
|
205
|
+
calc_min, calc_max = @data.select(&@filter).minmax
|
206
|
+
[min || @min || calc_min, max || @max || calc_max]
|
207
|
+
end
|
208
|
+
|
209
|
+
# Check maximum isn't less than minimum
|
210
|
+
#
|
211
|
+
# @raise [Error]
|
212
|
+
#
|
213
|
+
# @api private
|
214
|
+
def check_minmax(min, max)
|
215
|
+
return if min <= max
|
216
|
+
|
217
|
+
raise Error, "maximum value cannot be less than minimum"
|
218
|
+
end
|
219
|
+
|
220
|
+
# Check whether non_numeric has a valid conversion type
|
221
|
+
#
|
222
|
+
# @param [Symbol] type
|
223
|
+
# the type of conversion
|
224
|
+
#
|
225
|
+
# @raise [Error]
|
226
|
+
#
|
227
|
+
# @api private
|
228
|
+
def check_non_numeric(type)
|
229
|
+
return if NON_NUMERIC_CONVERSIONS.include?(type)
|
230
|
+
|
231
|
+
raise Error, "unknown non_numeric value: #{type.inspect}"
|
232
|
+
end
|
233
|
+
|
234
|
+
# Check whether or not to position this chart
|
235
|
+
#
|
236
|
+
# @return [Boolean]
|
237
|
+
#
|
238
|
+
# @api private
|
239
|
+
def position?
|
240
|
+
top || left
|
241
|
+
end
|
242
|
+
|
243
|
+
# Find a position at which to display this chart
|
244
|
+
#
|
245
|
+
# @return [String]
|
246
|
+
#
|
247
|
+
# @api private
|
248
|
+
def position(offset = 0)
|
249
|
+
if left && top
|
250
|
+
cursor.move_to(left, top + offset)
|
251
|
+
elsif left
|
252
|
+
cursor.column(left)
|
253
|
+
elsif top
|
254
|
+
cursor.row(top + offset)
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
# Find a range of data values matching width
|
259
|
+
#
|
260
|
+
# @return [Range]
|
261
|
+
#
|
262
|
+
# @api private
|
263
|
+
def data_range
|
264
|
+
start_index = 0
|
265
|
+
if width && width < @cached_data_size
|
266
|
+
start_index = @cached_data_size - width
|
267
|
+
end
|
268
|
+
start_index..-1
|
269
|
+
end
|
270
|
+
|
271
|
+
# Clamp value and scale it against height and number of bars
|
272
|
+
#
|
273
|
+
# @param [Object] value
|
274
|
+
# the value to clamp and scale
|
275
|
+
# @param [Integer] min
|
276
|
+
# the minimum value
|
277
|
+
# @param [Integer] max
|
278
|
+
# the maximum value
|
279
|
+
#
|
280
|
+
# @return [Integer]
|
281
|
+
#
|
282
|
+
# @api private
|
283
|
+
def clamp_and_scale(value, min, max)
|
284
|
+
return value unless value.is_a?(Numeric)
|
285
|
+
|
286
|
+
clamped_value = value > max ? max : (value < min ? min : value)
|
287
|
+
reduced_value = max == min ? clamped_value : clamped_value - min
|
288
|
+
reduced_max = max == min ? (max.zero? ? 1 : max) : max - min
|
289
|
+
reduced_value * height * (@num_of_bars - 1) / reduced_max
|
290
|
+
end
|
291
|
+
|
292
|
+
# Convert an index to a bar representation
|
293
|
+
#
|
294
|
+
# @param [Integer] bar_index
|
295
|
+
# the bar index within bars
|
296
|
+
# @param [Integer] offset
|
297
|
+
# the offset from the bottom
|
298
|
+
#
|
299
|
+
# @return [String]
|
300
|
+
# the rendered bar
|
301
|
+
#
|
302
|
+
# @api private
|
303
|
+
def convert_to_bar(bar_index, offset)
|
304
|
+
return convert_non_numeric unless bar_index.is_a?(Numeric)
|
305
|
+
|
306
|
+
if bar_index >= offset * @num_of_bars
|
307
|
+
bar_index -= offset * @num_of_bars
|
308
|
+
@bars[bar_index >= @num_of_bars ? -1 : bar_index]
|
309
|
+
else
|
310
|
+
SPACE
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
# Convert non-numeric value into display string
|
315
|
+
#
|
316
|
+
# @return [String]
|
317
|
+
#
|
318
|
+
# @api private
|
319
|
+
def convert_non_numeric
|
320
|
+
case @non_numeric
|
321
|
+
when :empty
|
322
|
+
SPACE
|
323
|
+
when :ignore
|
324
|
+
EMPTY
|
325
|
+
when :minimum
|
326
|
+
@bars[0]
|
327
|
+
end
|
328
|
+
end
|
329
|
+
end # Sparkline
|
330
|
+
end # TTY
|
metadata
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tty-sparkline
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Piotr Murach
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-05-20 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: tty-cursor
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.7'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.7'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.0'
|
55
|
+
description: Sparkline charts for terminal applications.
|
56
|
+
email:
|
57
|
+
- piotr@piotrmurach.com
|
58
|
+
executables: []
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files:
|
61
|
+
- README.md
|
62
|
+
- CHANGELOG.md
|
63
|
+
- LICENSE.txt
|
64
|
+
files:
|
65
|
+
- CHANGELOG.md
|
66
|
+
- LICENSE.txt
|
67
|
+
- README.md
|
68
|
+
- lib/tty-sparkline.rb
|
69
|
+
- lib/tty/sparkline.rb
|
70
|
+
- lib/tty/sparkline/version.rb
|
71
|
+
homepage: https://ttytoolkit.org
|
72
|
+
licenses:
|
73
|
+
- MIT
|
74
|
+
metadata:
|
75
|
+
allowed_push_host: https://rubygems.org
|
76
|
+
bug_tracker_uri: https://github.com/piotrmurach/tty-sparkline/issues
|
77
|
+
changelog_uri: https://github.com/piotrmurach/tty-sparkline/blob/master/CHANGELOG.md
|
78
|
+
documentation_uri: https://www.rubydoc.info/gems/tty-sparkline
|
79
|
+
homepage_uri: https://ttytoolkit.org
|
80
|
+
source_code_uri: https://github.com/piotrmurach/tty-sparkline
|
81
|
+
post_install_message:
|
82
|
+
rdoc_options: []
|
83
|
+
require_paths:
|
84
|
+
- lib
|
85
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 2.0.0
|
90
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
91
|
+
requirements:
|
92
|
+
- - ">="
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '0'
|
95
|
+
requirements: []
|
96
|
+
rubygems_version: 3.1.2
|
97
|
+
signing_key:
|
98
|
+
specification_version: 4
|
99
|
+
summary: Sparkline charts for terminal applications.
|
100
|
+
test_files: []
|