termplot 0.2.1 → 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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/README.md +124 -54
  4. data/Rakefile +27 -14
  5. data/doc/dash.png +0 -0
  6. data/doc/demo.png +0 -0
  7. data/doc/file.png +0 -0
  8. data/doc/memory.png +0 -0
  9. data/doc/ping.png +0 -0
  10. data/doc/sin.png +0 -0
  11. data/doc/tcp.png +0 -0
  12. data/examples/sample.rb +17 -0
  13. data/lib/termplot/character_map.rb +15 -4
  14. data/lib/termplot/cli.rb +16 -3
  15. data/lib/termplot/colors.rb +7 -0
  16. data/lib/termplot/commands.rb +27 -0
  17. data/lib/termplot/consumers.rb +12 -0
  18. data/lib/termplot/consumers/base_consumer.rb +132 -0
  19. data/lib/termplot/consumers/command_consumer.rb +14 -0
  20. data/lib/termplot/consumers/multi_source_consumer.rb +33 -0
  21. data/lib/termplot/consumers/single_source_consumer.rb +36 -0
  22. data/lib/termplot/consumers/stdin_consumer.rb +11 -0
  23. data/lib/termplot/cursors/buffered_console_cursor.rb +1 -1
  24. data/lib/termplot/cursors/virtual_cursor.rb +4 -0
  25. data/lib/termplot/dsl/panels.rb +80 -0
  26. data/lib/termplot/dsl/widgets.rb +128 -0
  27. data/lib/termplot/file_config.rb +37 -0
  28. data/lib/termplot/message_broker.rb +108 -0
  29. data/lib/termplot/options.rb +100 -20
  30. data/lib/termplot/positioned_widget.rb +8 -0
  31. data/lib/termplot/producer_options.rb +3 -0
  32. data/lib/termplot/producers.rb +3 -3
  33. data/lib/termplot/producers/base_producer.rb +12 -15
  34. data/lib/termplot/producers/command_producer.rb +25 -9
  35. data/lib/termplot/producers/stdin_producer.rb +1 -4
  36. data/lib/termplot/renderable.rb +35 -0
  37. data/lib/termplot/renderer.rb +16 -257
  38. data/lib/termplot/renderers.rb +6 -0
  39. data/lib/termplot/renderers/border_renderer.rb +48 -0
  40. data/lib/termplot/renderers/text_renderer.rb +73 -0
  41. data/lib/termplot/shell.rb +13 -9
  42. data/lib/termplot/utils/ansi_safe_string.rb +68 -0
  43. data/lib/termplot/version.rb +1 -1
  44. data/lib/termplot/widget_dsl.rb +130 -0
  45. data/lib/termplot/widgets.rb +8 -0
  46. data/lib/termplot/widgets/base_widget.rb +79 -0
  47. data/lib/termplot/widgets/border.rb +6 -0
  48. data/lib/termplot/widgets/dataset.rb +50 -0
  49. data/lib/termplot/widgets/histogram_widget.rb +196 -0
  50. data/lib/termplot/widgets/statistics.rb +21 -0
  51. data/lib/termplot/widgets/statistics_widget.rb +104 -0
  52. data/lib/termplot/widgets/time_series_widget.rb +248 -0
  53. data/lib/termplot/window.rb +25 -5
  54. data/termplot.gemspec +1 -6
  55. metadata +36 -24
  56. data/doc/MSFT.png +0 -0
  57. data/doc/cpu.png +0 -0
  58. data/doc/demo.cast +0 -638
  59. data/lib/termplot/consumer.rb +0 -75
  60. data/lib/termplot/cursors/console_cursor.rb +0 -57
  61. data/lib/termplot/series.rb +0 -37
