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,68 @@
1
+ module Termplot
2
+ module Utils
3
+ class AnsiSafeString
4
+ include Enumerable
5
+
6
+ # Regex to match ANSI escape sequences.
7
+ ANSI_MATCHER = Regexp.new("(\\[?\\033\\[?[;?\\d]*[\\dA-Za-z][\\];]?)")
8
+
9
+ attr_reader :string
10
+ def initialize(string)
11
+ @string = string
12
+ end
13
+
14
+ def length
15
+ sanitized.length
16
+ end
17
+
18
+ def sanitized
19
+ string.gsub(/#{ANSI_MATCHER}/, "")
20
+ end
21
+
22
+ # Yield each char in the string, folding any escape sequences into the
23
+ # next char. NOTE: If the string includes only ansi escape sequences,
24
+ # nothing will be yielded.
25
+ def each
26
+ ansi_code_positions = []
27
+
28
+ string.scan(ANSI_MATCHER) do |_|
29
+ ansi_code_positions << Regexp.last_match.offset(0)
30
+ end
31
+
32
+ i = 0
33
+ current_char = ""
34
+ while i < string.length
35
+ if ansi_code_positions.any? { |pos| pos[0] <= i && pos[1] > i }
36
+ current_char << string[i]
37
+ i += 1
38
+ else
39
+ current_char << string[i]
40
+
41
+ # If the next character is a terminating ansi sequence, we need to
42
+ # fold it into the current character, to prevent emitting an ansi
43
+ # sequence only as the last character.
44
+ next_char_is_terminating_ansi_sequence =
45
+ ansi_code_positions.length > 0 &&
46
+ ansi_code_positions.last[0] == i + 1 &&
47
+ ansi_code_positions.last[1] == string.length
48
+
49
+ if next_char_is_terminating_ansi_sequence
50
+ current_char << string[i + 1..-1]
51
+ i += 1 + (string.length - 1 - i + 1)
52
+ else
53
+ yield current_char
54
+ current_char = ""
55
+ i += 1
56
+ end
57
+ end
58
+ end
59
+
60
+ yield current_char unless current_char.empty?
61
+ end
62
+
63
+ def slice(start, stop)
64
+ AnsiSafeString.new(to_a[start..stop].join)
65
+ end
66
+ end
67
+ end
68
+ end
@@ -1,3 +1,3 @@
1
1
  module Termplot
2
- VERSION = "0.2.1"
2
+ VERSION = "0.3.0"
3
3
  end
@@ -0,0 +1,130 @@
1
+ require "termplot/positioned_widget"
2
+ require "termplot/widgets"
3
+
4
+ module Termplot
5
+ module DSL
6
+ module Widgets
7
+ def timeseries(attrs)
8
+ attrs = merge_defaults(attrs)
9
+ children.push(TimeSeriesConfig.new(attrs))
10
+ end
11
+
12
+ def statistics(attrs)
13
+ attrs = merge_defaults(attrs)
14
+ children.push(StatisticsConfig.new(attrs))
15
+ end
16
+
17
+ def histogram(attrs)
18
+ attrs = merge_defaults(attrs)
19
+ children.push(HistogramConfig.new(attrs))
20
+ end
21
+
22
+ private
23
+
24
+ def merge_defaults(attrs)
25
+ Termplot::Options.default_options.merge(attrs)
26
+ end
27
+
28
+ class WidgetConfig
29
+ attr_reader(
30
+ :title,
31
+ :command,
32
+ :interval,
33
+ :col,
34
+ :row,
35
+ :cols,
36
+ :rows,
37
+ :debug
38
+ )
39
+
40
+ def initialize(opts)
41
+ @title = opts[:title]
42
+
43
+ @command = opts[:command]
44
+ @interval = opts[:interval]
45
+ @debug = opts[:debug]
46
+
47
+ post_initialize(opts)
48
+ end
49
+
50
+ def post_initialize(opts)
51
+ # Implemented in subclasses
52
+ end
53
+
54
+ def set_dimensions(rows, cols, start_row, start_col)
55
+ @rows = rows
56
+ @cols = cols
57
+ @row = start_row
58
+ @col = start_col
59
+ end
60
+
61
+ def flatten
62
+ self
63
+ end
64
+
65
+ def positioned_widget
66
+ @positioned_widget ||= PositionedWidget.new(
67
+ row: row,
68
+ col: col,
69
+ widget: widget
70
+ )
71
+ end
72
+
73
+ def widget
74
+ raise "Must be implemented"
75
+ end
76
+
77
+ def producer_options
78
+ ProducerOptions.new(command: command, interval: interval)
79
+ end
80
+ end
81
+
82
+ class TimeSeriesConfig < WidgetConfig
83
+ attr_reader :color, :line_style
84
+ def post_initialize(opts)
85
+ @color = opts[:color]
86
+ @line_style = opts[:line_style]
87
+ end
88
+
89
+ def widget
90
+ @widget ||= Termplot::Widgets::TimeSeriesWidget.new(
91
+ title: title,
92
+ line_style: line_style,
93
+ color: color,
94
+ cols: cols,
95
+ rows: rows,
96
+ debug: debug
97
+ )
98
+ end
99
+ end
100
+
101
+ class StatisticsConfig < WidgetConfig
102
+ def widget
103
+ @widget ||= Termplot::Widgets::StatisticsWidget.new(
104
+ title: title,
105
+ cols: cols,
106
+ rows: rows,
107
+ debug: debug
108
+ )
109
+ end
110
+ end
111
+
112
+ class HistogramConfig < WidgetConfig
113
+ attr_reader :color
114
+ def post_initialize(opts)
115
+ @color = opts[:color]
116
+ end
117
+
118
+ def widget
119
+ @widget ||= Termplot::Widgets::HistogramWidget.new(
120
+ title: title,
121
+ color: color,
122
+ cols: cols,
123
+ rows: rows,
124
+ debug: debug
125
+ )
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,8 @@
1
+ module Termplot
2
+ module Widgets
3
+ autoload :BaseWidget, "termplot/widgets/base_widget"
4
+ autoload :TimeSeriesWidget, "termplot/widgets/time_series_widget"
5
+ autoload :StatisticsWidget, "termplot/widgets/statistics_widget"
6
+ autoload :HistogramWidget, "termplot/widgets/histogram_widget"
7
+ end
8
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "termplot/widgets/border"
4
+ require "termplot/widgets/dataset"
5
+
6
+ module Termplot
7
+ module Widgets
8
+ class BaseWidget
9
+ include Renderable
10
+
11
+ attr_reader(
12
+ :cols,
13
+ :rows,
14
+ :window,
15
+ :bordered_window,
16
+ :errors,
17
+ :title,
18
+ :decimals,
19
+ :dataset
20
+ )
21
+
22
+ def initialize(**opts)
23
+ @cols = opts[:cols] >= min_cols ? opts[:cols] : min_cols
24
+ @rows = opts[:rows] >= min_rows ? opts[:rows] : min_rows
25
+ @window = Window.new(
26
+ cols: @cols,
27
+ rows: @rows
28
+ )
29
+
30
+ @bordered_window = BorderedWindow.new(window, default_border_size)
31
+ @debug = opts[:debug]
32
+ @errors = []
33
+
34
+ @title = opts[:title]
35
+ @decimals = 2
36
+
37
+ @dataset = Dataset.new(max_count)
38
+
39
+ post_initialize(opts)
40
+ end
41
+
42
+ def post_initialize(opts)
43
+ # Implemented by subclasses
44
+ end
45
+
46
+ def <<(point)
47
+ dataset << point
48
+ dataset.set_capacity(max_count)
49
+ end
50
+
51
+ def render_to_window
52
+ raise "Must be implemented"
53
+ end
54
+
55
+ private
56
+ def max_count
57
+ 10_000
58
+ end
59
+
60
+ BorderedWindow = Struct.new(:window, :border_size) do
61
+ def inner_width
62
+ window.cols - border_size.left - border_size.right
63
+ end
64
+
65
+ def inner_height
66
+ window.rows - border_size.top - border_size.bottom
67
+ end
68
+
69
+ def method_missing(method, *args, &block)
70
+ if window.respond_to?(method)
71
+ window.send(method, *args, &block)
72
+ else
73
+ super
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,6 @@
1
+ module Termplot
2
+ module Widgets
3
+ class Border < Struct.new(:top, :right, :bottom, :left)
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,50 @@
1
+ require "termplot/widgets/statistics"
2
+
3
+ module Termplot
4
+ module Widgets
5
+ class Dataset
6
+ include Enumerable
7
+ include Statistics
8
+
9
+ attr_reader :capacity, :min, :max, :range, :data
10
+
11
+ def initialize(capacity)
12
+ @data = []
13
+ @min = 0
14
+ @max = 0
15
+ @range = 0
16
+ @capacity = capacity
17
+ end
18
+
19
+ def each(&block)
20
+ data.each(&block)
21
+ end
22
+
23
+ def << (point)
24
+ data.push(point)
25
+
26
+ discard_excess
27
+
28
+ @min = data.min
29
+ @max = data.max
30
+ @range = (max - min).abs
31
+ @range = 1 if range.zero?
32
+ end
33
+
34
+ def set_capacity(capacity)
35
+ @capacity = capacity
36
+ discard_excess
37
+ end
38
+
39
+ def empty?
40
+ data.empty?
41
+ end
42
+
43
+ private
44
+ def discard_excess
45
+ excess = [0, data.length - capacity].max
46
+ data.shift(excess)
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,196 @@
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
+ require "termplot/colors"
9
+
10
+ module Termplot
11
+ module Widgets
12
+ class HistogramWidget < BaseWidget
13
+ DEFAULT_COLOR = "green"
14
+ attr_reader :color
15
+
16
+ def post_initialize(opts)
17
+ @color = opts[:color] || DEFAULT_COLOR
18
+ end
19
+
20
+ def render_to_window
21
+ errors.clear
22
+ window.clear
23
+ window.cursor.reset_position
24
+
25
+ bins = calculate_bins
26
+ bins = bin_data(bins)
27
+ bins = calculate_bin_coordinates(bins)
28
+ calculate_axis_size(bins)
29
+ render_bins(bins)
30
+ window.cursor.reset_position
31
+
32
+ # Title bar
33
+ Termplot::Renderers::TextRenderer.new(
34
+ bordered_window: bordered_window,
35
+ text: title_text,
36
+ row: 0,
37
+ errors: errors
38
+ ).render
39
+
40
+ window.cursor.reset_position
41
+
42
+ # Borders
43
+ Termplot::Renderers::BorderRenderer.new(
44
+ bordered_window: bordered_window
45
+ ).render
46
+
47
+ window.cursor.reset_position
48
+
49
+ # Ticks
50
+ render_ticks(bins)
51
+ window.cursor.reset_position
52
+ end
53
+
54
+ private
55
+
56
+ def default_border_size
57
+ Border.new(2, 1, 1, 4)
58
+ end
59
+
60
+ def calculate_axis_size(bins)
61
+ return if bins.empty?
62
+ border_left = bins.map { |bin| format_tick_label(bin.midpoint).length }.max
63
+ border_left += 2
64
+
65
+ # Clamp border_left to prevent the renderer from crashing
66
+ # with very large numbers
67
+ if border_left > cols - 5
68
+ errors.push(Colors.yellow("Warning: Axis tick values have been clipped, consider using more columns with -c"))
69
+ border_left = cols - 5
70
+ end
71
+
72
+ @bordered_window.border_size = Border.new(2, 1, 1, border_left)
73
+ end
74
+
75
+ def min_cols
76
+ default_border_size.left + default_border_size.right + 5
77
+ end
78
+
79
+ def num_bins
80
+ bordered_window.inner_height
81
+ end
82
+
83
+ def min_rows
84
+ default_border_size.top + default_border_size.bottom + 1
85
+ end
86
+
87
+ def title_text
88
+ bin_char + " " + title
89
+ end
90
+
91
+ def render_bins(positioned_bins)
92
+ positioned_bins.each do |bin|
93
+ window.cursor.beginning_of_line
94
+ window.cursor.row = bin.y + bordered_window.border_size.top
95
+ window.cursor.forward(bordered_window.border_size.left)
96
+ bin.x.times { window.write(bin_char) }
97
+ window.write(" ")
98
+
99
+ bin.count.to_s.chars.each do |char|
100
+ window.write(char)
101
+ end
102
+ end
103
+ end
104
+
105
+ def render_ticks(positioned_bins)
106
+ positioned_bins.each do |bin|
107
+ window.cursor.row = bin.y + bordered_window.border_size.top
108
+ window.cursor.beginning_of_line
109
+
110
+ format_tick_label(bin.midpoint).rjust(bordered_window.border_size.left - 2, " ").chars.first(bordered_window.border_size.left - 2).each do |c|
111
+ window.write(c)
112
+ end
113
+ window.write(" ")
114
+ window.write(border_char_map[:tick_right])
115
+ end
116
+ end
117
+
118
+ def border_char_map
119
+ CharacterMap::DEFAULT
120
+ end
121
+
122
+ def format_tick_label(value)
123
+ "%.#{decimals}f" % value.round(decimals)
124
+ end
125
+
126
+ PositionedBin = Struct.new(:bin, :x, :y) do
127
+ extend(Forwardable)
128
+ def_delegators(:bin, :count, :min, :max, :midpoint)
129
+ end
130
+
131
+ def calculate_bin_coordinates(bins)
132
+ return [] unless bins.any?
133
+ max_count = bins.max_by { |bin| bin.count }&.count
134
+
135
+ bins.map.with_index do |bin, i|
136
+ row = i
137
+ # Save some chars for count
138
+ col = ((bin.count.to_f / max_count) * (bordered_window.inner_width - 4)).floor
139
+ PositionedBin.new(bin, col, row)
140
+ end
141
+ end
142
+
143
+ def bin_data(bins)
144
+ return [] unless bins.any?
145
+
146
+ dataset.each do |value|
147
+ bin = bins.find { |b| b.min <= value && b.max > value }
148
+ bin.count += 1 unless bin.nil?
149
+ end
150
+
151
+ bins
152
+ end
153
+
154
+ Bin = Struct.new(:min, :max, :count) do
155
+ def size
156
+ max - min
157
+ end
158
+
159
+ def midpoint
160
+ (max + min) / 2
161
+ end
162
+ end
163
+
164
+ def calculate_bins
165
+ return [] if dataset.empty?
166
+
167
+ min = dataset.min
168
+ max = dataset.max
169
+ bin_size = dataset.range.to_f / num_bins.to_f
170
+
171
+ if bin_size.zero?
172
+ min -= 1
173
+ max += 1
174
+ bin_size = 1
175
+ end
176
+
177
+ bins = []
178
+ while min < max && bins.length < num_bins
179
+ bins.push(Bin.new(min, min + bin_size, 0))
180
+ min += bin_size
181
+ end
182
+
183
+ # Correct for floating point errors on max bin
184
+ if bins.any?
185
+ bins.last.max = max if bins.last.max < max
186
+ end
187
+
188
+ bins
189
+ end
190
+
191
+ def bin_char
192
+ Colors.send(color, "▇")
193
+ end
194
+ end
195
+ end
196
+ end