mini_histogram 0.2.2 → 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/CHANGELOG.md +4 -0
- data/README.md +67 -1
- data/lib/mini_histogram/plot.rb +574 -510
- data/lib/mini_histogram/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b95e08050cb7942fba011d3c18bcd493064695bd15206826752a9432ba147f0a
|
4
|
+
data.tar.gz: cb225c6fcc1d5da019f1037ba7559c89b6957b98e3571d13cdeb780795d06a4d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a33e73068d9f3c7c85db911a392cf9728eccecbbd6c744fd33a2aa42c2d0218b63819f9b6e8627fc1f68b33adb9551bceaa191d6f6a1dbace7b0380aadedcd36
|
7
|
+
data.tar.gz: e45c116668523722f82c41644874e21ec5ad39986c0222e3bc34edc343badb9ccf2478b7384ab99caeb7c36aafe4745cd1ac07edf46f034865f00b2ac11cca04
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -38,7 +38,7 @@ puts histogram.weights
|
|
38
38
|
|
39
39
|
This means that the `array` here had three items between 0.0 and 2.0, four items between 4.0 and 6.0 and three items between 10.0 and 12.0
|
40
40
|
|
41
|
-
## Plotting
|
41
|
+
## Plotting [experimental]
|
42
42
|
|
43
43
|
You can plot!
|
44
44
|
|
@@ -73,6 +73,72 @@ histogram = MiniHistogram.new(array)
|
|
73
73
|
puts UnicodePlot.histogram(histogram)
|
74
74
|
```
|
75
75
|
|
76
|
+
## Plotting dualing histograms [experimental]
|
77
|
+
|
78
|
+
If you're plotting multiple histograms (first, please normalize the bucket sizes), second. It can be hard to compare them vertically. Here's an example:
|
79
|
+
|
80
|
+
```
|
81
|
+
┌ ┐
|
82
|
+
[11.2 , 11.28) ┤▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 12
|
83
|
+
[11.28, 11.36) ┤▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 22
|
84
|
+
[11.35, 11.43) ┤▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 30
|
85
|
+
[11.43, 11.51) ┤▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 17
|
86
|
+
[11.5 , 11.58) ┤▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 13
|
87
|
+
[11.58, 11.66) ┤▇▇▇▇▇▇▇ 6
|
88
|
+
[11.65, 11.73) ┤ 0
|
89
|
+
[11.73, 11.81) ┤ 0
|
90
|
+
[11.8 , 11.88) ┤ 0
|
91
|
+
└ ┘
|
92
|
+
Frequency
|
93
|
+
┌ ┐
|
94
|
+
[11.2 , 11.28) ┤▇▇▇▇ 3
|
95
|
+
[11.28, 11.36) ┤▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 19
|
96
|
+
[11.35, 11.43) ┤▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 17
|
97
|
+
[11.43, 11.51) ┤▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 25
|
98
|
+
[11.5 , 11.58) ┤▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 15
|
99
|
+
[11.58, 11.66) ┤▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 13
|
100
|
+
[11.65, 11.73) ┤▇▇▇▇ 3
|
101
|
+
[11.73, 11.81) ┤▇▇▇▇ 3
|
102
|
+
[11.8 , 11.88) ┤▇▇▇ 2
|
103
|
+
└ ┘
|
104
|
+
Frequency
|
105
|
+
```
|
106
|
+
|
107
|
+
Here's the same data set plotted side-by-side:
|
108
|
+
|
109
|
+
```
|
110
|
+
┌ ┐ ┌ ┐
|
111
|
+
[11.2 , 11.28) ┤▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 12 [11.2 , 11.28) ┤▇▇▇▇ 3
|
112
|
+
[11.28, 11.36) ┤▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 22 [11.28, 11.36) ┤▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 19
|
113
|
+
[11.35, 11.43) ┤▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 30 [11.35, 11.43) ┤▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 17
|
114
|
+
[11.43, 11.51) ┤▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 17 [11.43, 11.51) ┤▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 25
|
115
|
+
[11.5 , 11.58) ┤▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 13 [11.5 , 11.58) ┤▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 15
|
116
|
+
[11.58, 11.66) ┤▇▇▇▇▇▇▇ 6 [11.58, 11.66) ┤▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 13
|
117
|
+
[11.65, 11.73) ┤ 0 [11.65, 11.73) ┤▇▇▇▇ 3
|
118
|
+
[11.73, 11.81) ┤ 0 [11.73, 11.81) ┤▇▇▇▇ 3
|
119
|
+
[11.8 , 11.88) ┤ 0 [11.8 , 11.88) ┤▇▇▇ 2
|
120
|
+
└ ┘ └ ┘
|
121
|
+
Frequency Frequency
|
122
|
+
```
|
123
|
+
|
124
|
+
This method might require more scrolling in the github issue, but makes it easier to compare two distributions. Here's how you plot dualing histograms:
|
125
|
+
|
126
|
+
```
|
127
|
+
require 'mini_histogram/plot'
|
128
|
+
|
129
|
+
a = MiniHistogram.new [11.205184, 11.223665, 11.228286, 11.23219, 11.233325, 11.234516, 11.245781, 11.248441, 11.250758, 11.255686, 11.265876, 11.26641, 11.279456, 11.281067, 11.284281, 11.287656, 11.289316, 11.289682, 11.292289, 11.294518, 11.296454, 11.299277, 11.305801, 11.306602, 11.309311, 11.318465, 11.318477, 11.322258, 11.328267, 11.334188, 11.339722, 11.340585, 11.346084, 11.346197, 11.351863, 11.35982, 11.362358, 11.364476, 11.365743, 11.368492, 11.368566, 11.36869, 11.37268, 11.374204, 11.374217, 11.374955, 11.376422, 11.377989, 11.383357, 11.383593, 11.385184, 11.394766, 11.395829, 11.398455, 11.399739, 11.401304, 11.411387, 11.411978, 11.413585, 11.413659, 11.418504, 11.419194, 11.419415, 11.421374, 11.4261, 11.427901, 11.429651, 11.434272, 11.435012, 11.440848, 11.447495, 11.456107, 11.457434, 11.467112, 11.471005, 11.473235, 11.485025, 11.485852, 11.488256, 11.488275, 11.499545, 11.509588, 11.51378, 11.51544, 11.520783, 11.52246, 11.522855, 11.5322, 11.533764, 11.544047, 11.552597, 11.558062, 11.567239, 11.569749, 11.575796, 11.588014, 11.614032, 11.615062, 11.618194, 11.635267]
|
130
|
+
b = MiniHistogram.new [11.233813, 11.240717, 11.254617, 11.282013, 11.290658, 11.303213, 11.305237, 11.305299, 11.306397, 11.313867, 11.31397, 11.314444, 11.318032, 11.328111, 11.330127, 11.333235, 11.33678, 11.337799, 11.343758, 11.347798, 11.347915, 11.349594, 11.358198, 11.358507, 11.3628, 11.366111, 11.374993, 11.378195, 11.38166, 11.384867, 11.385235, 11.395825, 11.404434, 11.406065, 11.406677, 11.410244, 11.414527, 11.421267, 11.424535, 11.427231, 11.427869, 11.428548, 11.432594, 11.433524, 11.434903, 11.437769, 11.439761, 11.443437, 11.443846, 11.451106, 11.458503, 11.462256, 11.462324, 11.464342, 11.464716, 11.46477, 11.465271, 11.466843, 11.468789, 11.475492, 11.488113, 11.489616, 11.493736, 11.496842, 11.502074, 11.511367, 11.512634, 11.515562, 11.525771, 11.531415, 11.535379, 11.53966, 11.540969, 11.541265, 11.541978, 11.545301, 11.545533, 11.545701, 11.572584, 11.578881, 11.580701, 11.580922, 11.588731, 11.594082, 11.595915, 11.613622, 11.619884, 11.632889, 11.64377, 11.645225, 11.647167, 11.648257, 11.667158, 11.670378, 11.681261, 11.734586, 11.747066, 11.792425, 11.808377, 11.812346]
|
131
|
+
|
132
|
+
dual_histogram = MiniHistogram.dual_plot do |x, y|
|
133
|
+
x.histogram = a
|
134
|
+
x.options = {}
|
135
|
+
y.histogram = b
|
136
|
+
y.options = {}
|
137
|
+
end
|
138
|
+
puts dual_histogram
|
139
|
+
```
|
140
|
+
|
141
|
+
|
76
142
|
## Alternatives
|
77
143
|
|
78
144
|
Alternatives to this gem include https://github.com/mrkn/enumerable-statistics/. I needed this gem to be able to calculate a "shared" or "average" edge value as seen in this PR https://github.com/mrkn/enumerable-statistics/pull/23. So that I could add histograms to derailed benchmarks: https://github.com/schneems/derailed_benchmarks/pull/169. This gem provides a `MiniHistogram.set_average_edges!` method to help there. Also this gem does not require a native extension compilation (faster to install, but performance is slower), and this gem does not extend or monkeypatch an core classes.
|
data/lib/mini_histogram/plot.rb
CHANGED
@@ -11,9 +11,69 @@
|
|
11
11
|
# require 'mini_histogram/plot'
|
12
12
|
# array = 50.times.map { rand(11.2..11.6) }
|
13
13
|
# histogram = MiniHistogram.new(array)
|
14
|
-
# puts histogram.plot
|
14
|
+
# puts histogram.plot => Generates a plot
|
15
15
|
#
|
16
16
|
class MiniHistogram
|
17
|
+
|
18
|
+
# This is an object that holds a histogram
|
19
|
+
# and it's corresponding plot options
|
20
|
+
#
|
21
|
+
# Example:
|
22
|
+
#
|
23
|
+
# x = PlotValue.new
|
24
|
+
# x.values = [1,2,3,4,5]
|
25
|
+
# x.options = {xlabel: "random"}
|
26
|
+
#
|
27
|
+
# x.plot # => Generates a histogram plot with these values and options
|
28
|
+
class PlotValue
|
29
|
+
attr_accessor :histogram, :options
|
30
|
+
|
31
|
+
def initialize
|
32
|
+
@histogram = nil
|
33
|
+
@options = {}
|
34
|
+
end
|
35
|
+
|
36
|
+
def plot
|
37
|
+
raise "@histogram cannot be empty set via `values=` or `histogram=` methods" if @histogram.nil?
|
38
|
+
|
39
|
+
@histogram.plot(**@options)
|
40
|
+
end
|
41
|
+
|
42
|
+
def values=(values)
|
43
|
+
@histogram = MiniHistogram.new(values)
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.dual_plot(plot_a, plot_b)
|
47
|
+
a_lines = plot_a.to_s.lines
|
48
|
+
b_lines = plot_b.to_s.lines
|
49
|
+
|
50
|
+
max_length = a_lines.map(&:length).max
|
51
|
+
|
52
|
+
side_by_side = String.new("")
|
53
|
+
a_lines.each_index do |i|
|
54
|
+
side_by_side << a_lines[i].chomp.ljust(max_length) # Remove newline, ensure same length
|
55
|
+
side_by_side << b_lines[i]
|
56
|
+
end
|
57
|
+
|
58
|
+
return side_by_side
|
59
|
+
end
|
60
|
+
end
|
61
|
+
private_constant :PlotValue
|
62
|
+
|
63
|
+
def self.dual_plot
|
64
|
+
a = PlotValue.new
|
65
|
+
b = PlotValue.new
|
66
|
+
|
67
|
+
yield a, b
|
68
|
+
|
69
|
+
if b.options[:ylabel] == a.options[:ylabel]
|
70
|
+
b.options[:ylabel] = nil
|
71
|
+
end
|
72
|
+
|
73
|
+
MiniHistogram.set_average_edges!(a.histogram, b.histogram)
|
74
|
+
PlotValue.dual_plot(a.plot, b.plot)
|
75
|
+
end
|
76
|
+
|
17
77
|
def plot(
|
18
78
|
nbins: nil,
|
19
79
|
closed: :left,
|
@@ -50,7 +110,7 @@ class MiniHistogram
|
|
50
110
|
"\e[90m#{r_str}\e[0m"
|
51
111
|
end
|
52
112
|
xscale = kw.delete(:xscale)
|
53
|
-
xlabel = kw.delete(:xlabel) || ValueTransformer.transform_name(xscale, "Frequency")
|
113
|
+
xlabel = kw.delete(:xlabel) || MiniUnicodePlot::ValueTransformer.transform_name(xscale, "Frequency")
|
54
114
|
barplot(labels, counts,
|
55
115
|
symbol: symbol,
|
56
116
|
xscale: xscale,
|
@@ -58,6 +118,7 @@ class MiniHistogram
|
|
58
118
|
**kw)
|
59
119
|
end
|
60
120
|
|
121
|
+
## Begin copy/pasta from unicode_plot.rb with some slight modifications
|
61
122
|
private def barplot(
|
62
123
|
*args,
|
63
124
|
width: 40,
|
@@ -88,7 +149,7 @@ class MiniHistogram
|
|
88
149
|
end
|
89
150
|
|
90
151
|
xlabel ||= ValueTransformer.transform_name(xscale)
|
91
|
-
plot = Barplot.new(heights, width, color, symbol, xscale,
|
152
|
+
plot = MiniUnicodePlot::Barplot.new(heights, width, color, symbol, xscale,
|
92
153
|
border: border, xlabel: xlabel,
|
93
154
|
**kw)
|
94
155
|
keys.each_with_index do |key, i|
|
@@ -122,597 +183,600 @@ class MiniHistogram
|
|
122
183
|
x.to_i == x && INT64_MIN <= x && x < INT64_MAX
|
123
184
|
end
|
124
185
|
|
125
|
-
module
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
186
|
+
module MiniUnicodePlot
|
187
|
+
module ValueTransformer
|
188
|
+
PREDEFINED_TRANSFORM_FUNCTIONS = {
|
189
|
+
log: Math.method(:log),
|
190
|
+
ln: Math.method(:log),
|
191
|
+
log10: Math.method(:log10),
|
192
|
+
lg: Math.method(:log10),
|
193
|
+
log2: Math.method(:log2),
|
194
|
+
lb: Math.method(:log2),
|
195
|
+
}.freeze
|
134
196
|
|
135
|
-
|
136
|
-
|
197
|
+
def transform_values(func, values)
|
198
|
+
return values unless func
|
137
199
|
|
138
|
-
unless func.respond_to?(:call)
|
139
|
-
func = PREDEFINED_TRANSFORM_FUNCTIONS[func]
|
140
200
|
unless func.respond_to?(:call)
|
141
|
-
|
201
|
+
func = PREDEFINED_TRANSFORM_FUNCTIONS[func]
|
202
|
+
unless func.respond_to?(:call)
|
203
|
+
raise ArgumentError, "func must be callable"
|
204
|
+
end
|
142
205
|
end
|
143
|
-
end
|
144
206
|
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
207
|
+
case values
|
208
|
+
when Numeric
|
209
|
+
func.(values)
|
210
|
+
else
|
211
|
+
values.map(&func)
|
212
|
+
end
|
150
213
|
end
|
151
|
-
end
|
152
214
|
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
215
|
+
module_function def transform_name(func, basename="")
|
216
|
+
return basename unless func
|
217
|
+
case func
|
218
|
+
when String, Symbol
|
219
|
+
name = func
|
220
|
+
when ->(f) { f.respond_to?(:name) }
|
221
|
+
name = func.name
|
222
|
+
else
|
223
|
+
name = "custom"
|
224
|
+
end
|
225
|
+
"#{basename} [#{name}]"
|
226
|
+
end
|
164
227
|
end
|
165
|
-
end
|
166
228
|
|
167
229
|
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
230
|
+
module BorderMaps
|
231
|
+
BORDER_SOLID = {
|
232
|
+
tl: "┌",
|
233
|
+
tr: "┐",
|
234
|
+
bl: "└",
|
235
|
+
br: "┘",
|
236
|
+
t: "─",
|
237
|
+
l: "│",
|
238
|
+
b: "─",
|
239
|
+
r: "│"
|
240
|
+
}.freeze
|
241
|
+
|
242
|
+
BORDER_CORNERS = {
|
243
|
+
tl: "┌",
|
244
|
+
tr: "┐",
|
245
|
+
bl: "└",
|
246
|
+
br: "┘",
|
247
|
+
t: " ",
|
248
|
+
l: " ",
|
249
|
+
b: " ",
|
250
|
+
r: " ",
|
251
|
+
}.freeze
|
252
|
+
|
253
|
+
BORDER_BARPLOT = {
|
254
|
+
tl: "┌",
|
255
|
+
tr: "┐",
|
256
|
+
bl: "└",
|
257
|
+
br: "┘",
|
258
|
+
t: " ",
|
259
|
+
l: "┤",
|
260
|
+
b: " ",
|
261
|
+
r: " ",
|
262
|
+
}.freeze
|
263
|
+
end
|
264
|
+
|
265
|
+
BORDER_MAP = {
|
266
|
+
solid: BorderMaps::BORDER_SOLID,
|
267
|
+
corners: BorderMaps::BORDER_CORNERS,
|
268
|
+
barplot: BorderMaps::BORDER_BARPLOT,
|
189
269
|
}.freeze
|
190
270
|
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
light_green: "\033[92m",
|
223
|
-
light_yellow: "\033[93m",
|
224
|
-
light_blue: "\033[94m",
|
225
|
-
light_magenta: "\033[95m",
|
226
|
-
light_cyan: "\033[96m",
|
227
|
-
normal: "\033[0m",
|
228
|
-
default: "\033[39m",
|
229
|
-
bold: "\033[1m",
|
230
|
-
underline: "\033[4m",
|
231
|
-
blink: "\033[5m",
|
232
|
-
reverse: "\033[7m",
|
233
|
-
hidden: "\033[8m",
|
234
|
-
nothing: "",
|
235
|
-
}
|
236
|
-
|
237
|
-
0.upto(255) do |i|
|
238
|
-
TEXT_COLORS[i] = "\033[38;5;#{i}m"
|
239
|
-
end
|
240
|
-
|
241
|
-
TEXT_COLORS.freeze
|
242
|
-
|
243
|
-
DISABLE_TEXT_STYLE = {
|
244
|
-
bold: "\033[22m",
|
245
|
-
underline: "\033[24m",
|
246
|
-
blink: "\033[25m",
|
247
|
-
reverse: "\033[27m",
|
248
|
-
hidden: "\033[28m",
|
249
|
-
normal: "",
|
250
|
-
default: "",
|
251
|
-
nothing: "",
|
252
|
-
}.freeze
|
253
|
-
|
254
|
-
COLOR_ENCODE = {
|
255
|
-
normal: 0b000,
|
256
|
-
blue: 0b001,
|
257
|
-
red: 0b010,
|
258
|
-
magenta: 0b011,
|
259
|
-
green: 0b100,
|
260
|
-
cyan: 0b101,
|
261
|
-
yellow: 0b110,
|
262
|
-
white: 0b111
|
263
|
-
}.freeze
|
271
|
+
module StyledPrinter
|
272
|
+
TEXT_COLORS = {
|
273
|
+
black: "\033[30m",
|
274
|
+
red: "\033[31m",
|
275
|
+
green: "\033[32m",
|
276
|
+
yellow: "\033[33m",
|
277
|
+
blue: "\033[34m",
|
278
|
+
magenta: "\033[35m",
|
279
|
+
cyan: "\033[36m",
|
280
|
+
white: "\033[37m",
|
281
|
+
gray: "\033[90m",
|
282
|
+
light_black: "\033[90m",
|
283
|
+
light_red: "\033[91m",
|
284
|
+
light_green: "\033[92m",
|
285
|
+
light_yellow: "\033[93m",
|
286
|
+
light_blue: "\033[94m",
|
287
|
+
light_magenta: "\033[95m",
|
288
|
+
light_cyan: "\033[96m",
|
289
|
+
normal: "\033[0m",
|
290
|
+
default: "\033[39m",
|
291
|
+
bold: "\033[1m",
|
292
|
+
underline: "\033[4m",
|
293
|
+
blink: "\033[5m",
|
294
|
+
reverse: "\033[7m",
|
295
|
+
hidden: "\033[8m",
|
296
|
+
nothing: "",
|
297
|
+
}
|
298
|
+
|
299
|
+
0.upto(255) do |i|
|
300
|
+
TEXT_COLORS[i] = "\033[38;5;#{i}m"
|
301
|
+
end
|
264
302
|
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
303
|
+
TEXT_COLORS.freeze
|
304
|
+
|
305
|
+
DISABLE_TEXT_STYLE = {
|
306
|
+
bold: "\033[22m",
|
307
|
+
underline: "\033[24m",
|
308
|
+
blink: "\033[25m",
|
309
|
+
reverse: "\033[27m",
|
310
|
+
hidden: "\033[28m",
|
311
|
+
normal: "",
|
312
|
+
default: "",
|
313
|
+
nothing: "",
|
314
|
+
}.freeze
|
315
|
+
|
316
|
+
COLOR_ENCODE = {
|
317
|
+
normal: 0b000,
|
318
|
+
blue: 0b001,
|
319
|
+
red: 0b010,
|
320
|
+
magenta: 0b011,
|
321
|
+
green: 0b100,
|
322
|
+
cyan: 0b101,
|
323
|
+
yellow: 0b110,
|
324
|
+
white: 0b111
|
325
|
+
}.freeze
|
326
|
+
|
327
|
+
COLOR_DECODE = COLOR_ENCODE.map {|k, v| [v, k] }.to_h.freeze
|
328
|
+
|
329
|
+
def print_styled(out, *args, bold: false, color: :normal)
|
330
|
+
return out.print(*args) unless color?(out)
|
331
|
+
|
332
|
+
str = StringIO.open {|sio| sio.print(*args); sio.close; sio.string }
|
333
|
+
color = :nothing if bold && color == :bold
|
334
|
+
enable_ansi = TEXT_COLORS.fetch(color, TEXT_COLORS[:default]) +
|
335
|
+
(bold ? TEXT_COLORS[:bold] : "")
|
336
|
+
disable_ansi = (bold ? DISABLE_TEXT_STYLE[:bold] : "") +
|
337
|
+
DISABLE_TEXT_STYLE.fetch(color, TEXT_COLORS[:default])
|
338
|
+
first = true
|
339
|
+
StringIO.open do |sio|
|
340
|
+
str.each_line do |line|
|
341
|
+
sio.puts unless first
|
342
|
+
first = false
|
343
|
+
continue if line.empty?
|
344
|
+
sio.print(enable_ansi, line, disable_ansi)
|
345
|
+
end
|
346
|
+
sio.close
|
347
|
+
out.print(sio.string)
|
283
348
|
end
|
284
|
-
sio.close
|
285
|
-
out.print(sio.string)
|
286
349
|
end
|
287
|
-
end
|
288
350
|
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
351
|
+
def print_color(out, color, *args)
|
352
|
+
color = COLOR_DECODE[color]
|
353
|
+
print_styled(out, *args, color: color)
|
354
|
+
end
|
293
355
|
|
294
|
-
|
295
|
-
|
356
|
+
def color?(out)
|
357
|
+
(out && out.tty?) || false
|
358
|
+
end
|
296
359
|
end
|
297
|
-
end
|
298
360
|
|
299
|
-
|
300
|
-
|
361
|
+
module BorderPrinter
|
362
|
+
include StyledPrinter
|
301
363
|
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
364
|
+
def print_border_top(out, padding, length, border=:solid, color: :light_black)
|
365
|
+
return if border == :none
|
366
|
+
b = BORDER_MAP[border]
|
367
|
+
print_styled(out, padding, b[:tl], b[:t] * length, b[:tr], color: color)
|
368
|
+
end
|
307
369
|
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
370
|
+
def print_border_bottom(out, padding, length, border=:solid, color: :light_black)
|
371
|
+
return if border == :none
|
372
|
+
b = BORDER_MAP[border]
|
373
|
+
print_styled(out, padding, b[:bl], b[:b] * length, b[:br], color: color)
|
374
|
+
end
|
312
375
|
end
|
313
|
-
end
|
314
376
|
|
315
|
-
|
316
|
-
|
377
|
+
class Renderer
|
378
|
+
include BorderPrinter
|
317
379
|
|
318
|
-
|
319
|
-
|
320
|
-
|
380
|
+
def self.render(out, plot)
|
381
|
+
new(plot).render(out)
|
382
|
+
end
|
321
383
|
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
384
|
+
def initialize(plot)
|
385
|
+
@plot = plot
|
386
|
+
@out = nil
|
387
|
+
end
|
326
388
|
|
327
|
-
|
328
|
-
|
389
|
+
attr_reader :plot
|
390
|
+
attr_reader :out
|
329
391
|
|
330
|
-
|
331
|
-
|
332
|
-
|
392
|
+
def render(out)
|
393
|
+
@out = out
|
394
|
+
init_render
|
333
395
|
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
396
|
+
render_top
|
397
|
+
render_rows
|
398
|
+
render_bottom
|
399
|
+
end
|
338
400
|
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
401
|
+
private
|
402
|
+
|
403
|
+
def render_top
|
404
|
+
# plot the title and the top border
|
405
|
+
print_title(@border_padding, plot.title, p_width: @border_length, color: :bold)
|
406
|
+
puts if plot.title_given?
|
407
|
+
|
408
|
+
if plot.show_labels?
|
409
|
+
topleft_str = plot.decorations.fetch(:tl, "")
|
410
|
+
topleft_col = plot.colors_deco.fetch(:tl, :light_black)
|
411
|
+
topmid_str = plot.decorations.fetch(:t, "")
|
412
|
+
topmid_col = plot.colors_deco.fetch(:t, :light_black)
|
413
|
+
topright_str = plot.decorations.fetch(:tr, "")
|
414
|
+
topright_col = plot.colors_deco.fetch(:tr, :light_black)
|
415
|
+
|
416
|
+
if topleft_str != "" || topright_str != "" || topmid_str != ""
|
417
|
+
topleft_len = topleft_str.length
|
418
|
+
topmid_len = topmid_str.length
|
419
|
+
topright_len = topright_str.length
|
420
|
+
print_styled(out, @border_padding, topleft_str, color: topleft_col)
|
421
|
+
cnt = (@border_length / 2.0 - topmid_len / 2.0 - topleft_len).round
|
422
|
+
pad = cnt > 0 ? " " * cnt : ""
|
423
|
+
print_styled(out, pad, topmid_str, color: topmid_col)
|
424
|
+
cnt = @border_length - topright_len - topleft_len - topmid_len + 2 - cnt
|
425
|
+
pad = cnt > 0 ? " " * cnt : ""
|
426
|
+
print_styled(out, pad, topright_str, "\n", color: topright_col)
|
427
|
+
end
|
365
428
|
end
|
366
|
-
end
|
367
429
|
|
368
|
-
|
369
|
-
|
370
|
-
|
430
|
+
print_border_top(out, @border_padding, @border_length, plot.border)
|
431
|
+
print(" " * @max_len_r, @plot_padding, "\n")
|
432
|
+
end
|
371
433
|
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
434
|
+
# render all rows
|
435
|
+
def render_rows
|
436
|
+
(0 ... plot.n_rows).each {|row| render_row(row) }
|
437
|
+
end
|
376
438
|
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
end
|
390
|
-
|
391
|
-
# print left annotations
|
392
|
-
print(" " * plot.margin)
|
393
|
-
if plot.show_labels?
|
394
|
-
if row == @y_lab_row
|
395
|
-
# print ylabel
|
396
|
-
print_styled(out, plot.ylabel, color: :normal)
|
397
|
-
print(" " * (@max_len_l - plot.ylabel_length - left_len))
|
398
|
-
else
|
399
|
-
# print padding to fill ylabel length
|
400
|
-
print(" " * (@max_len_l - left_len))
|
439
|
+
def render_row(row)
|
440
|
+
# Current labels to left and right of the row and their length
|
441
|
+
left_str = plot.labels_left.fetch(row, "")
|
442
|
+
left_col = plot.colors_left.fetch(row, :light_black)
|
443
|
+
right_str = plot.labels_right.fetch(row, "")
|
444
|
+
right_col = plot.colors_right.fetch(row, :light_black)
|
445
|
+
left_len = nocolor_string(left_str).length
|
446
|
+
right_len = nocolor_string(right_str).length
|
447
|
+
|
448
|
+
unless color?(out)
|
449
|
+
left_str = nocolor_string(left_str)
|
450
|
+
right_str = nocolor_string(right_str)
|
401
451
|
end
|
402
|
-
# print the left annotation
|
403
|
-
print_styled(out, left_str, color: left_col)
|
404
|
-
end
|
405
452
|
|
406
|
-
|
407
|
-
|
453
|
+
# print left annotations
|
454
|
+
print(" " * plot.margin)
|
455
|
+
if plot.show_labels?
|
456
|
+
if row == @y_lab_row
|
457
|
+
# print ylabel
|
458
|
+
print_styled(out, plot.ylabel, color: :normal)
|
459
|
+
print(" " * (@max_len_l - plot.ylabel_length - left_len))
|
460
|
+
else
|
461
|
+
# print padding to fill ylabel length
|
462
|
+
print(" " * (@max_len_l - left_len))
|
463
|
+
end
|
464
|
+
# print the left annotation
|
465
|
+
print_styled(out, left_str, color: left_col)
|
466
|
+
end
|
408
467
|
|
409
|
-
|
410
|
-
|
468
|
+
# print left border
|
469
|
+
print_styled(out, @plot_padding, @b[:l], color: :light_black)
|
411
470
|
|
412
|
-
|
413
|
-
|
414
|
-
if plot.show_labels?
|
415
|
-
print(@plot_padding)
|
416
|
-
print_styled(out, right_str, color: right_col)
|
417
|
-
print(" " * (@max_len_r - right_len))
|
418
|
-
end
|
419
|
-
puts
|
420
|
-
end
|
471
|
+
# print canvas row
|
472
|
+
plot.print_row(out, row)
|
421
473
|
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
botleft_col = plot.colors_deco.fetch(:bl, :light_black)
|
429
|
-
botmid_str = plot.decorations.fetch(:b, "")
|
430
|
-
botmid_col = plot.colors_deco.fetch(:b, :light_black)
|
431
|
-
botright_str = plot.decorations.fetch(:br, "")
|
432
|
-
botright_col = plot.colors_deco.fetch(:br, :light_black)
|
433
|
-
|
434
|
-
if botleft_str != "" || botright_str != "" || botmid_str != ""
|
435
|
-
puts
|
436
|
-
botleft_len = botleft_str.length
|
437
|
-
botmid_len = botmid_str.length
|
438
|
-
botright_len = botright_str.length
|
439
|
-
print_styled(out, @border_padding, botleft_str, color: botleft_col)
|
440
|
-
cnt = (@border_length / 2.0 - botmid_len / 2.0 - botleft_len).round
|
441
|
-
pad = cnt > 0 ? " " * cnt : ""
|
442
|
-
print_styled(out, pad, botmid_str, color: botmid_col)
|
443
|
-
cnt = @border_length - botright_len - botleft_len - botmid_len + 2 - cnt
|
444
|
-
pad = cnt > 0 ? " " * cnt : ""
|
445
|
-
print_styled(out, pad, botright_str, color: botright_col)
|
474
|
+
#print right label and padding
|
475
|
+
print_styled(out, @b[:r], color: :light_black)
|
476
|
+
if plot.show_labels?
|
477
|
+
print(@plot_padding)
|
478
|
+
print_styled(out, right_str, color: right_col)
|
479
|
+
print(" " * (@max_len_r - right_len))
|
446
480
|
end
|
447
|
-
|
448
|
-
# abuse the print_title function to print the xlabel. maybe refactor this
|
449
|
-
puts if plot.xlabel_given?
|
450
|
-
print_title(@border_padding, plot.xlabel, p_width: @border_length)
|
481
|
+
puts
|
451
482
|
end
|
452
|
-
end
|
453
483
|
|
454
|
-
|
455
|
-
|
456
|
-
|
484
|
+
def render_bottom
|
485
|
+
# draw bottom border and bottom labels
|
486
|
+
print_border_bottom(out, @border_padding, @border_length, plot.border)
|
487
|
+
print(" " * @max_len_r, @plot_padding)
|
488
|
+
if plot.show_labels?
|
489
|
+
botleft_str = plot.decorations.fetch(:bl, "")
|
490
|
+
botleft_col = plot.colors_deco.fetch(:bl, :light_black)
|
491
|
+
botmid_str = plot.decorations.fetch(:b, "")
|
492
|
+
botmid_col = plot.colors_deco.fetch(:b, :light_black)
|
493
|
+
botright_str = plot.decorations.fetch(:br, "")
|
494
|
+
botright_col = plot.colors_deco.fetch(:br, :light_black)
|
495
|
+
|
496
|
+
if botleft_str != "" || botright_str != "" || botmid_str != ""
|
497
|
+
puts
|
498
|
+
botleft_len = botleft_str.length
|
499
|
+
botmid_len = botmid_str.length
|
500
|
+
botright_len = botright_str.length
|
501
|
+
print_styled(out, @border_padding, botleft_str, color: botleft_col)
|
502
|
+
cnt = (@border_length / 2.0 - botmid_len / 2.0 - botleft_len).round
|
503
|
+
pad = cnt > 0 ? " " * cnt : ""
|
504
|
+
print_styled(out, pad, botmid_str, color: botmid_col)
|
505
|
+
cnt = @border_length - botright_len - botleft_len - botmid_len + 2 - cnt
|
506
|
+
pad = cnt > 0 ? " " * cnt : ""
|
507
|
+
print_styled(out, pad, botright_str, color: botright_col)
|
508
|
+
end
|
457
509
|
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
@max_len_r = plot.show_labels? && !plot.labels_right.empty? ?
|
463
|
-
plot.labels_right.each_value.map {|l| nocolor_string(l).length }.max :
|
464
|
-
0
|
465
|
-
if plot.show_labels? && plot.ylabel_given?
|
466
|
-
@max_len_l += plot.ylabel_length + 1
|
510
|
+
# abuse the print_title function to print the xlabel. maybe refactor this
|
511
|
+
puts if plot.xlabel_given?
|
512
|
+
print_title(@border_padding, plot.xlabel, p_width: @border_length)
|
513
|
+
end
|
467
514
|
end
|
468
515
|
|
469
|
-
|
470
|
-
|
516
|
+
def init_render
|
517
|
+
@b = BORDER_MAP[plot.border]
|
518
|
+
@border_length = plot.n_columns
|
519
|
+
|
520
|
+
# get length of largest strings to the left and right
|
521
|
+
@max_len_l = plot.show_labels? && !plot.labels_left.empty? ?
|
522
|
+
plot.labels_left.each_value.map {|l| nocolor_string(l).length }.max :
|
523
|
+
0
|
524
|
+
@max_len_r = plot.show_labels? && !plot.labels_right.empty? ?
|
525
|
+
plot.labels_right.each_value.map {|l| nocolor_string(l).length }.max :
|
526
|
+
0
|
527
|
+
if plot.show_labels? && plot.ylabel_given?
|
528
|
+
@max_len_l += plot.ylabel_length + 1
|
529
|
+
end
|
471
530
|
|
472
|
-
|
473
|
-
|
531
|
+
# offset where the plot (incl border) begins
|
532
|
+
@plot_offset = @max_len_l + plot.margin + plot.padding
|
474
533
|
|
475
|
-
|
476
|
-
|
534
|
+
# padding-string from left to border
|
535
|
+
@plot_padding = " " * plot.padding
|
477
536
|
|
478
|
-
|
479
|
-
|
480
|
-
end
|
537
|
+
# padding-string between labels and border
|
538
|
+
@border_padding = " " * @plot_offset
|
481
539
|
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
offset = [offset, 0].max
|
486
|
-
tpad = " " * offset
|
487
|
-
print_styled(out, padding, tpad, title, color: color)
|
488
|
-
end
|
540
|
+
# compute position of ylabel
|
541
|
+
@y_lab_row = (plot.n_rows / 2.0).round - 1
|
542
|
+
end
|
489
543
|
|
490
|
-
|
491
|
-
|
492
|
-
|
544
|
+
def print_title(padding, title, p_width: 0, color: :normal)
|
545
|
+
return unless title && title != ""
|
546
|
+
offset = (p_width / 2.0 - title.length / 2.0).round
|
547
|
+
offset = [offset, 0].max
|
548
|
+
tpad = " " * offset
|
549
|
+
print_styled(out, padding, tpad, title, color: color)
|
550
|
+
end
|
493
551
|
|
494
|
-
|
495
|
-
|
496
|
-
|
552
|
+
def print(*args)
|
553
|
+
out.print(*args)
|
554
|
+
end
|
497
555
|
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
end
|
556
|
+
def puts(*args)
|
557
|
+
out.puts(*args)
|
558
|
+
end
|
502
559
|
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
DEFAULT_WIDTH = 40
|
507
|
-
DEFAULT_BORDER = :solid
|
508
|
-
DEFAULT_MARGIN = 3
|
509
|
-
DEFAULT_PADDING = 1
|
510
|
-
|
511
|
-
def initialize(title: nil,
|
512
|
-
xlabel: nil,
|
513
|
-
ylabel: nil,
|
514
|
-
border: DEFAULT_BORDER,
|
515
|
-
margin: DEFAULT_MARGIN,
|
516
|
-
padding: DEFAULT_PADDING,
|
517
|
-
labels: true)
|
518
|
-
@title = title
|
519
|
-
@xlabel = xlabel
|
520
|
-
@ylabel = ylabel
|
521
|
-
@border = border
|
522
|
-
@margin = check_margin(margin)
|
523
|
-
@padding = padding
|
524
|
-
@labels_left = {}
|
525
|
-
@colors_left = {}
|
526
|
-
@labels_right = {}
|
527
|
-
@colors_right = {}
|
528
|
-
@decorations = {}
|
529
|
-
@colors_deco = {}
|
530
|
-
@show_labels = labels
|
531
|
-
@auto_color = 0
|
560
|
+
def nocolor_string(str)
|
561
|
+
str.to_s.gsub(/\e\[[0-9]+m/, "")
|
562
|
+
end
|
532
563
|
end
|
533
564
|
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
565
|
+
class Plot
|
566
|
+
include StyledPrinter
|
567
|
+
|
568
|
+
DEFAULT_WIDTH = 40
|
569
|
+
DEFAULT_BORDER = :solid
|
570
|
+
DEFAULT_MARGIN = 3
|
571
|
+
DEFAULT_PADDING = 1
|
572
|
+
|
573
|
+
def initialize(title: nil,
|
574
|
+
xlabel: nil,
|
575
|
+
ylabel: nil,
|
576
|
+
border: DEFAULT_BORDER,
|
577
|
+
margin: DEFAULT_MARGIN,
|
578
|
+
padding: DEFAULT_PADDING,
|
579
|
+
labels: true)
|
580
|
+
@title = title
|
581
|
+
@xlabel = xlabel
|
582
|
+
@ylabel = ylabel
|
583
|
+
@border = border
|
584
|
+
@margin = check_margin(margin)
|
585
|
+
@padding = padding
|
586
|
+
@labels_left = {}
|
587
|
+
@colors_left = {}
|
588
|
+
@labels_right = {}
|
589
|
+
@colors_right = {}
|
590
|
+
@decorations = {}
|
591
|
+
@colors_deco = {}
|
592
|
+
@show_labels = labels
|
593
|
+
@auto_color = 0
|
594
|
+
end
|
550
595
|
|
551
|
-
|
552
|
-
|
553
|
-
|
596
|
+
attr_reader :title
|
597
|
+
attr_reader :xlabel
|
598
|
+
attr_reader :ylabel
|
599
|
+
attr_reader :border
|
600
|
+
attr_reader :margin
|
601
|
+
attr_reader :padding
|
602
|
+
attr_reader :labels_left
|
603
|
+
attr_reader :colors_left
|
604
|
+
attr_reader :labels_right
|
605
|
+
attr_reader :colors_right
|
606
|
+
attr_reader :decorations
|
607
|
+
attr_reader :colors_deco
|
608
|
+
|
609
|
+
def title_given?
|
610
|
+
title && title != ""
|
611
|
+
end
|
554
612
|
|
555
|
-
|
556
|
-
|
557
|
-
|
613
|
+
def xlabel_given?
|
614
|
+
xlabel && xlabel != ""
|
615
|
+
end
|
558
616
|
|
559
|
-
|
560
|
-
|
561
|
-
|
617
|
+
def ylabel_given?
|
618
|
+
ylabel && ylabel != ""
|
619
|
+
end
|
562
620
|
|
563
|
-
|
564
|
-
|
565
|
-
|
621
|
+
def ylabel_length
|
622
|
+
(ylabel && ylabel.length) || 0
|
623
|
+
end
|
624
|
+
|
625
|
+
def show_labels?
|
626
|
+
@show_labels
|
627
|
+
end
|
566
628
|
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
629
|
+
def annotate!(loc, value, color: :normal)
|
630
|
+
case loc
|
631
|
+
when :l
|
632
|
+
(0 ... n_rows).each do |row|
|
633
|
+
if @labels_left.fetch(row, "") == ""
|
634
|
+
@labels_left[row] = value
|
635
|
+
@colors_left[row] = color
|
636
|
+
break
|
637
|
+
end
|
575
638
|
end
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
639
|
+
when :r
|
640
|
+
(0 ... n_rows).each do |row|
|
641
|
+
if @labels_right.fetch(row, "") == ""
|
642
|
+
@labels_right[row] = value
|
643
|
+
@colors_right[row] = color
|
644
|
+
break
|
645
|
+
end
|
583
646
|
end
|
647
|
+
when :t, :b, :tl, :tr, :bl, :br
|
648
|
+
@decorations[loc] = value
|
649
|
+
@colors_deco[loc] = color
|
650
|
+
else
|
651
|
+
raise ArgumentError,
|
652
|
+
"unknown location to annotate (#{loc.inspect} for :t, :b, :l, :r, :tl, :tr, :bl, or :br)"
|
584
653
|
end
|
585
|
-
when :t, :b, :tl, :tr, :bl, :br
|
586
|
-
@decorations[loc] = value
|
587
|
-
@colors_deco[loc] = color
|
588
|
-
else
|
589
|
-
raise ArgumentError,
|
590
|
-
"unknown location to annotate (#{loc.inspect} for :t, :b, :l, :r, :tl, :tr, :bl, or :br)"
|
591
654
|
end
|
592
|
-
end
|
593
655
|
|
594
|
-
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
656
|
+
def annotate_row!(loc, row_index, value, color: :normal)
|
657
|
+
case loc
|
658
|
+
when :l
|
659
|
+
@labels_left[row_index] = value
|
660
|
+
@colors_left[row_index] = color
|
661
|
+
when :r
|
662
|
+
@labels_right[row_index] = value
|
663
|
+
@colors_right[row_index] = color
|
664
|
+
else
|
665
|
+
raise ArgumentError, "unknown location `#{loc}`, try :l or :r instead"
|
666
|
+
end
|
604
667
|
end
|
605
|
-
end
|
606
668
|
|
607
|
-
|
608
|
-
|
609
|
-
|
669
|
+
def render(out)
|
670
|
+
Renderer.render(out, self)
|
671
|
+
end
|
610
672
|
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
673
|
+
COLOR_CYCLE = [
|
674
|
+
:green,
|
675
|
+
:blue,
|
676
|
+
:red,
|
677
|
+
:magenta,
|
678
|
+
:yellow,
|
679
|
+
:cyan
|
680
|
+
].freeze
|
681
|
+
|
682
|
+
def next_color
|
683
|
+
COLOR_CYCLE[@auto_color]
|
684
|
+
ensure
|
685
|
+
@auto_color = (@auto_color + 1) % COLOR_CYCLE.length
|
686
|
+
end
|
625
687
|
|
626
|
-
|
627
|
-
|
628
|
-
|
629
|
-
|
630
|
-
|
688
|
+
def to_s
|
689
|
+
StringIO.open do |sio|
|
690
|
+
render(sio)
|
691
|
+
sio.close
|
692
|
+
sio.string
|
693
|
+
end
|
631
694
|
end
|
632
|
-
end
|
633
695
|
|
634
|
-
|
635
|
-
|
636
|
-
|
696
|
+
private def check_margin(margin)
|
697
|
+
if margin < 0
|
698
|
+
raise ArgumentError, "margin must be >= 0"
|
699
|
+
end
|
700
|
+
margin
|
637
701
|
end
|
638
|
-
margin
|
639
|
-
end
|
640
702
|
|
641
|
-
|
642
|
-
|
643
|
-
|
703
|
+
private def check_row_index(row_index)
|
704
|
+
unless 0 <= row_index && row_index < n_rows
|
705
|
+
raise ArgumentError, "row_index out of bounds"
|
706
|
+
end
|
644
707
|
end
|
645
708
|
end
|
646
|
-
end
|
647
709
|
|
648
|
-
|
649
|
-
|
650
|
-
|
651
|
-
MIN_WIDTH = 10
|
652
|
-
DEFAULT_COLOR = :green
|
653
|
-
DEFAULT_SYMBOL = "■"
|
654
|
-
|
655
|
-
def initialize(bars, width, color, symbol, transform, **kw)
|
656
|
-
if symbol.length > 1
|
657
|
-
raise ArgumentError, "symbol must be a single character"
|
658
|
-
end
|
659
|
-
@bars = bars
|
660
|
-
@symbol = symbol
|
661
|
-
@max_freq, i = find_max(transform_values(transform, bars))
|
662
|
-
@max_len = bars[i].to_s.length
|
663
|
-
@width = [width, max_len + 7, MIN_WIDTH].max
|
664
|
-
@color = color
|
665
|
-
@symbol = symbol
|
666
|
-
@transform = transform
|
667
|
-
super(**kw)
|
668
|
-
end
|
710
|
+
class Barplot < Plot
|
711
|
+
include ValueTransformer
|
669
712
|
|
670
|
-
|
671
|
-
|
672
|
-
|
713
|
+
MIN_WIDTH = 10
|
714
|
+
DEFAULT_COLOR = :green
|
715
|
+
DEFAULT_SYMBOL = "■"
|
673
716
|
|
674
|
-
|
675
|
-
|
676
|
-
|
717
|
+
def initialize(bars, width, color, symbol, transform, **kw)
|
718
|
+
if symbol.length > 1
|
719
|
+
raise ArgumentError, "symbol must be a single character"
|
720
|
+
end
|
721
|
+
@bars = bars
|
722
|
+
@symbol = symbol
|
723
|
+
@max_freq, i = find_max(transform_values(transform, bars))
|
724
|
+
@max_len = bars[i].to_s.length
|
725
|
+
@width = [width, max_len + 7, MIN_WIDTH].max
|
726
|
+
@color = color
|
727
|
+
@symbol = symbol
|
728
|
+
@transform = transform
|
729
|
+
super(**kw)
|
730
|
+
end
|
677
731
|
|
678
|
-
|
679
|
-
|
680
|
-
|
732
|
+
attr_reader :max_freq
|
733
|
+
attr_reader :max_len
|
734
|
+
attr_reader :width
|
681
735
|
|
682
|
-
|
683
|
-
|
684
|
-
|
685
|
-
@max_len = @bars[i].to_s.length
|
686
|
-
end
|
736
|
+
def n_rows
|
737
|
+
@bars.length
|
738
|
+
end
|
687
739
|
|
688
|
-
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
|
693
|
-
|
694
|
-
|
695
|
-
|
696
|
-
|
697
|
-
|
698
|
-
|
699
|
-
|
700
|
-
|
701
|
-
|
702
|
-
|
703
|
-
|
740
|
+
def n_columns
|
741
|
+
@width
|
742
|
+
end
|
743
|
+
|
744
|
+
def add_row!(bars)
|
745
|
+
@bars.concat(bars)
|
746
|
+
@max_freq, i = find_max(transform_values(@transform, bars))
|
747
|
+
@max_len = @bars[i].to_s.length
|
748
|
+
end
|
749
|
+
|
750
|
+
def print_row(out, row_index)
|
751
|
+
check_row_index(row_index)
|
752
|
+
bar = @bars[row_index]
|
753
|
+
max_bar_width = [width - 2 - max_len, 1].max
|
754
|
+
val = transform_values(@transform, bar)
|
755
|
+
bar_len = max_freq > 0 ?
|
756
|
+
([val, 0].max.fdiv(max_freq) * max_bar_width).round :
|
757
|
+
0
|
758
|
+
bar_str = max_freq > 0 ? @symbol * bar_len : ""
|
759
|
+
bar_lbl = bar.to_s
|
760
|
+
print_styled(out, bar_str, color: @color)
|
761
|
+
print_styled(out, " ", bar_lbl, color: :normal)
|
762
|
+
pan_len = [max_bar_width + 1 + max_len - bar_len - bar_lbl.length, 0].max
|
763
|
+
pad = " " * pan_len.round
|
764
|
+
out.print(pad)
|
765
|
+
end
|
704
766
|
|
705
|
-
|
706
|
-
|
707
|
-
|
708
|
-
|
709
|
-
|
710
|
-
|
767
|
+
private def find_max(values)
|
768
|
+
i = j = 0
|
769
|
+
max = values[i]
|
770
|
+
while j < values.length
|
771
|
+
if values[j] > max
|
772
|
+
i, max = j, values[j]
|
773
|
+
end
|
774
|
+
j += 1
|
711
775
|
end
|
712
|
-
|
776
|
+
[max, i]
|
713
777
|
end
|
714
|
-
[max, i]
|
715
778
|
end
|
716
779
|
end
|
780
|
+
private_constant :MiniUnicodePlot
|
717
781
|
end
|
718
782
|
|