mini_histogram 0.1.0 → 0.2.1
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/.github/workflows/check_changelog.yml +10 -0
- data/.gitignore +2 -0
- data/.travis.yml +0 -1
- data/CHANGELOG.md +25 -0
- data/README.md +38 -1
- data/Rakefile +3 -4
- data/lib/mini_histogram.rb +56 -32
- data/lib/mini_histogram/plot.rb +716 -0
- data/lib/mini_histogram/version.rb +1 -1
- data/mini_histogram.gemspec +3 -0
- metadata +22 -6
- data/Gemfile.lock +0 -26
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3dc9a1c55d5d10c4c56a27b12ce42b3fdc5d2853997152e79850c081a0e2d07f
|
4
|
+
data.tar.gz: 5d5ef7440f2148b3154df577e7997195ff48a0be0d8194a0771517d3fd3e33a7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 306b0ca28c902e98ac3edcdbb829e9287615b8f4be470132c48c8bb8b76481f3fd0470b29ef402c4c68f5d65af45c59632e1405e9f29fb355f785b32c7064a04
|
7
|
+
data.tar.gz: 1b088faab263d9efc61d0b61247a24b2a78ced9748960e8bf7b2e70f33c4a19a0334975ef5c5cdda8872e022d2055c1006c001b2ce72819144eea8f99ca464f7
|
@@ -0,0 +1,10 @@
|
|
1
|
+
name: Check Changelog
|
2
|
+
on: [pull_request]
|
3
|
+
jobs:
|
4
|
+
build:
|
5
|
+
runs-on: ubuntu-latest
|
6
|
+
steps:
|
7
|
+
- uses: actions/checkout@v1
|
8
|
+
- name: Check that CHANGELOG is touched
|
9
|
+
run: |
|
10
|
+
cat $GITHUB_EVENT_PATH | jq .pull_request.title | grep -i '\[\(\(changelog skip\)\|\(ci skip\)\)\]' || git diff remotes/origin/${{ github.base_ref }} --name-only | grep CHANGELOG.md
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
data/CHANGELOG.md
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
## HEAD
|
2
|
+
|
3
|
+
## 0.2.1
|
4
|
+
|
5
|
+
- Added missing constant needed for plotting support (https://github.com/zombocom/mini_histogram/pull/4)
|
6
|
+
|
7
|
+
## 0.2.0
|
8
|
+
|
9
|
+
- Experimental plotting support added (https://github.com/zombocom/mini_histogram/pull/3)
|
10
|
+
|
11
|
+
## 0.1.3
|
12
|
+
|
13
|
+
- Handle edge cases (https://github.com/zombocom/mini_histogram/pull/2)
|
14
|
+
|
15
|
+
## 0.1.2
|
16
|
+
|
17
|
+
- Add `edge` as alias to `edges`
|
18
|
+
|
19
|
+
## 0.1.1
|
20
|
+
|
21
|
+
- Fix multi histogram weights, with set_average_edges! method (https://github.com/zombocom/mini_histogram/pull/1)
|
22
|
+
|
23
|
+
## 0.1.0
|
24
|
+
|
25
|
+
- First
|
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# MiniHistogram
|
1
|
+
# MiniHistogram [](https://travis-ci.org/zombocom/mini_histogram)
|
2
2
|
|
3
3
|
What's a histogram and why should you care? First read [Lies, Damned Lies, and Averages: Perc50, Perc95 explained for Programmers](https://schneems.com/2020/03/17/lies-damned-lies-and-averages-perc50-perc95-explained-for-programmers/). This library lets you build histograms in pure Ruby.
|
4
4
|
|
@@ -38,6 +38,43 @@ 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
|
42
|
+
|
43
|
+
You can plot!
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
require 'mini_histogram/plot'
|
47
|
+
array = 50.times.map { rand(11.2..11.6) }
|
48
|
+
histogram = MiniHistogram.new(array)
|
49
|
+
puts histogram.plot
|
50
|
+
```
|
51
|
+
|
52
|
+
Will generate:
|
53
|
+
|
54
|
+
```
|
55
|
+
┌ ┐
|
56
|
+
[11.2 , 11.25) ┤▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 9
|
57
|
+
[11.25, 11.3 ) ┤▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 6
|
58
|
+
[11.3 , 11.35) ┤▇▇▇▇▇▇▇▇▇▇▇▇▇ 4
|
59
|
+
[11.35, 11.4 ) ┤▇▇▇▇▇▇▇▇▇▇▇▇▇ 4
|
60
|
+
[11.4 , 11.45) ┤▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 11
|
61
|
+
[11.45, 11.5 ) ┤▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 5
|
62
|
+
[11.5 , 11.55) ┤▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 7
|
63
|
+
[11.55, 11.6 ) ┤▇▇▇▇▇▇▇▇▇▇▇▇▇ 4
|
64
|
+
└ ┘
|
65
|
+
Frequency
|
66
|
+
```
|
67
|
+
|
68
|
+
Integrated plotting is an experimental currently, use with some caution. If you are on Ruby 2.4+ you can pass an instance of MiniHistogram to [unicode_plot.rb](https://github.com/red-data-tools/unicode_plot.rb):
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
array = 50.times.map { rand(11.2..11.6) }
|
72
|
+
histogram = MiniHistogram.new(array)
|
73
|
+
puts UnicodePlot.histogram(histogram)
|
74
|
+
```
|
75
|
+
|
76
|
+
## Alternatives
|
77
|
+
|
41
78
|
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.
|
42
79
|
|
43
80
|
[MiniHistogram API Docs](https://rubydoc.info/github/zombocom/mini_histogram/master/MiniHistogram)
|
data/Rakefile
CHANGED
@@ -19,8 +19,8 @@ task :bench do
|
|
19
19
|
|
20
20
|
array = 1000.times.map { rand }
|
21
21
|
|
22
|
-
|
23
|
-
my_weights =
|
22
|
+
histogram = MiniHistogram.new(array)
|
23
|
+
my_weights = histogram.weights
|
24
24
|
puts array.histogram.weights == my_weights
|
25
25
|
puts array.histogram.weights.inspect
|
26
26
|
puts my_weights.inspect
|
@@ -29,8 +29,7 @@ task :bench do
|
|
29
29
|
Benchmark.ips do |x|
|
30
30
|
x.report("enumerable stats") { array.histogram }
|
31
31
|
x.report("mini histogram ") {
|
32
|
-
|
33
|
-
MiniHistogram.counts_from_edges(array, edges: edges)
|
32
|
+
MiniHistogram.new(array).weights
|
34
33
|
}
|
35
34
|
x.compare!
|
36
35
|
end
|
data/lib/mini_histogram.rb
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
require "mini_histogram/version"
|
2
|
-
require 'math'
|
3
2
|
|
4
3
|
# A class for building histogram info
|
5
4
|
#
|
@@ -20,13 +19,15 @@ require 'math'
|
|
20
19
|
#
|
21
20
|
class MiniHistogram
|
22
21
|
class Error < StandardError; end
|
23
|
-
attr_reader :array, :left_p
|
22
|
+
attr_reader :array, :left_p, :max
|
24
23
|
|
25
|
-
def initialize(array, left_p:
|
24
|
+
def initialize(array, left_p: true, edges: nil)
|
26
25
|
@array = array
|
27
26
|
@left_p = left_p
|
28
27
|
@edges = edges
|
29
28
|
@weights = nil
|
29
|
+
|
30
|
+
@min, @max = array.minmax
|
30
31
|
end
|
31
32
|
|
32
33
|
def edges_min
|
@@ -37,14 +38,25 @@ class MiniHistogram
|
|
37
38
|
edges.max
|
38
39
|
end
|
39
40
|
|
41
|
+
def histogram(*_)
|
42
|
+
self
|
43
|
+
end
|
44
|
+
|
45
|
+
def closed
|
46
|
+
@left_p ? :left : :right
|
47
|
+
end
|
48
|
+
|
40
49
|
# Sets the edge value to something new,
|
41
50
|
# also clears any previously calculated values
|
42
|
-
def
|
43
|
-
@edges =
|
51
|
+
def update_values(edges:, max: )
|
52
|
+
@edges = edges
|
53
|
+
@max = max
|
44
54
|
@weights = nil # clear memoized value
|
45
55
|
end
|
46
56
|
|
47
57
|
def bin_size
|
58
|
+
return 0 if edges.length <= 1
|
59
|
+
|
48
60
|
edges[1] - edges[0]
|
49
61
|
end
|
50
62
|
|
@@ -77,11 +89,12 @@ class MiniHistogram
|
|
77
89
|
# 4 values between 4.0 and 6.0 and three values between 10.0 and 12.0
|
78
90
|
def weights
|
79
91
|
return @weights if @weights
|
92
|
+
return @weights = [] if array.empty?
|
80
93
|
|
81
94
|
lo = edges.first
|
82
95
|
step = edges[1] - edges[0]
|
83
96
|
|
84
|
-
max_index = ((
|
97
|
+
max_index = ((@max - lo) / step).floor
|
85
98
|
@weights = Array.new(max_index + 1, 0)
|
86
99
|
|
87
100
|
array.each do |x|
|
@@ -109,16 +122,18 @@ class MiniHistogram
|
|
109
122
|
def edges
|
110
123
|
return @edges if @edges
|
111
124
|
|
112
|
-
|
113
|
-
lo = array.min
|
125
|
+
return @edges = [0.0] if array.empty?
|
114
126
|
|
115
|
-
|
127
|
+
lo = @min
|
128
|
+
hi = @max
|
129
|
+
|
130
|
+
nbins = sturges.to_f
|
116
131
|
|
117
132
|
if hi == lo
|
118
|
-
start =
|
133
|
+
start = lo
|
119
134
|
step = 1.0
|
120
135
|
divisor = 1.0
|
121
|
-
len = 1
|
136
|
+
len = 1
|
122
137
|
else
|
123
138
|
bw = (hi - lo) / nbins
|
124
139
|
lbw = Math.log10(bw)
|
@@ -154,31 +169,37 @@ class MiniHistogram
|
|
154
169
|
start = (lo * divisor).floor
|
155
170
|
len = (hi * divisor - start).ceil
|
156
171
|
end
|
172
|
+
end
|
157
173
|
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
end
|
162
|
-
|
163
|
-
while (start + (len - 1)*step)/divisor <= hi
|
164
|
-
len += 1
|
165
|
-
end
|
166
|
-
else
|
167
|
-
while lo <= start/divisor
|
168
|
-
start -= step
|
169
|
-
end
|
170
|
-
while (start + (len - 1)*step)/divisor < hi
|
171
|
-
len += 1
|
172
|
-
end
|
174
|
+
if left_p
|
175
|
+
while (lo < start/divisor)
|
176
|
+
start -= step
|
173
177
|
end
|
174
178
|
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
+
while (start + (len - 1)*step)/divisor <= hi
|
180
|
+
len += 1
|
181
|
+
end
|
182
|
+
else
|
183
|
+
while lo <= start/divisor
|
184
|
+
start -= step
|
185
|
+
end
|
186
|
+
while (start + (len - 1)*step)/divisor < hi
|
187
|
+
len += 1
|
179
188
|
end
|
180
|
-
return @edges
|
181
189
|
end
|
190
|
+
|
191
|
+
@edges = []
|
192
|
+
len.times.each do
|
193
|
+
@edges << start/divisor
|
194
|
+
start += step
|
195
|
+
end
|
196
|
+
|
197
|
+
return @edges
|
198
|
+
end
|
199
|
+
alias :edge :edges
|
200
|
+
|
201
|
+
def plot
|
202
|
+
raise "You must `require 'mini_histogram/plot'` to get this feature"
|
182
203
|
end
|
183
204
|
|
184
205
|
# Given an array of Histograms this function calcualtes
|
@@ -194,6 +215,8 @@ class MiniHistogram
|
|
194
215
|
steps = array_of_histograms.map(&:bin_size)
|
195
216
|
avg_step_size = steps.inject(&:+).to_f / steps.length
|
196
217
|
|
218
|
+
max_value = array_of_histograms.map(&:max).max
|
219
|
+
|
197
220
|
max_edge = array_of_histograms.map(&:edges_max).max
|
198
221
|
min_edge = array_of_histograms.map(&:edges_min).min
|
199
222
|
|
@@ -202,8 +225,9 @@ class MiniHistogram
|
|
202
225
|
average_edges << average_edges.last + avg_step_size
|
203
226
|
end
|
204
227
|
|
205
|
-
array_of_histograms.each {|h| h.
|
228
|
+
array_of_histograms.each {|h| h.update_values(edges: average_edges, max: max_value) }
|
206
229
|
|
207
230
|
return array_of_histograms
|
208
231
|
end
|
209
232
|
end
|
233
|
+
|
@@ -0,0 +1,716 @@
|
|
1
|
+
# Plots the histogram in unicode characters
|
2
|
+
#
|
3
|
+
# Thanks to https://github.com/red-data-tools/unicode_plot.rb
|
4
|
+
# it could not be used because the dependency enumerable-statistics has a hard
|
5
|
+
# lock on a specific version of Ruby and this library needs to support older Rubies
|
6
|
+
#
|
7
|
+
# Example:
|
8
|
+
#
|
9
|
+
# require 'mini_histogram/plot'
|
10
|
+
# array = 50.times.map { rand(11.2..11.6) }
|
11
|
+
# histogram = MiniHistogram.new(array)
|
12
|
+
# puts histogram.plot
|
13
|
+
#
|
14
|
+
class MiniHistogram
|
15
|
+
def plot(
|
16
|
+
nbins: nil,
|
17
|
+
closed: :left,
|
18
|
+
symbol: "▇",
|
19
|
+
**kw)
|
20
|
+
hist = self.histogram(*[nbins].compact, closed: closed)
|
21
|
+
edge, counts = hist.edge, hist.weights
|
22
|
+
labels = []
|
23
|
+
bin_width = edge[1] - edge[0]
|
24
|
+
pad_left, pad_right = 0, 0
|
25
|
+
(0 ... edge.length).each do |i|
|
26
|
+
val1 = float_round_log10(edge[i], bin_width)
|
27
|
+
val2 = float_round_log10(val1 + bin_width, bin_width)
|
28
|
+
a1 = val1.to_s.split('.', 2).map(&:length)
|
29
|
+
a2 = val2.to_s.split('.', 2).map(&:length)
|
30
|
+
pad_left = [pad_left, a1[0], a2[0]].max
|
31
|
+
pad_right = [pad_right, a1[1], a2[1]].max
|
32
|
+
end
|
33
|
+
l_str = hist.closed == :right ? "(" : "["
|
34
|
+
r_str = hist.closed == :right ? "]" : ")"
|
35
|
+
counts.each_with_index do |n, i|
|
36
|
+
val1 = float_round_log10(edge[i], bin_width)
|
37
|
+
val2 = float_round_log10(val1 + bin_width, bin_width)
|
38
|
+
a1 = val1.to_s.split('.', 2).map(&:length)
|
39
|
+
a2 = val2.to_s.split('.', 2).map(&:length)
|
40
|
+
labels[i] = "\e[90m#{l_str}\e[0m" +
|
41
|
+
(" " * (pad_left - a1[0])) +
|
42
|
+
val1.to_s +
|
43
|
+
(" " * (pad_right - a1[1])) +
|
44
|
+
"\e[90m, \e[0m" +
|
45
|
+
(" " * (pad_left - a2[0])) +
|
46
|
+
val2.to_s +
|
47
|
+
(" " * (pad_right - a2[1])) +
|
48
|
+
"\e[90m#{r_str}\e[0m"
|
49
|
+
end
|
50
|
+
xscale = kw.delete(:xscale)
|
51
|
+
xlabel = kw.delete(:xlabel) || ValueTransformer.transform_name(xscale, "Frequency")
|
52
|
+
barplot(labels, counts,
|
53
|
+
symbol: symbol,
|
54
|
+
xscale: xscale,
|
55
|
+
xlabel: xlabel,
|
56
|
+
**kw)
|
57
|
+
end
|
58
|
+
|
59
|
+
private def barplot(
|
60
|
+
*args,
|
61
|
+
width: 40,
|
62
|
+
color: :green,
|
63
|
+
symbol: "■",
|
64
|
+
border: :barplot,
|
65
|
+
xscale: nil,
|
66
|
+
xlabel: nil,
|
67
|
+
data: nil,
|
68
|
+
**kw)
|
69
|
+
case args.length
|
70
|
+
when 0
|
71
|
+
data = Hash(data)
|
72
|
+
keys = data.keys.map(&:to_s)
|
73
|
+
heights = data.values
|
74
|
+
when 2
|
75
|
+
keys = Array(args[0])
|
76
|
+
heights = Array(args[1])
|
77
|
+
else
|
78
|
+
raise ArgumentError, "invalid arguments"
|
79
|
+
end
|
80
|
+
|
81
|
+
unless keys.length == heights.length
|
82
|
+
raise ArgumentError, "The given vectors must be of the same length"
|
83
|
+
end
|
84
|
+
unless heights.min >= 0
|
85
|
+
raise ArgumentError, "All values have to be positive. Negative bars are not supported."
|
86
|
+
end
|
87
|
+
|
88
|
+
xlabel ||= ValueTransformer.transform_name(xscale)
|
89
|
+
plot = Barplot.new(heights, width, color, symbol, xscale,
|
90
|
+
border: border, xlabel: xlabel,
|
91
|
+
**kw)
|
92
|
+
keys.each_with_index do |key, i|
|
93
|
+
plot.annotate_row!(:l, i, key)
|
94
|
+
end
|
95
|
+
|
96
|
+
plot
|
97
|
+
end
|
98
|
+
|
99
|
+
private def float_round_log10(x, m)
|
100
|
+
if x == 0
|
101
|
+
0.0
|
102
|
+
elsif x > 0
|
103
|
+
x.round(ceil_neg_log10(m) + 1).to_f
|
104
|
+
else
|
105
|
+
-(-x).round(ceil_neg_log10(m) + 1).to_f
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
private def ceil_neg_log10(x)
|
110
|
+
if roundable?(-Math.log10(x))
|
111
|
+
(-Math.log10(x)).ceil
|
112
|
+
else
|
113
|
+
(-Math.log10(x)).floor
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
INT64_MIN = -9223372036854775808
|
118
|
+
INT64_MAX = 9223372036854775807
|
119
|
+
private def roundable?(x)
|
120
|
+
x.to_i == x && INT64_MIN <= x && x < INT64_MAX
|
121
|
+
end
|
122
|
+
|
123
|
+
module ValueTransformer
|
124
|
+
PREDEFINED_TRANSFORM_FUNCTIONS = {
|
125
|
+
log: Math.method(:log),
|
126
|
+
ln: Math.method(:log),
|
127
|
+
log10: Math.method(:log10),
|
128
|
+
lg: Math.method(:log10),
|
129
|
+
log2: Math.method(:log2),
|
130
|
+
lb: Math.method(:log2),
|
131
|
+
}.freeze
|
132
|
+
|
133
|
+
def transform_values(func, values)
|
134
|
+
return values unless func
|
135
|
+
|
136
|
+
unless func.respond_to?(:call)
|
137
|
+
func = PREDEFINED_TRANSFORM_FUNCTIONS[func]
|
138
|
+
unless func.respond_to?(:call)
|
139
|
+
raise ArgumentError, "func must be callable"
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
case values
|
144
|
+
when Numeric
|
145
|
+
func.(values)
|
146
|
+
else
|
147
|
+
values.map(&func)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
module_function def transform_name(func, basename="")
|
152
|
+
return basename unless func
|
153
|
+
case func
|
154
|
+
when String, Symbol
|
155
|
+
name = func
|
156
|
+
when ->(f) { f.respond_to?(:name) }
|
157
|
+
name = func.name
|
158
|
+
else
|
159
|
+
name = "custom"
|
160
|
+
end
|
161
|
+
"#{basename} [#{name}]"
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
|
166
|
+
module BorderMaps
|
167
|
+
BORDER_SOLID = {
|
168
|
+
tl: "┌",
|
169
|
+
tr: "┐",
|
170
|
+
bl: "└",
|
171
|
+
br: "┘",
|
172
|
+
t: "─",
|
173
|
+
l: "│",
|
174
|
+
b: "─",
|
175
|
+
r: "│"
|
176
|
+
}.freeze
|
177
|
+
|
178
|
+
BORDER_CORNERS = {
|
179
|
+
tl: "┌",
|
180
|
+
tr: "┐",
|
181
|
+
bl: "└",
|
182
|
+
br: "┘",
|
183
|
+
t: " ",
|
184
|
+
l: " ",
|
185
|
+
b: " ",
|
186
|
+
r: " ",
|
187
|
+
}.freeze
|
188
|
+
|
189
|
+
BORDER_BARPLOT = {
|
190
|
+
tl: "┌",
|
191
|
+
tr: "┐",
|
192
|
+
bl: "└",
|
193
|
+
br: "┘",
|
194
|
+
t: " ",
|
195
|
+
l: "┤",
|
196
|
+
b: " ",
|
197
|
+
r: " ",
|
198
|
+
}.freeze
|
199
|
+
end
|
200
|
+
|
201
|
+
BORDER_MAP = {
|
202
|
+
solid: BorderMaps::BORDER_SOLID,
|
203
|
+
corners: BorderMaps::BORDER_CORNERS,
|
204
|
+
barplot: BorderMaps::BORDER_BARPLOT,
|
205
|
+
}.freeze
|
206
|
+
|
207
|
+
module StyledPrinter
|
208
|
+
TEXT_COLORS = {
|
209
|
+
black: "\033[30m",
|
210
|
+
red: "\033[31m",
|
211
|
+
green: "\033[32m",
|
212
|
+
yellow: "\033[33m",
|
213
|
+
blue: "\033[34m",
|
214
|
+
magenta: "\033[35m",
|
215
|
+
cyan: "\033[36m",
|
216
|
+
white: "\033[37m",
|
217
|
+
gray: "\033[90m",
|
218
|
+
light_black: "\033[90m",
|
219
|
+
light_red: "\033[91m",
|
220
|
+
light_green: "\033[92m",
|
221
|
+
light_yellow: "\033[93m",
|
222
|
+
light_blue: "\033[94m",
|
223
|
+
light_magenta: "\033[95m",
|
224
|
+
light_cyan: "\033[96m",
|
225
|
+
normal: "\033[0m",
|
226
|
+
default: "\033[39m",
|
227
|
+
bold: "\033[1m",
|
228
|
+
underline: "\033[4m",
|
229
|
+
blink: "\033[5m",
|
230
|
+
reverse: "\033[7m",
|
231
|
+
hidden: "\033[8m",
|
232
|
+
nothing: "",
|
233
|
+
}
|
234
|
+
|
235
|
+
0.upto(255) do |i|
|
236
|
+
TEXT_COLORS[i] = "\033[38;5;#{i}m"
|
237
|
+
end
|
238
|
+
|
239
|
+
TEXT_COLORS.freeze
|
240
|
+
|
241
|
+
DISABLE_TEXT_STYLE = {
|
242
|
+
bold: "\033[22m",
|
243
|
+
underline: "\033[24m",
|
244
|
+
blink: "\033[25m",
|
245
|
+
reverse: "\033[27m",
|
246
|
+
hidden: "\033[28m",
|
247
|
+
normal: "",
|
248
|
+
default: "",
|
249
|
+
nothing: "",
|
250
|
+
}.freeze
|
251
|
+
|
252
|
+
COLOR_ENCODE = {
|
253
|
+
normal: 0b000,
|
254
|
+
blue: 0b001,
|
255
|
+
red: 0b010,
|
256
|
+
magenta: 0b011,
|
257
|
+
green: 0b100,
|
258
|
+
cyan: 0b101,
|
259
|
+
yellow: 0b110,
|
260
|
+
white: 0b111
|
261
|
+
}.freeze
|
262
|
+
|
263
|
+
COLOR_DECODE = COLOR_ENCODE.map {|k, v| [v, k] }.to_h.freeze
|
264
|
+
|
265
|
+
def print_styled(out, *args, bold: false, color: :normal)
|
266
|
+
return out.print(*args) unless color?(out)
|
267
|
+
|
268
|
+
str = StringIO.open {|sio| sio.print(*args); sio.close; sio.string }
|
269
|
+
color = :nothing if bold && color == :bold
|
270
|
+
enable_ansi = TEXT_COLORS.fetch(color, TEXT_COLORS[:default]) +
|
271
|
+
(bold ? TEXT_COLORS[:bold] : "")
|
272
|
+
disable_ansi = (bold ? DISABLE_TEXT_STYLE[:bold] : "") +
|
273
|
+
DISABLE_TEXT_STYLE.fetch(color, TEXT_COLORS[:default])
|
274
|
+
first = true
|
275
|
+
StringIO.open do |sio|
|
276
|
+
str.each_line do |line|
|
277
|
+
sio.puts unless first
|
278
|
+
first = false
|
279
|
+
continue if line.empty?
|
280
|
+
sio.print(enable_ansi, line, disable_ansi)
|
281
|
+
end
|
282
|
+
sio.close
|
283
|
+
out.print(sio.string)
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
def print_color(out, color, *args)
|
288
|
+
color = COLOR_DECODE[color]
|
289
|
+
print_styled(out, *args, color: color)
|
290
|
+
end
|
291
|
+
|
292
|
+
def color?(out)
|
293
|
+
(out && out.tty?) || false
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
module BorderPrinter
|
298
|
+
include StyledPrinter
|
299
|
+
|
300
|
+
def print_border_top(out, padding, length, border=:solid, color: :light_black)
|
301
|
+
return if border == :none
|
302
|
+
b = BORDER_MAP[border]
|
303
|
+
print_styled(out, padding, b[:tl], b[:t] * length, b[:tr], color: color)
|
304
|
+
end
|
305
|
+
|
306
|
+
def print_border_bottom(out, padding, length, border=:solid, color: :light_black)
|
307
|
+
return if border == :none
|
308
|
+
b = BORDER_MAP[border]
|
309
|
+
print_styled(out, padding, b[:bl], b[:b] * length, b[:br], color: color)
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
class Renderer
|
314
|
+
include BorderPrinter
|
315
|
+
|
316
|
+
def self.render(out, plot)
|
317
|
+
new(plot).render(out)
|
318
|
+
end
|
319
|
+
|
320
|
+
def initialize(plot)
|
321
|
+
@plot = plot
|
322
|
+
@out = nil
|
323
|
+
end
|
324
|
+
|
325
|
+
attr_reader :plot
|
326
|
+
attr_reader :out
|
327
|
+
|
328
|
+
def render(out)
|
329
|
+
@out = out
|
330
|
+
init_render
|
331
|
+
|
332
|
+
render_top
|
333
|
+
render_rows
|
334
|
+
render_bottom
|
335
|
+
end
|
336
|
+
|
337
|
+
private
|
338
|
+
|
339
|
+
def render_top
|
340
|
+
# plot the title and the top border
|
341
|
+
print_title(@border_padding, plot.title, p_width: @border_length, color: :bold)
|
342
|
+
puts if plot.title_given?
|
343
|
+
|
344
|
+
if plot.show_labels?
|
345
|
+
topleft_str = plot.decorations.fetch(:tl, "")
|
346
|
+
topleft_col = plot.colors_deco.fetch(:tl, :light_black)
|
347
|
+
topmid_str = plot.decorations.fetch(:t, "")
|
348
|
+
topmid_col = plot.colors_deco.fetch(:t, :light_black)
|
349
|
+
topright_str = plot.decorations.fetch(:tr, "")
|
350
|
+
topright_col = plot.colors_deco.fetch(:tr, :light_black)
|
351
|
+
|
352
|
+
if topleft_str != "" || topright_str != "" || topmid_str != ""
|
353
|
+
topleft_len = topleft_str.length
|
354
|
+
topmid_len = topmid_str.length
|
355
|
+
topright_len = topright_str.length
|
356
|
+
print_styled(out, @border_padding, topleft_str, color: topleft_col)
|
357
|
+
cnt = (@border_length / 2.0 - topmid_len / 2.0 - topleft_len).round
|
358
|
+
pad = cnt > 0 ? " " * cnt : ""
|
359
|
+
print_styled(out, pad, topmid_str, color: topmid_col)
|
360
|
+
cnt = @border_length - topright_len - topleft_len - topmid_len + 2 - cnt
|
361
|
+
pad = cnt > 0 ? " " * cnt : ""
|
362
|
+
print_styled(out, pad, topright_str, "\n", color: topright_col)
|
363
|
+
end
|
364
|
+
end
|
365
|
+
|
366
|
+
print_border_top(out, @border_padding, @border_length, plot.border)
|
367
|
+
print(" " * @max_len_r, @plot_padding, "\n")
|
368
|
+
end
|
369
|
+
|
370
|
+
# render all rows
|
371
|
+
def render_rows
|
372
|
+
(0 ... plot.n_rows).each {|row| render_row(row) }
|
373
|
+
end
|
374
|
+
|
375
|
+
def render_row(row)
|
376
|
+
# Current labels to left and right of the row and their length
|
377
|
+
left_str = plot.labels_left.fetch(row, "")
|
378
|
+
left_col = plot.colors_left.fetch(row, :light_black)
|
379
|
+
right_str = plot.labels_right.fetch(row, "")
|
380
|
+
right_col = plot.colors_right.fetch(row, :light_black)
|
381
|
+
left_len = nocolor_string(left_str).length
|
382
|
+
right_len = nocolor_string(right_str).length
|
383
|
+
|
384
|
+
unless color?(out)
|
385
|
+
left_str = nocolor_string(left_str)
|
386
|
+
right_str = nocolor_string(right_str)
|
387
|
+
end
|
388
|
+
|
389
|
+
# print left annotations
|
390
|
+
print(" " * plot.margin)
|
391
|
+
if plot.show_labels?
|
392
|
+
if row == @y_lab_row
|
393
|
+
# print ylabel
|
394
|
+
print_styled(out, plot.ylabel, color: :normal)
|
395
|
+
print(" " * (@max_len_l - plot.ylabel_length - left_len))
|
396
|
+
else
|
397
|
+
# print padding to fill ylabel length
|
398
|
+
print(" " * (@max_len_l - left_len))
|
399
|
+
end
|
400
|
+
# print the left annotation
|
401
|
+
print_styled(out, left_str, color: left_col)
|
402
|
+
end
|
403
|
+
|
404
|
+
# print left border
|
405
|
+
print_styled(out, @plot_padding, @b[:l], color: :light_black)
|
406
|
+
|
407
|
+
# print canvas row
|
408
|
+
plot.print_row(out, row)
|
409
|
+
|
410
|
+
#print right label and padding
|
411
|
+
print_styled(out, @b[:r], color: :light_black)
|
412
|
+
if plot.show_labels?
|
413
|
+
print(@plot_padding)
|
414
|
+
print_styled(out, right_str, color: right_col)
|
415
|
+
print(" " * (@max_len_r - right_len))
|
416
|
+
end
|
417
|
+
puts
|
418
|
+
end
|
419
|
+
|
420
|
+
def render_bottom
|
421
|
+
# draw bottom border and bottom labels
|
422
|
+
print_border_bottom(out, @border_padding, @border_length, plot.border)
|
423
|
+
print(" " * @max_len_r, @plot_padding)
|
424
|
+
if plot.show_labels?
|
425
|
+
botleft_str = plot.decorations.fetch(:bl, "")
|
426
|
+
botleft_col = plot.colors_deco.fetch(:bl, :light_black)
|
427
|
+
botmid_str = plot.decorations.fetch(:b, "")
|
428
|
+
botmid_col = plot.colors_deco.fetch(:b, :light_black)
|
429
|
+
botright_str = plot.decorations.fetch(:br, "")
|
430
|
+
botright_col = plot.colors_deco.fetch(:br, :light_black)
|
431
|
+
|
432
|
+
if botleft_str != "" || botright_str != "" || botmid_str != ""
|
433
|
+
puts
|
434
|
+
botleft_len = botleft_str.length
|
435
|
+
botmid_len = botmid_str.length
|
436
|
+
botright_len = botright_str.length
|
437
|
+
print_styled(out, @border_padding, botleft_str, color: botleft_col)
|
438
|
+
cnt = (@border_length / 2.0 - botmid_len / 2.0 - botleft_len).round
|
439
|
+
pad = cnt > 0 ? " " * cnt : ""
|
440
|
+
print_styled(out, pad, botmid_str, color: botmid_col)
|
441
|
+
cnt = @border_length - botright_len - botleft_len - botmid_len + 2 - cnt
|
442
|
+
pad = cnt > 0 ? " " * cnt : ""
|
443
|
+
print_styled(out, pad, botright_str, color: botright_col)
|
444
|
+
end
|
445
|
+
|
446
|
+
# abuse the print_title function to print the xlabel. maybe refactor this
|
447
|
+
puts if plot.xlabel_given?
|
448
|
+
print_title(@border_padding, plot.xlabel, p_width: @border_length)
|
449
|
+
end
|
450
|
+
end
|
451
|
+
|
452
|
+
def init_render
|
453
|
+
@b = BORDER_MAP[plot.border]
|
454
|
+
@border_length = plot.n_columns
|
455
|
+
|
456
|
+
# get length of largest strings to the left and right
|
457
|
+
@max_len_l = plot.show_labels? && !plot.labels_left.empty? ?
|
458
|
+
plot.labels_left.each_value.map {|l| nocolor_string(l).length }.max :
|
459
|
+
0
|
460
|
+
@max_len_r = plot.show_labels? && !plot.labels_right.empty? ?
|
461
|
+
plot.labels_right.each_value.map {|l| nocolor_string(l).length }.max :
|
462
|
+
0
|
463
|
+
if plot.show_labels? && plot.ylabel_given?
|
464
|
+
@max_len_l += plot.ylabel_length + 1
|
465
|
+
end
|
466
|
+
|
467
|
+
# offset where the plot (incl border) begins
|
468
|
+
@plot_offset = @max_len_l + plot.margin + plot.padding
|
469
|
+
|
470
|
+
# padding-string from left to border
|
471
|
+
@plot_padding = " " * plot.padding
|
472
|
+
|
473
|
+
# padding-string between labels and border
|
474
|
+
@border_padding = " " * @plot_offset
|
475
|
+
|
476
|
+
# compute position of ylabel
|
477
|
+
@y_lab_row = (plot.n_rows / 2.0).round - 1
|
478
|
+
end
|
479
|
+
|
480
|
+
def print_title(padding, title, p_width: 0, color: :normal)
|
481
|
+
return unless title && title != ""
|
482
|
+
offset = (p_width / 2.0 - title.length / 2.0).round
|
483
|
+
offset = [offset, 0].max
|
484
|
+
tpad = " " * offset
|
485
|
+
print_styled(out, padding, tpad, title, color: color)
|
486
|
+
end
|
487
|
+
|
488
|
+
def print(*args)
|
489
|
+
out.print(*args)
|
490
|
+
end
|
491
|
+
|
492
|
+
def puts(*args)
|
493
|
+
out.puts(*args)
|
494
|
+
end
|
495
|
+
|
496
|
+
def nocolor_string(str)
|
497
|
+
str.to_s.gsub(/\e\[[0-9]+m/, "")
|
498
|
+
end
|
499
|
+
end
|
500
|
+
|
501
|
+
class Plot
|
502
|
+
include StyledPrinter
|
503
|
+
|
504
|
+
DEFAULT_WIDTH = 40
|
505
|
+
DEFAULT_BORDER = :solid
|
506
|
+
DEFAULT_MARGIN = 3
|
507
|
+
DEFAULT_PADDING = 1
|
508
|
+
|
509
|
+
def initialize(title: nil,
|
510
|
+
xlabel: nil,
|
511
|
+
ylabel: nil,
|
512
|
+
border: DEFAULT_BORDER,
|
513
|
+
margin: DEFAULT_MARGIN,
|
514
|
+
padding: DEFAULT_PADDING,
|
515
|
+
labels: true)
|
516
|
+
@title = title
|
517
|
+
@xlabel = xlabel
|
518
|
+
@ylabel = ylabel
|
519
|
+
@border = border
|
520
|
+
@margin = check_margin(margin)
|
521
|
+
@padding = padding
|
522
|
+
@labels_left = {}
|
523
|
+
@colors_left = {}
|
524
|
+
@labels_right = {}
|
525
|
+
@colors_right = {}
|
526
|
+
@decorations = {}
|
527
|
+
@colors_deco = {}
|
528
|
+
@show_labels = labels
|
529
|
+
@auto_color = 0
|
530
|
+
end
|
531
|
+
|
532
|
+
attr_reader :title
|
533
|
+
attr_reader :xlabel
|
534
|
+
attr_reader :ylabel
|
535
|
+
attr_reader :border
|
536
|
+
attr_reader :margin
|
537
|
+
attr_reader :padding
|
538
|
+
attr_reader :labels_left
|
539
|
+
attr_reader :colors_left
|
540
|
+
attr_reader :labels_right
|
541
|
+
attr_reader :colors_right
|
542
|
+
attr_reader :decorations
|
543
|
+
attr_reader :colors_deco
|
544
|
+
|
545
|
+
def title_given?
|
546
|
+
title && title != ""
|
547
|
+
end
|
548
|
+
|
549
|
+
def xlabel_given?
|
550
|
+
xlabel && xlabel != ""
|
551
|
+
end
|
552
|
+
|
553
|
+
def ylabel_given?
|
554
|
+
ylabel && ylabel != ""
|
555
|
+
end
|
556
|
+
|
557
|
+
def ylabel_length
|
558
|
+
(ylabel && ylabel.length) || 0
|
559
|
+
end
|
560
|
+
|
561
|
+
def show_labels?
|
562
|
+
@show_labels
|
563
|
+
end
|
564
|
+
|
565
|
+
def annotate!(loc, value, color: :normal)
|
566
|
+
case loc
|
567
|
+
when :l
|
568
|
+
(0 ... n_rows).each do |row|
|
569
|
+
if @labels_left.fetch(row, "") == ""
|
570
|
+
@labels_left[row] = value
|
571
|
+
@colors_left[row] = color
|
572
|
+
break
|
573
|
+
end
|
574
|
+
end
|
575
|
+
when :r
|
576
|
+
(0 ... n_rows).each do |row|
|
577
|
+
if @labels_right.fetch(row, "") == ""
|
578
|
+
@labels_right[row] = value
|
579
|
+
@colors_right[row] = color
|
580
|
+
break
|
581
|
+
end
|
582
|
+
end
|
583
|
+
when :t, :b, :tl, :tr, :bl, :br
|
584
|
+
@decorations[loc] = value
|
585
|
+
@colors_deco[loc] = color
|
586
|
+
else
|
587
|
+
raise ArgumentError,
|
588
|
+
"unknown location to annotate (#{loc.inspect} for :t, :b, :l, :r, :tl, :tr, :bl, or :br)"
|
589
|
+
end
|
590
|
+
end
|
591
|
+
|
592
|
+
def annotate_row!(loc, row_index, value, color: :normal)
|
593
|
+
case loc
|
594
|
+
when :l
|
595
|
+
@labels_left[row_index] = value
|
596
|
+
@colors_left[row_index] = color
|
597
|
+
when :r
|
598
|
+
@labels_right[row_index] = value
|
599
|
+
@colors_right[row_index] = color
|
600
|
+
else
|
601
|
+
raise ArgumentError, "unknown location `#{loc}`, try :l or :r instead"
|
602
|
+
end
|
603
|
+
end
|
604
|
+
|
605
|
+
def render(out)
|
606
|
+
Renderer.render(out, self)
|
607
|
+
end
|
608
|
+
|
609
|
+
COLOR_CYCLE = [
|
610
|
+
:green,
|
611
|
+
:blue,
|
612
|
+
:red,
|
613
|
+
:magenta,
|
614
|
+
:yellow,
|
615
|
+
:cyan
|
616
|
+
].freeze
|
617
|
+
|
618
|
+
def next_color
|
619
|
+
COLOR_CYCLE[@auto_color]
|
620
|
+
ensure
|
621
|
+
@auto_color = (@auto_color + 1) % COLOR_CYCLE.length
|
622
|
+
end
|
623
|
+
|
624
|
+
def to_s
|
625
|
+
StringIO.open do |sio|
|
626
|
+
render(sio)
|
627
|
+
sio.close
|
628
|
+
sio.string
|
629
|
+
end
|
630
|
+
end
|
631
|
+
|
632
|
+
private def check_margin(margin)
|
633
|
+
if margin < 0
|
634
|
+
raise ArgumentError, "margin must be >= 0"
|
635
|
+
end
|
636
|
+
margin
|
637
|
+
end
|
638
|
+
|
639
|
+
private def check_row_index(row_index)
|
640
|
+
unless 0 <= row_index && row_index < n_rows
|
641
|
+
raise ArgumentError, "row_index out of bounds"
|
642
|
+
end
|
643
|
+
end
|
644
|
+
end
|
645
|
+
|
646
|
+
class Barplot < Plot
|
647
|
+
include ValueTransformer
|
648
|
+
|
649
|
+
MIN_WIDTH = 10
|
650
|
+
DEFAULT_COLOR = :green
|
651
|
+
DEFAULT_SYMBOL = "■"
|
652
|
+
|
653
|
+
def initialize(bars, width, color, symbol, transform, **kw)
|
654
|
+
if symbol.length > 1
|
655
|
+
raise ArgumentError, "symbol must be a single character"
|
656
|
+
end
|
657
|
+
@bars = bars
|
658
|
+
@symbol = symbol
|
659
|
+
@max_freq, i = find_max(transform_values(transform, bars))
|
660
|
+
@max_len = bars[i].to_s.length
|
661
|
+
@width = [width, max_len + 7, MIN_WIDTH].max
|
662
|
+
@color = color
|
663
|
+
@symbol = symbol
|
664
|
+
@transform = transform
|
665
|
+
super(**kw)
|
666
|
+
end
|
667
|
+
|
668
|
+
attr_reader :max_freq
|
669
|
+
attr_reader :max_len
|
670
|
+
attr_reader :width
|
671
|
+
|
672
|
+
def n_rows
|
673
|
+
@bars.length
|
674
|
+
end
|
675
|
+
|
676
|
+
def n_columns
|
677
|
+
@width
|
678
|
+
end
|
679
|
+
|
680
|
+
def add_row!(bars)
|
681
|
+
@bars.concat(bars)
|
682
|
+
@max_freq, i = find_max(transform_values(@transform, bars))
|
683
|
+
@max_len = @bars[i].to_s.length
|
684
|
+
end
|
685
|
+
|
686
|
+
def print_row(out, row_index)
|
687
|
+
check_row_index(row_index)
|
688
|
+
bar = @bars[row_index]
|
689
|
+
max_bar_width = [width - 2 - max_len, 1].max
|
690
|
+
val = transform_values(@transform, bar)
|
691
|
+
bar_len = max_freq > 0 ?
|
692
|
+
([val, 0].max.fdiv(max_freq) * max_bar_width).round :
|
693
|
+
0
|
694
|
+
bar_str = max_freq > 0 ? @symbol * bar_len : ""
|
695
|
+
bar_lbl = bar.to_s
|
696
|
+
print_styled(out, bar_str, color: @color)
|
697
|
+
print_styled(out, " ", bar_lbl, color: :normal)
|
698
|
+
pan_len = [max_bar_width + 1 + max_len - bar_len - bar_lbl.length, 0].max
|
699
|
+
pad = " " * pan_len.round
|
700
|
+
out.print(pad)
|
701
|
+
end
|
702
|
+
|
703
|
+
private def find_max(values)
|
704
|
+
i = j = 0
|
705
|
+
max = values[i]
|
706
|
+
while j < values.length
|
707
|
+
if values[j] > max
|
708
|
+
i, max = j, values[j]
|
709
|
+
end
|
710
|
+
j += 1
|
711
|
+
end
|
712
|
+
[max, i]
|
713
|
+
end
|
714
|
+
end
|
715
|
+
end
|
716
|
+
|
data/mini_histogram.gemspec
CHANGED
@@ -26,4 +26,7 @@ Gem::Specification.new do |spec|
|
|
26
26
|
spec.require_paths = ["lib"]
|
27
27
|
|
28
28
|
spec.add_development_dependency "m"
|
29
|
+
# Used for comparison testing, but only supports Ruby 2.4+
|
30
|
+
# spec.add_development_dependency "enumerable-statistics"
|
31
|
+
spec.add_development_dependency "benchmark-ips"
|
29
32
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mini_histogram
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1
|
4
|
+
version: 0.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- schneems
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-09-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: m
|
@@ -24,6 +24,20 @@ dependencies:
|
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: benchmark-ips
|
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'
|
27
41
|
description: It makes histograms out of Ruby data. How cool is that!? Pretty cool
|
28
42
|
if you ask me.
|
29
43
|
email:
|
@@ -32,17 +46,19 @@ executables: []
|
|
32
46
|
extensions: []
|
33
47
|
extra_rdoc_files: []
|
34
48
|
files:
|
49
|
+
- ".github/workflows/check_changelog.yml"
|
35
50
|
- ".gitignore"
|
36
51
|
- ".travis.yml"
|
52
|
+
- CHANGELOG.md
|
37
53
|
- CODE_OF_CONDUCT.md
|
38
54
|
- Gemfile
|
39
|
-
- Gemfile.lock
|
40
55
|
- LICENSE.txt
|
41
56
|
- README.md
|
42
57
|
- Rakefile
|
43
58
|
- bin/console
|
44
59
|
- bin/setup
|
45
60
|
- lib/mini_histogram.rb
|
61
|
+
- lib/mini_histogram/plot.rb
|
46
62
|
- lib/mini_histogram/version.rb
|
47
63
|
- mini_histogram.gemspec
|
48
64
|
homepage: https://github.com/zombocom/mini_histogram
|
@@ -50,7 +66,7 @@ licenses:
|
|
50
66
|
- MIT
|
51
67
|
metadata:
|
52
68
|
homepage_uri: https://github.com/zombocom/mini_histogram
|
53
|
-
post_install_message:
|
69
|
+
post_install_message:
|
54
70
|
rdoc_options: []
|
55
71
|
require_paths:
|
56
72
|
- lib
|
@@ -66,7 +82,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
66
82
|
version: '0'
|
67
83
|
requirements: []
|
68
84
|
rubygems_version: 3.1.2
|
69
|
-
signing_key:
|
85
|
+
signing_key:
|
70
86
|
specification_version: 4
|
71
87
|
summary: A small gem for building histograms out of Ruby arrays
|
72
88
|
test_files: []
|
data/Gemfile.lock
DELETED
@@ -1,26 +0,0 @@
|
|
1
|
-
PATH
|
2
|
-
remote: .
|
3
|
-
specs:
|
4
|
-
mini_histogram (0.1.0)
|
5
|
-
|
6
|
-
GEM
|
7
|
-
remote: https://rubygems.org/
|
8
|
-
specs:
|
9
|
-
m (1.5.1)
|
10
|
-
method_source (>= 0.6.7)
|
11
|
-
rake (>= 0.9.2.2)
|
12
|
-
method_source (0.9.2)
|
13
|
-
minitest (5.14.0)
|
14
|
-
rake (12.3.3)
|
15
|
-
|
16
|
-
PLATFORMS
|
17
|
-
ruby
|
18
|
-
|
19
|
-
DEPENDENCIES
|
20
|
-
m
|
21
|
-
mini_histogram!
|
22
|
-
minitest (~> 5.0)
|
23
|
-
rake (~> 12.0)
|
24
|
-
|
25
|
-
BUNDLED WITH
|
26
|
-
2.1.2
|