tty-sparkline 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|