@@ -0,0 +1,21 @@
1
+ module Termplot
2
+ module Widgets
3
+ module Statistics
4
+ def count
5
+ data.count
6
+ end
7
+
8
+ def mean
9
+ return 0 if data.empty?
10
+ data.sum(0.0) / count
11
+ end
12
+
13
+ def standard_deviation
14
+ return 0 if data.empty?
15
+ data_mean = mean
16
+ variance = data.map { |x| (data_mean - x) ** 2 }.sum / count
17
+ Math.sqrt(variance)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "termplot/window"
4
+ require "termplot/renderable"
5
+ require "termplot/window"
6
+ require "termplot/character_map"
7
+ require "termplot/renderers"
8
+
9
+ module Termplot
10
+ module Widgets
11
+ class StatisticsWidget < BaseWidget
12
+ def render_to_window
13
+ errors.clear
14
+ window.clear
15
+ window.cursor.reset_position
16
+
17
+ render_statistics
18
+ window.cursor.reset_position
19
+
20
+ # Title bar
21
+ Termplot::Renderers::TextRenderer.new(
22
+ bordered_window: bordered_window,
23
+ text: title,
24
+ row: 0,
25
+ align: :center,
26
+ errors: errors
27
+ ).render
28
+
29
+ window.cursor.reset_position
30
+
31
+ # Borders
32
+ Termplot::Renderers::BorderRenderer.new(
33
+ bordered_window: bordered_window
34
+ ).render
35
+
36
+ window.cursor.reset_position
37
+ end
38
+
39
+ private
40
+ def default_border_size
41
+ Border.new(2, 1, 1, 1)
42
+ end
43
+
44
+ def min_cols
45
+ 20
46
+ end
47
+
48
+ def min_rows
49
+ 5 + default_border_size.top + default_border_size.bottom
50
+ end
51
+
52
+ def render_statistics
53
+ titles, values = formatted_stats
54
+
55
+ window.cursor.down(bordered_window.border_size.top)
56
+ window.cursor.beginning_of_line
57
+ window.cursor.forward(bordered_window.border_size.left)
58
+
59
+ title_color = "blue"
60
+ value_color = "green"
61
+
62
+ justified_stats = titles.zip(values).map do |(title, value)|
63
+ field_size = [title.size, value.size].max
64
+ title = Colors.send(title_color, title.ljust(field_size, " "))
65
+ value = Colors.send(value_color, value.ljust(field_size, " "))
66
+ [title, value]
67
+ end
68
+
69
+ col_separator = " #{border_char_map[:vert_right]} "
70
+ stats_table = justified_stats.transpose.map { |row| row.join(col_separator) }
71
+
72
+ start_row = bordered_window.inner_height > 2 ? bordered_window.border_size.top - 1 + bordered_window.inner_height / 2 : bordered_window.border_size.top
73
+
74
+ stats_table.each_with_index do |row, index|
75
+ Termplot::Renderers::TextRenderer.new(
76
+ bordered_window: bordered_window,
77
+ text: row,
78
+ row: start_row + index,
79
+ errors: errors,
80
+ align: :center
81
+ ).render
82
+ end
83
+ end
84
+
85
+ def formatted_stats
86
+ titles = %w[Samples Min Max Mean Stdev]
87
+
88
+ values = [:count, :min, :max, :mean, :standard_deviation].map do |stat|
89
+ format_number(dataset.send(stat))
90
+ end
91
+
92
+ [titles, values]
93
+ end
94
+
95
+ def format_number(n)
96
+ "%.#{decimals}f" % n.round(decimals)
97
+ end
98
+
99
+ def border_char_map
100
+ CharacterMap::DEFAULT
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,248 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "termplot/window"
4
+ require "termplot/character_map"
5
+ require "termplot/colors"
6
+ require "termplot/renderable"
7
+ require "termplot/renderers"
8
+
9
+ module Termplot
10
+ module Widgets
11
+ class TimeSeriesWidget < BaseWidget
12
+ DEFAULT_COLOR = "yellow"
13
+ DEFAULT_LINE_STYLE = "line"
14
+
15
+ attr_reader :color, :line_style, :tick_spacing
16
+
17
+ def post_initialize(opts)
18
+ @color = Termplot::Colors.fetch(opts[:color], DEFAULT_COLOR)
19
+ @line_style = Termplot::CharacterMap::LINE_STYLES.fetch(
20
+ opts[:line_style],
21
+ Termplot::CharacterMap::LINE_STYLES[DEFAULT_LINE_STYLE]
22
+ )
23
+
24
+ @tick_spacing = 3
25
+ end
26
+
27
+ def render_to_window
28
+ errors.clear
29
+ window.clear
30
+
31
+ # Calculate width of right hand axis
32
+ calculate_axis_size
33
+
34
+ # Points
35
+ points = build_points
36
+ render_points(points)
37
+ window.cursor.reset_position
38
+
39
+ # Title bar
40
+ Termplot::Renderers::TextRenderer.new(
41
+ bordered_window: bordered_window,
42
+ text: title_text,
43
+ row: 0,
44
+ errors: errors
45
+ ).render
46
+ window.cursor.reset_position
47
+
48
+ # Borders
49
+ Termplot::Renderers::BorderRenderer.new(
50
+ bordered_window: bordered_window
51
+ ).render
52
+
53
+ window.cursor.reset_position
54
+
55
+ # Draw axis
56
+ ticks = build_ticks(points)
57
+ render_axis(ticks)
58
+ end
59
+
60
+ private
61
+ def max_count
62
+ bordered_window.inner_width
63
+ end
64
+
65
+ # Axis size = length of the longest point value , formatted as a string to
66
+ # @decimals decimal places, + 2 for some extra buffer + 1 for the border
67
+ # itself.
68
+ def calculate_axis_size
69
+ return if dataset.empty?
70
+ border_right = dataset.map { |n| n.round(decimals).to_s.length }.max
71
+ border_right += 3
72
+
73
+ # Clamp border_right at cols - 3 to prevent the renderer from crashing
74
+ # with very large numbers
75
+ if border_right > cols - 3
76
+ errors.push(Colors.yellow("Warning: Axis tick values have been clipped, consider using more columns with -c"))
77
+ border_right = cols - 3
78
+ end
79
+
80
+ @bordered_window.border_size = Border.new(2, border_right, 1, 1)
81
+ end
82
+
83
+ def border_char_map
84
+ CharacterMap::DEFAULT
85
+ end
86
+
87
+ def default_border_size
88
+ Border.new(2, 4, 1, 1)
89
+ end
90
+
91
+ # At minimum, 2 cols of inner_width for values
92
+ def min_cols
93
+ default_border_size.left + default_border_size.right + 2
94
+ end
95
+
96
+ # At minimum, 2 rows of inner_height for values
97
+ def min_rows
98
+ default_border_size.top + default_border_size.bottom + 2
99
+ end
100
+
101
+ Point = Struct.new(:x, :y, :value)
102
+ def build_points
103
+ return [] if dataset.empty?
104
+
105
+ dataset.map.with_index do |p, x|
106
+ # Map from series Y range to inner height
107
+ y = map_value(p, [dataset.min, dataset.max], [0, bordered_window.inner_height - 1])
108
+
109
+ # Invert Y value since pixel Y is inverse of cartesian Y
110
+ y = bordered_window.border_size.top - 1 + bordered_window.inner_height - y.round
111
+
112
+ # Add padding for border width
113
+ Point.new(x + bordered_window.border_size.left, y, p.to_f)
114
+ end
115
+ end
116
+
117
+ def render_points(points)
118
+ # Render points
119
+ points.each_with_index do |point, i|
120
+ window.cursor.position = point.y * cols + point.x
121
+
122
+ if line_style[:extended]
123
+ prev_point = ((i - 1) >= 0) ? points[i - 1] : nil
124
+ render_connected_line(prev_point, point)
125
+ elsif line_style[:filled]
126
+ render_filled_point(point)
127
+ else
128
+ window.write(colored(line_style[:point]))
129
+ end
130
+ end
131
+ end
132
+
133
+ def render_connected_line(prev_point, point)
134
+ if prev_point.nil? || (prev_point.y == point.y)
135
+ window.write(colored(line_style[:horz_top]))
136
+ elsif prev_point.y > point.y
137
+ diff = prev_point.y - point.y
138
+
139
+ window.write(colored(line_style[:top_left]))
140
+ window.cursor.down
141
+ window.cursor.back
142
+
143
+ (diff - 1).times do
144
+ window.write(colored(line_style[:vert_right]))
145
+ window.cursor.down
146
+ window.cursor.back
147
+ end
148
+
149
+ window.write(colored(line_style[:bot_right]))
150
+ elsif prev_point.y < point.y
151
+ diff = point.y - prev_point.y
152
+
153
+ window.write(colored(line_style[:bot_left]))
154
+ window.cursor.up
155
+ window.cursor.back
156
+
157
+ (diff - 1).times do
158
+ window.write(colored(line_style[:vert_left]))
159
+ window.cursor.up
160
+ window.cursor.back
161
+ end
162
+
163
+ window.write(colored(line_style[:top_right]))
164
+ end
165
+ end
166
+
167
+ def render_filled_point(point)
168
+ diff = (bordered_window.inner_height + bordered_window.border_size.bottom) - point.y
169
+ diff.times { window.cursor.down }
170
+
171
+ diff.times do
172
+ window.write(Colors.send("#{color}_bg", colored(line_style[:point])))
173
+ window.cursor.up
174
+ window.cursor.back
175
+ end
176
+
177
+ window.write(colored(line_style[:point]))
178
+ end
179
+
180
+ Tick = Struct.new(:y, :label)
181
+ def build_ticks(points)
182
+ return [] if points.empty?
183
+ max_point = points.max_by(&:value)
184
+ min_point = points.min_by(&:value)
185
+ point_y_range = points.max_by(&:y).y - points.min_by(&:y).y
186
+ ticks = []
187
+ ticks.push(Tick.new(max_point.y, format_label(max_point.value)))
188
+
189
+ # Distribute ticks between min and max, maintaining spacinig as much as
190
+ # possible. tick_spacing is inclusive of the tick row itself.
191
+ if max_point.value != min_point.value && (point_y_range - 2) > tick_spacing
192
+ num_ticks = (point_y_range - 2) / tick_spacing
193
+
194
+ num_ticks.times do |i|
195
+ tick_y = max_point.y + (i + 1) * tick_spacing
196
+ value = max_point.value - dataset.range * ((i + 1) * tick_spacing) / point_y_range
197
+ ticks.push(Tick.new(tick_y, format_label(value)))
198
+ end
199
+ end
200
+
201
+ ticks.push(Tick.new(min_point.y, format_label(min_point.value)))
202
+ ticks
203
+ end
204
+
205
+ # Map value from one range to another
206
+ def map_value(val, from_range, to_range)
207
+ orig_range = [1, (from_range[1] - from_range[0]).abs].max
208
+ new_range = [1, (to_range[1] - to_range[0]).abs].max
209
+
210
+ ((val.to_f - from_range[0]) / orig_range) * new_range + to_range[0]
211
+ end
212
+
213
+ def title_text
214
+ colored(line_style[:point]) + " " + title
215
+ end
216
+
217
+ def render_axis(ticks)
218
+ window.cursor.down(bordered_window.border_size.top - 1)
219
+ window.cursor.forward(bordered_window.border_size.left + bordered_window.inner_width + 1)
220
+
221
+ # Render ticks
222
+ ticks.each do |tick|
223
+ window.cursor.row = tick.y
224
+ window.cursor.back
225
+ window.write(border_char_map[:tick_right])
226
+
227
+ tick.label.chars.each do |c|
228
+ window.write(c)
229
+ end
230
+
231
+ window.cursor.back(label_chars)
232
+ end
233
+ end
234
+
235
+ def format_label(num)
236
+ ("%.#{decimals}f" % num.round(decimals))[0..label_chars - 1].ljust(label_chars, " ")
237
+ end
238
+
239
+ def label_chars
240
+ bordered_window.border_size.right - 2
241
+ end
242
+
243
+ def colored(text)
244
+ Colors.send(color, text)
245
+ end
246
+ end
247
+ end
248
+ end
@@ -35,6 +35,7 @@ module Termplot
35
35
  size.times { write CharacterMap::DEFAULT[:empty] }
