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 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
@@ -0,0 +1,5 @@
1
+ ## [v0.1.0] - 2021-05-20
2
+
3
+ * Initial release
4
+
5
+ [v0.1.0]: https://github.com/piotrmurach/tty-sparkline/compare/469d490...v0.1.0
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 Version](https://badge.fury.io/rb/tty-sparkline.svg)][gem]
8
+ [![Actions CI](https://github.com/piotrmurach/tty-sparkline/workflows/CI/badge.svg?branch=master)][gh_actions_ci]
9
+ [![Build status](https://ci.appveyor.com/api/projects/status/3pk69ulk2fs330ja?svg=true)][appveyor]
10
+ [![Maintainability](https://api.codeclimate.com/v1/badges/aade5679e021d0ca5b55/maintainability)][codeclimate]
11
+ [![Coverage Status](https://coveralls.io/repos/github/piotrmurach/tty-sparkline/badge.svg)][coverage]
12
+ [![Inline docs](https://inch-ci.org/github/piotrmurach/tty-sparkline.svg?branch=master)][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
+ ![tty-sparkline](https://github.com/piotrmurach/tty-sparkline/raw/master/assets/tty-sparkline.png)
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,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "tty/sparkline"
@@ -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
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TTY
4
+ class Sparkline
5
+ VERSION = "0.1.0"
6
+ end # Sparkline
7
+ 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: []