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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/README.md +124 -54
- data/Rakefile +27 -14
- data/doc/dash.png +0 -0
- data/doc/demo.png +0 -0
- data/doc/file.png +0 -0
- data/doc/memory.png +0 -0
- data/doc/ping.png +0 -0
- data/doc/sin.png +0 -0
- data/doc/tcp.png +0 -0
- data/examples/sample.rb +17 -0
- data/lib/termplot/character_map.rb +15 -4
- data/lib/termplot/cli.rb +16 -3
- data/lib/termplot/colors.rb +7 -0
- data/lib/termplot/commands.rb +27 -0
- data/lib/termplot/consumers.rb +12 -0
- data/lib/termplot/consumers/base_consumer.rb +132 -0
- data/lib/termplot/consumers/command_consumer.rb +14 -0
- data/lib/termplot/consumers/multi_source_consumer.rb +33 -0
- data/lib/termplot/consumers/single_source_consumer.rb +36 -0
- data/lib/termplot/consumers/stdin_consumer.rb +11 -0
- data/lib/termplot/cursors/buffered_console_cursor.rb +1 -1
- data/lib/termplot/cursors/virtual_cursor.rb +4 -0
- data/lib/termplot/dsl/panels.rb +80 -0
- data/lib/termplot/dsl/widgets.rb +128 -0
- data/lib/termplot/file_config.rb +37 -0
- data/lib/termplot/message_broker.rb +108 -0
- data/lib/termplot/options.rb +100 -20
- data/lib/termplot/positioned_widget.rb +8 -0
- data/lib/termplot/producer_options.rb +3 -0
- data/lib/termplot/producers.rb +3 -3
- data/lib/termplot/producers/base_producer.rb +12 -15
- data/lib/termplot/producers/command_producer.rb +25 -9
- data/lib/termplot/producers/stdin_producer.rb +1 -4
- data/lib/termplot/renderable.rb +35 -0
- data/lib/termplot/renderer.rb +16 -257
- data/lib/termplot/renderers.rb +6 -0
- data/lib/termplot/renderers/border_renderer.rb +48 -0
- data/lib/termplot/renderers/text_renderer.rb +73 -0
- data/lib/termplot/shell.rb +13 -9
- data/lib/termplot/utils/ansi_safe_string.rb +68 -0
- data/lib/termplot/version.rb +1 -1
- data/lib/termplot/widget_dsl.rb +130 -0
- data/lib/termplot/widgets.rb +8 -0
- data/lib/termplot/widgets/base_widget.rb +79 -0
- data/lib/termplot/widgets/border.rb +6 -0
- data/lib/termplot/widgets/dataset.rb +50 -0
- data/lib/termplot/widgets/histogram_widget.rb +196 -0
- data/lib/termplot/widgets/statistics.rb +21 -0
- data/lib/termplot/widgets/statistics_widget.rb +104 -0
- data/lib/termplot/widgets/time_series_widget.rb +248 -0
- data/lib/termplot/window.rb +25 -5
- data/termplot.gemspec +1 -6
- metadata +36 -24
- data/doc/MSFT.png +0 -0
- data/doc/cpu.png +0 -0
- data/doc/demo.cast +0 -638
- data/lib/termplot/consumer.rb +0 -75
- data/lib/termplot/cursors/console_cursor.rb +0 -57
- 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
|
data/lib/termplot/version.rb
CHANGED
@@ -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,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
|