36
36
  end
37
37
 
38
+ # Flush rendered window to a string
38
39
  def flush
39
40
  console_cursor.clear_buffer
40
41
  console_cursor.reset_position
@@ -47,16 +48,17 @@ module Termplot
47
48
  console_cursor.flush
48
49
  end
49
50
 
50
- def flush_debug(str = "Window")
51
- padding = "-" * 10
52
- puts "\n#{padding} #{str} #{padding}\n"
51
+ # Flush to 2d array rather than string
52
+ def flush_debug
53
+ debug_arr = []
53
54
  buffer.each_slice(cols).with_index do |line, y|
54
55
  render_line = line.each_with_index.map do |c, x|
55
56
  y * cols + x == cursor.position ? "𝥺" : c
56
57
  end
57
- print render_line
58
- puts
58
+ debug_arr << render_line
59
+ debug_arr << "\n"
59
60
  end
61
+ debug_arr
60
62
  end
61
63
 
62
64
  # TODO: Refine later and include errors properly in the window
@@ -65,5 +67,23 @@ module Termplot
65
67
  print Termplot::ControlChars::NEWLINE
66
68
  errors.length.times { print Termplot::ControlChars::UP }
67
69
  end
70
+
71
+ def blit(other, start_row, start_col)
72
+ cursor.position = start_row * cols + start_col
73
+
74
+ other.each_row do |row|
75
+ row.each do |char|
76
+ write(char)
77
+ end
78
+ cursor.down unless cursor.beginning_of_line?
79
+ cursor.col = start_col
80
+ end
81
+ end
82
+
83
+ def each_row
84
+ buffer.each_slice(cols) do |row|
85
+ yield row
86
+ end
87
+ end
68
88
  end
69
89
  end
@@ -10,9 +10,7 @@ Gem::Specification.new do |spec|
10
10
  spec.description = %q{Plot time series charts in your terminal}
11
11
  spec.homepage = "https://github.com/Martin-Nyaga/termplot"
12
12
  spec.license = "MIT"
13
- spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
14
-
15
- # spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
13
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.5.0")
16
14
 
17
15
  spec.metadata["homepage_uri"] = spec.homepage
18
16
  spec.metadata["source_code_uri"] = "https://github.com/Martin-Nyaga/termplot"
@@ -26,7 +24,4 @@ Gem::Specification.new do |spec|
26
24
  spec.bindir = "bin"
27
25
  spec.executables = "termplot"
28
26
  spec.require_paths = ["lib"]
29
-
30
- # Dependencies
31
- spec.add_runtime_dependency "ruby-termios", "~> 1.0"
32
27
  end
metadata CHANGED
@@ -1,29 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: termplot
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Martin Nyaga
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-11-10 00:00:00.000000000 Z
12
- dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: ruby-termios
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - "~>"
18
- - !ruby/object:Gem::Version
19
- version: '1.0'
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - "~>"
25
- - !ruby/object:Gem::Version
26
- version: '1.0'
11
+ date: 2020-12-11 00:00:00.000000000 Z
12
+ dependencies: []
27
13
  description: Plot time series charts in your terminal
28
14
  email:
29
15
  - nyagamartin72@gmail.com
@@ -40,31 +26,57 @@ files:
40
26
  - bin/console
41
27
  - bin/setup
42
28
  - bin/termplot
43
- - doc/MSFT.png
44
- - doc/cpu.png
45
- - doc/demo.cast
29
+ - doc/dash.png
30
+ - doc/demo.png
31
+ - doc/file.png
46
32
  - doc/memory.png
33
+ - doc/ping.png
47
34
  - doc/sin.png
48
35
  - doc/tcp.png
36
+ - examples/sample.rb
49
37
  - lib/termplot.rb
50
38
  - lib/termplot/character_map.rb
51
39
  - lib/termplot/cli.rb
52
40
  - lib/termplot/colors.rb
53
- - lib/termplot/consumer.rb
41
+ - lib/termplot/commands.rb
42
+ - lib/termplot/consumers.rb
43
+ - lib/termplot/consumers/base_consumer.rb
44
+ - lib/termplot/consumers/command_consumer.rb
45
+ - lib/termplot/consumers/multi_source_consumer.rb
46
+ - lib/termplot/consumers/single_source_consumer.rb
47
+ - lib/termplot/consumers/stdin_consumer.rb
54
48
  - lib/termplot/control_chars.rb
55
49
  - lib/termplot/cursors.rb
56
50
  - lib/termplot/cursors/buffered_console_cursor.rb
57
- - lib/termplot/cursors/console_cursor.rb
58
51
  - lib/termplot/cursors/virtual_cursor.rb
52
+ - lib/termplot/dsl/panels.rb
53
+ - lib/termplot/dsl/widgets.rb
54
+ - lib/termplot/file_config.rb
55
+ - lib/termplot/message_broker.rb
59
56
  - lib/termplot/options.rb
57
+ - lib/termplot/positioned_widget.rb
58
+ - lib/termplot/producer_options.rb
60
59
  - lib/termplot/producers.rb
61
60
  - lib/termplot/producers/base_producer.rb
62
61
  - lib/termplot/producers/command_producer.rb
63
62
  - lib/termplot/producers/stdin_producer.rb
63
+ - lib/termplot/renderable.rb
64
64
  - lib/termplot/renderer.rb
65
- - lib/termplot/series.rb
65
+ - lib/termplot/renderers.rb
66
+ - lib/termplot/renderers/border_renderer.rb
67
+ - lib/termplot/renderers/text_renderer.rb
66
68
  - lib/termplot/shell.rb
69
+ - lib/termplot/utils/ansi_safe_string.rb
67
70
  - lib/termplot/version.rb
71
+ - lib/termplot/widget_dsl.rb
72
+ - lib/termplot/widgets.rb
73
+ - lib/termplot/widgets/base_widget.rb
74
+ - lib/termplot/widgets/border.rb
75
+ - lib/termplot/widgets/dataset.rb
76
+ - lib/termplot/widgets/histogram_widget.rb
77
+ - lib/termplot/widgets/statistics.rb
78
+ - lib/termplot/widgets/statistics_widget.rb
79
+ - lib/termplot/widgets/time_series_widget.rb
68
80
  - lib/termplot/window.rb
69
81
  - termplot.gemspec
70
82
  homepage: https://github.com/Martin-Nyaga/termplot
@@ -82,7 +94,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
82
94
  requirements:
83
95
  - - ">="
84
96
  - !ruby/object:Gem::Version
85
- version: 2.3.0
97
+ version: 2.5.0
86
98
  required_rubygems_version: !ruby/object:Gem::Requirement
87
99
  requirements:
88
100
  - - ">="