ascii-charts 0.9.2 → 0.9.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/CHANGELOG.md +20 -0
- data/Gemfile +3 -0
- data/LICENSE +19 -0
- data/README.md +160 -0
- data/Rakefile +7 -0
- data/ascii_charts.gemspec +21 -0
- data/lib/ascii_charts.rb +1 -280
- data/lib/ascii_charts/cartesian.rb +88 -0
- data/lib/ascii_charts/chart.rb +248 -0
- data/lib/ascii_charts/version.rb +3 -0
- data/spec/lib/ascii_charts/cartesian_spec.rb +25 -0
- data/spec/spec_helper.rb +1 -0
- metadata +57 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5e7cee4bd8b2d6d35b1a04a0a0d8bcadeb3be218
|
4
|
+
data.tar.gz: 6e5df74b6d593cf8890153cfcf7c083d48d416b6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6475a1999e18dc3d726324993878011182f5d7f8b24ec62843f494a167b0901044dce98b52ef5e41b9cf9a193055eeb5f623a84d301ea274fd1c3072aa088c15
|
7
|
+
data.tar.gz: a9ccc7f7e54a70bb607c7cbe2e6e42ac4b9c38a393baad4579371d3da994c88ebaa32427cee66a4c67d30bfb1c236a9c8ad2f45a1036f0caf38af495e70c64d8
|
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
Gemfile.lock
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
0.9.3
|
2
|
+
---
|
3
|
+
|
4
|
+
Features
|
5
|
+
|
6
|
+
* Support for multiple data series. Thanks to [@AlexNisnevich](https://github.com/AlexNisnevich)
|
7
|
+
* Markers can now be modified by passing a `markers:` option. Goodbye `*` and welcome `👋 (yes even unicode are working!). Again thanks to [@AlexNisnevich](https://github.com/AlexNisnevich)
|
8
|
+
|
9
|
+
Misc
|
10
|
+
|
11
|
+
* Code reorganisation
|
12
|
+
* Add specs
|
13
|
+
|
14
|
+
0.9.2
|
15
|
+
---
|
16
|
+
|
17
|
+
Fix
|
18
|
+
|
19
|
+
* `min_y_vals` option has been fixed and can be used again
|
20
|
+
* Data is scan within the whole range of R
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2011 Ben Lund
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,160 @@
|
|
1
|
+
# ASCII Charts
|
2
|
+
|
3
|
+
A Ruby library for generating plain text x,y cartesian plots and histograms that can be displayed in a terminal session.
|
4
|
+
|
5
|
+
## Features
|
6
|
+
|
7
|
+
* Very simple API, your data may already in the correct format to be plotted
|
8
|
+
* Dynamically scales the y-axis
|
9
|
+
* Simple configuration options, including chart title
|
10
|
+
|
11
|
+
## Unfeatures
|
12
|
+
|
13
|
+
* Data must be pre-sorted
|
14
|
+
* x axis will not be continuous if your data isn't
|
15
|
+
* Only x,y point graphs and bar histograms supported
|
16
|
+
* Minimal configuration options
|
17
|
+
|
18
|
+
## Install
|
19
|
+
|
20
|
+
$ sudo gem install ascii_charts
|
21
|
+
|
22
|
+
## Summary
|
23
|
+
|
24
|
+
require 'ascii_charts'
|
25
|
+
|
26
|
+
Display a simple graph
|
27
|
+
|
28
|
+
## data must be a pre-sorted array of x,y pairs
|
29
|
+
puts AsciiCharts::Cartesian.new([[0, 1], [1, 3], [2, 7], [3, 15], [4, 4]]).draw
|
30
|
+
|
31
|
+
15| *
|
32
|
+
14|
|
33
|
+
13|
|
34
|
+
12|
|
35
|
+
11|
|
36
|
+
10|
|
37
|
+
9|
|
38
|
+
8|
|
39
|
+
7| *
|
40
|
+
6|
|
41
|
+
5|
|
42
|
+
4| *
|
43
|
+
3| *
|
44
|
+
2|
|
45
|
+
1|*
|
46
|
+
0+----------
|
47
|
+
0 1 2 3 4
|
48
|
+
|
49
|
+
Display the same graph as an histogram
|
50
|
+
|
51
|
+
## as a histogram
|
52
|
+
puts AsciiCharts::Cartesian.new([[0, 1], [1, 3], [2, 7], [3, 15], [4, 4]], :bar => true, :hide_zero => true).draw
|
53
|
+
|
54
|
+
15| *
|
55
|
+
14| *
|
56
|
+
13| *
|
57
|
+
12| *
|
58
|
+
11| *
|
59
|
+
10| *
|
60
|
+
9| *
|
61
|
+
8| *
|
62
|
+
7| * *
|
63
|
+
6| * *
|
64
|
+
5| * *
|
65
|
+
4| * * *
|
66
|
+
3| * * * *
|
67
|
+
2| * * * *
|
68
|
+
1|* * * * *
|
69
|
+
0+----------
|
70
|
+
0 1 2 3 4
|
71
|
+
|
72
|
+
Draw a function (e^x)
|
73
|
+
|
74
|
+
## draw y = e^x for 0 <= x < 10
|
75
|
+
puts AsciiCharts::Cartesian.new((0...10).to_a.map{|x| [x, Math::E ** x]}, :title => 'y = e^x').draw
|
76
|
+
|
77
|
+
y = e^x
|
78
|
+
|
79
|
+
8500.0|
|
80
|
+
8000.0| *
|
81
|
+
7500.0|
|
82
|
+
7000.0|
|
83
|
+
6500.0|
|
84
|
+
6000.0|
|
85
|
+
5500.0|
|
86
|
+
5000.0|
|
87
|
+
4500.0|
|
88
|
+
4000.0|
|
89
|
+
3500.0|
|
90
|
+
3000.0| *
|
91
|
+
2500.0|
|
92
|
+
2000.0|
|
93
|
+
1500.0|
|
94
|
+
1000.0| *
|
95
|
+
500.0| *
|
96
|
+
0.0+*-*-*-*-*-*---------
|
97
|
+
0 1 2 3 4 5 6 7 8 9
|
98
|
+
|
99
|
+
Draw a normal distribution
|
100
|
+
|
101
|
+
## draw a normal distribution with a mean of 10 and a variance of 3 for 0 <= x < 20
|
102
|
+
puts AsciiCharts::Cartesian.new((0...20).to_a.map{|x| [x, (1/Math.sqrt(2*Math::PI*3)) * (Math::E ** -(((x-10)**2)/(2*3)))]}, :title => 'Normal Distribution', :bar => true).draw
|
103
|
+
|
104
|
+
Normal Distribution
|
105
|
+
|
106
|
+
0.24| * * * * *
|
107
|
+
0.22| * * * * *
|
108
|
+
0.2| * * * * *
|
109
|
+
0.18| * * * * *
|
110
|
+
0.16| * * * * *
|
111
|
+
0.14| * * * * *
|
112
|
+
0.13| * * * * *
|
113
|
+
0.12| * * * * *
|
114
|
+
0.1| * * * * *
|
115
|
+
0.08| * * * * * * *
|
116
|
+
0.06| * * * * * * *
|
117
|
+
0.04| * * * * * * * * *
|
118
|
+
0.02| * * * * * * * * *
|
119
|
+
0.0+-*--*--*--*--*--*--*--*--*--*--*--*--*--*--*--*--*--*--*--*-
|
120
|
+
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
121
|
+
|
122
|
+
|
123
|
+
Draw two series with custom markers
|
124
|
+
|
125
|
+
xs1 = (1..10).to_a
|
126
|
+
ys1 = (1..10).to_a
|
127
|
+
ys2 = (1..10).to_a.map { |i| i*(1/2.0) }
|
128
|
+
|
129
|
+
graph = AsciiCharts::Cartesian.new(
|
130
|
+
[xs1, ys1, ys2],
|
131
|
+
markers: ['👋', '👍']
|
132
|
+
)
|
133
|
+
|
134
|
+
10.0| 👋
|
135
|
+
9.5|
|
136
|
+
9.0| 👋
|
137
|
+
8.5|
|
138
|
+
8.0| 👋
|
139
|
+
7.5|
|
140
|
+
7.0| 👋
|
141
|
+
6.5|
|
142
|
+
6.0| 👋
|
143
|
+
5.5|
|
144
|
+
5.0| 👋 👍
|
145
|
+
4.5| 👍
|
146
|
+
4.0| 👋 👍
|
147
|
+
3.5| 👍
|
148
|
+
3.0| 👋 👍
|
149
|
+
2.5| 👍
|
150
|
+
2.0| 👋 👍
|
151
|
+
1.5| 👍
|
152
|
+
1.0| 👋 👍
|
153
|
+
0.5| 👍
|
154
|
+
0.0+------------------------------
|
155
|
+
1 2 3 4 5 6 7 8 9 10
|
156
|
+
|
157
|
+
|
158
|
+
## Changelog
|
159
|
+
|
160
|
+
Please see the [CHANGELOG.md](https://github.com/paulRbr/ascii_charts/blob/master/CHANGELOG.md) file for details
|
data/Rakefile
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
$:.unshift File.expand_path('../lib', __FILE__)
|
2
|
+
require 'ascii_charts/version'
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = 'ascii-charts'
|
6
|
+
s.version = AsciiCharts::VERSION
|
7
|
+
|
8
|
+
s.authors = ['Ben Lund']
|
9
|
+
s.description = 'Library to draw simple ASCII charts (x,y graph plots and histograms)'
|
10
|
+
s.summary = 'Very simple API, your data may already in the correct format to be plotted. Dynamically scales the y-axis. Simple configuration options, including chart title.'
|
11
|
+
s.email = 'ben@benlund.com'
|
12
|
+
s.homepage = 'http://github.com/benlund/ascii_charts'
|
13
|
+
s.licenses = ['MIT']
|
14
|
+
|
15
|
+
s.files = `git ls-files`.split("\n")
|
16
|
+
s.require_paths = ["lib"]
|
17
|
+
|
18
|
+
s.add_development_dependency 'rake'
|
19
|
+
s.add_development_dependency 'rspec'
|
20
|
+
s.add_development_dependency 'pry'
|
21
|
+
end
|
data/lib/ascii_charts.rb
CHANGED
@@ -1,280 +1 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
VERSION = '0.9.2'
|
4
|
-
|
5
|
-
class Chart
|
6
|
-
|
7
|
-
attr_reader :options, :data
|
8
|
-
|
9
|
-
DEFAULT_MAX_Y_VALS = 20
|
10
|
-
DEFAULT_MIN_Y_VALS = 10
|
11
|
-
|
12
|
-
#data is a sorted array of [x, y] pairs
|
13
|
-
|
14
|
-
def initialize(data, options={})
|
15
|
-
@data = data
|
16
|
-
@options = options
|
17
|
-
end
|
18
|
-
|
19
|
-
def rounded_data
|
20
|
-
@rounded_data ||= self.data.map{|pair| [pair[0], self.round_value(pair[1])]}
|
21
|
-
end
|
22
|
-
|
23
|
-
def step_size
|
24
|
-
if !defined? @step_size
|
25
|
-
if self.options[:y_step_size]
|
26
|
-
@step_size = self.options[:y_step_size]
|
27
|
-
else
|
28
|
-
max_y_vals = self.options[:max_y_vals] || DEFAULT_MAX_Y_VALS
|
29
|
-
min_y_vals = self.options[:min_y_vals] || DEFAULT_MIN_Y_VALS
|
30
|
-
y_span = (self.max_yval - self.min_yval).to_f
|
31
|
-
step_size = self.nearest_step( y_span.to_f / (self.data.size + 1) )
|
32
|
-
|
33
|
-
if @all_ints && (step_size < 1)
|
34
|
-
step_size = 1
|
35
|
-
else
|
36
|
-
while (y_span / step_size) < min_y_vals
|
37
|
-
candidate_step_size = self.next_step_down(step_size)
|
38
|
-
if @all_ints && (candidate_step_size < 1) ## don't go below one
|
39
|
-
break
|
40
|
-
end
|
41
|
-
step_size = candidate_step_size
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
|
-
#go up if we undershot, or were never over
|
46
|
-
while (y_span / step_size) > max_y_vals
|
47
|
-
step_size = self.next_step_up(step_size)
|
48
|
-
end
|
49
|
-
@step_size = step_size
|
50
|
-
end
|
51
|
-
if !@all_ints && @step_size.is_a?(Integer)
|
52
|
-
@step_size = @step_size.to_f
|
53
|
-
end
|
54
|
-
end
|
55
|
-
@step_size
|
56
|
-
end
|
57
|
-
|
58
|
-
STEPS = [1, 2, 5]
|
59
|
-
|
60
|
-
def from_step(val)
|
61
|
-
if 0 == val
|
62
|
-
[0, 0]
|
63
|
-
else
|
64
|
-
order = Math.log10(val).floor.to_i
|
65
|
-
num = (val / (10 ** order))
|
66
|
-
[num, order]
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
def to_step(num, order)
|
71
|
-
s = num * (10 ** order)
|
72
|
-
if order < 0
|
73
|
-
s.to_f
|
74
|
-
else
|
75
|
-
s
|
76
|
-
end
|
77
|
-
end
|
78
|
-
|
79
|
-
def nearest_step(val)
|
80
|
-
num, order = self.from_step(val)
|
81
|
-
self.to_step(2, order) ##@@
|
82
|
-
end
|
83
|
-
|
84
|
-
def next_step_up(val)
|
85
|
-
num, order = self.from_step(val)
|
86
|
-
next_index = STEPS.index(num.to_i) + 1
|
87
|
-
if STEPS.size == next_index
|
88
|
-
next_index = 0
|
89
|
-
order += 1
|
90
|
-
end
|
91
|
-
self.to_step(STEPS[next_index], order)
|
92
|
-
end
|
93
|
-
|
94
|
-
def next_step_down(val)
|
95
|
-
num, order = self.from_step(val)
|
96
|
-
next_index = STEPS.index(num.to_i) - 1
|
97
|
-
if -1 == next_index
|
98
|
-
STEPS.size - 1
|
99
|
-
order -= 1
|
100
|
-
end
|
101
|
-
self.to_step(STEPS[next_index], order)
|
102
|
-
end
|
103
|
-
|
104
|
-
#round to nearest step size, making sure we curtail precision to same order of magnitude as the step size to avoid 0.4 + 0.2 = 0.6000000000000001
|
105
|
-
def round_value(val)
|
106
|
-
remainder = val % self.step_size
|
107
|
-
unprecised = if (remainder * 2) >= self.step_size
|
108
|
-
(val - remainder) + self.step_size
|
109
|
-
else
|
110
|
-
val - remainder
|
111
|
-
end
|
112
|
-
if self.step_size < 1
|
113
|
-
precision = -Math.log10(self.step_size).floor
|
114
|
-
(unprecised * (10 ** precision)).to_i.to_f / (10 ** precision)
|
115
|
-
else
|
116
|
-
unprecised
|
117
|
-
end
|
118
|
-
end
|
119
|
-
|
120
|
-
def max_yval
|
121
|
-
if !defined? @max_yval
|
122
|
-
scan_data
|
123
|
-
end
|
124
|
-
@max_yval
|
125
|
-
end
|
126
|
-
|
127
|
-
def min_yval
|
128
|
-
if !defined? @min_yval
|
129
|
-
scan_data
|
130
|
-
end
|
131
|
-
@min_yval
|
132
|
-
end
|
133
|
-
|
134
|
-
def all_ints
|
135
|
-
if !defined? @all_ints
|
136
|
-
scan_data
|
137
|
-
end
|
138
|
-
@all_ints
|
139
|
-
end
|
140
|
-
|
141
|
-
def scan_data
|
142
|
-
@max_yval = -Float::INFINITY
|
143
|
-
@min_yval = Float::INFINITY
|
144
|
-
@all_ints = true
|
145
|
-
|
146
|
-
@max_xval_width = 1
|
147
|
-
|
148
|
-
self.data.each do |pair|
|
149
|
-
if pair[1] > @max_yval
|
150
|
-
@max_yval = pair[1]
|
151
|
-
end
|
152
|
-
if pair[1] < @min_yval
|
153
|
-
@min_yval = pair[1]
|
154
|
-
end
|
155
|
-
if @all_ints && !pair[1].is_a?(Integer)
|
156
|
-
@all_ints = false
|
157
|
-
end
|
158
|
-
|
159
|
-
if (xw = pair[0].to_s.length) > @max_xval_width
|
160
|
-
@max_xval_width = xw
|
161
|
-
end
|
162
|
-
end
|
163
|
-
end
|
164
|
-
|
165
|
-
def max_xval_width
|
166
|
-
if !defined? @max_xval_width
|
167
|
-
scan_data
|
168
|
-
end
|
169
|
-
@max_xval_width
|
170
|
-
end
|
171
|
-
|
172
|
-
def max_yval_width
|
173
|
-
if !defined? @max_yval_width
|
174
|
-
scan_y_range
|
175
|
-
end
|
176
|
-
@max_yval_width
|
177
|
-
end
|
178
|
-
|
179
|
-
def scan_y_range
|
180
|
-
@max_yval_width = 1
|
181
|
-
|
182
|
-
self.y_range.each do |yval|
|
183
|
-
if (yw = yval.to_s.length) > @max_yval_width
|
184
|
-
@max_yval_width = yw
|
185
|
-
end
|
186
|
-
end
|
187
|
-
end
|
188
|
-
|
189
|
-
def y_range
|
190
|
-
if !defined? @y_range
|
191
|
-
@y_range = []
|
192
|
-
first_y = self.round_value(self.min_yval)
|
193
|
-
if first_y > self.min_yval
|
194
|
-
first_y = first_y - self.step_size
|
195
|
-
end
|
196
|
-
last_y = self.round_value(self.max_yval)
|
197
|
-
if last_y < self.max_yval
|
198
|
-
last_y = last_y + self.step_size
|
199
|
-
end
|
200
|
-
current_y = first_y
|
201
|
-
while current_y <= last_y
|
202
|
-
@y_range << current_y
|
203
|
-
current_y = self.round_value(current_y + self.step_size) ## to avoid fp arithmetic oddness
|
204
|
-
end
|
205
|
-
end
|
206
|
-
@y_range
|
207
|
-
end
|
208
|
-
|
209
|
-
def lines
|
210
|
-
raise "lines must be overridden"
|
211
|
-
end
|
212
|
-
|
213
|
-
def draw
|
214
|
-
lines.join("\n")
|
215
|
-
end
|
216
|
-
|
217
|
-
def to_string
|
218
|
-
draw
|
219
|
-
end
|
220
|
-
|
221
|
-
end
|
222
|
-
|
223
|
-
class Cartesian < Chart
|
224
|
-
|
225
|
-
def lines
|
226
|
-
if self.data.size == 0
|
227
|
-
return [[' ', self.options[:title], ' ', '|', '+-', ' ']]
|
228
|
-
end
|
229
|
-
|
230
|
-
lines = [' ']
|
231
|
-
|
232
|
-
bar_width = self.max_xval_width + 1
|
233
|
-
|
234
|
-
lines << (' ' * self.max_yval_width) + ' ' + self.rounded_data.map{|pair| pair[0].to_s.center(bar_width)}.join('')
|
235
|
-
|
236
|
-
self.y_range.each_with_index do |current_y, i|
|
237
|
-
yval = current_y.to_s
|
238
|
-
bar = if 0 == i
|
239
|
-
'+'
|
240
|
-
else
|
241
|
-
'|'
|
242
|
-
end
|
243
|
-
current_line = [(' ' * (self.max_yval_width - yval.length) ) + "#{current_y}#{bar}"]
|
244
|
-
|
245
|
-
self.rounded_data.each do |pair|
|
246
|
-
marker = if (0 == i) && options[:hide_zero]
|
247
|
-
'-'
|
248
|
-
else
|
249
|
-
'*'
|
250
|
-
end
|
251
|
-
filler = if 0 == i
|
252
|
-
'-'
|
253
|
-
else
|
254
|
-
' '
|
255
|
-
end
|
256
|
-
comparison = if self.options[:bar]
|
257
|
-
current_y <= pair[1]
|
258
|
-
else
|
259
|
-
current_y == pair[1]
|
260
|
-
end
|
261
|
-
if comparison
|
262
|
-
current_line << marker.center(bar_width, filler)
|
263
|
-
else
|
264
|
-
current_line << filler * bar_width
|
265
|
-
end
|
266
|
-
end
|
267
|
-
lines << current_line.join('')
|
268
|
-
current_y = current_y + self.step_size
|
269
|
-
end
|
270
|
-
lines << ' '
|
271
|
-
if self.options[:title]
|
272
|
-
lines << self.options[:title].center(lines[1].length)
|
273
|
-
end
|
274
|
-
lines << ' '
|
275
|
-
lines.reverse
|
276
|
-
end
|
277
|
-
|
278
|
-
end
|
279
|
-
|
280
|
-
end
|
1
|
+
require 'ascii_charts/cartesian'
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'ascii_charts/chart'
|
2
|
+
|
3
|
+
module AsciiCharts
|
4
|
+
class Cartesian < Chart
|
5
|
+
|
6
|
+
def lines
|
7
|
+
if self.data.size == 0
|
8
|
+
return [[' ', self.options[:title], ' ', '|', '+-', ' ']]
|
9
|
+
end
|
10
|
+
|
11
|
+
lines = [' ']
|
12
|
+
|
13
|
+
bar_width = self.max_xval_width + 1
|
14
|
+
|
15
|
+
lines << (' ' * self.max_yval_width) + ' ' + self.rounded_data.map{|point| point[0].to_s.center(bar_width)}.join('')
|
16
|
+
|
17
|
+
self.y_range.each_with_index do |current_y, i|
|
18
|
+
yval = current_y.to_s
|
19
|
+
bar = if 0 == i
|
20
|
+
'+'
|
21
|
+
else
|
22
|
+
'|'
|
23
|
+
end
|
24
|
+
current_line = [(' ' * (self.max_yval_width - yval.length) ) + "#{current_y}#{bar}"]
|
25
|
+
|
26
|
+
self.rounded_data.each do |point|
|
27
|
+
def marker(series, i)
|
28
|
+
if (0 == i) && options[:hide_zero]
|
29
|
+
marker = '-'
|
30
|
+
else
|
31
|
+
if (options[:markers])
|
32
|
+
marker = options[:markers][series]
|
33
|
+
|
34
|
+
# unicode characters need to be treated as two-character strings for string.center() to work correctly
|
35
|
+
if marker.length > 1
|
36
|
+
marker += if 0 == i
|
37
|
+
'-'
|
38
|
+
else
|
39
|
+
' '
|
40
|
+
end
|
41
|
+
end
|
42
|
+
else
|
43
|
+
marker = '*'
|
44
|
+
end
|
45
|
+
end
|
46
|
+
marker
|
47
|
+
end
|
48
|
+
|
49
|
+
filler = if 0 == i
|
50
|
+
'-'
|
51
|
+
else
|
52
|
+
' '
|
53
|
+
end
|
54
|
+
|
55
|
+
matching_series = false
|
56
|
+
lowest_point = INFINITY
|
57
|
+
(1..(point.length - 1)).each do |series|
|
58
|
+
if self.options[:bar]
|
59
|
+
if current_y <= point[series] && lowest_point > point[series]
|
60
|
+
matching_series = series
|
61
|
+
lowest_point = point[series]
|
62
|
+
end
|
63
|
+
else
|
64
|
+
if current_y == point[series]
|
65
|
+
matching_series = series
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
if matching_series
|
71
|
+
current_line << marker(matching_series - 1, i).center(bar_width, filler)
|
72
|
+
else
|
73
|
+
current_line << filler * bar_width
|
74
|
+
end
|
75
|
+
end
|
76
|
+
lines << current_line.join('')
|
77
|
+
current_y = current_y + self.step_size
|
78
|
+
end
|
79
|
+
lines << ' '
|
80
|
+
if self.options[:title]
|
81
|
+
lines << self.options[:title].center(lines[1].length)
|
82
|
+
end
|
83
|
+
lines << ' '
|
84
|
+
lines.reverse
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,248 @@
|
|
1
|
+
module AsciiCharts
|
2
|
+
class Chart
|
3
|
+
|
4
|
+
attr_reader :options, :data
|
5
|
+
|
6
|
+
DEFAULT_MAX_Y_VALS = 20
|
7
|
+
DEFAULT_MIN_Y_VALS = 10
|
8
|
+
INFINITY = +1.0/0.0
|
9
|
+
|
10
|
+
#data is a sorted array of [x, y] pairs
|
11
|
+
|
12
|
+
def initialize(data, options={})
|
13
|
+
if (data[0].length == 2)
|
14
|
+
@data = data # treat as array of points
|
15
|
+
else
|
16
|
+
@data = series_to_points(data) # treat as array of series
|
17
|
+
end
|
18
|
+
|
19
|
+
@options = options
|
20
|
+
end
|
21
|
+
|
22
|
+
def series_to_points(arr_of_series)
|
23
|
+
points = []
|
24
|
+
(0..(arr_of_series[0].length - 1)).each do |i|
|
25
|
+
point = []
|
26
|
+
(0..(arr_of_series.length - 1)).each do |series|
|
27
|
+
point.push(arr_of_series[series][i])
|
28
|
+
end
|
29
|
+
points.push(point)
|
30
|
+
end
|
31
|
+
points
|
32
|
+
end
|
33
|
+
|
34
|
+
def rounded_data
|
35
|
+
@rounded_data ||= self.data.map do |point|
|
36
|
+
point.each_with_index.map do |coord, i|
|
37
|
+
if i == 0
|
38
|
+
coord
|
39
|
+
else
|
40
|
+
round_value(coord)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def step_size
|
47
|
+
if !defined? @step_size
|
48
|
+
if self.options[:y_step_size]
|
49
|
+
@step_size = self.options[:y_step_size]
|
50
|
+
else
|
51
|
+
max_y_vals = self.options[:max_y_vals] || DEFAULT_MAX_Y_VALS
|
52
|
+
min_y_vals = self.options[:max_y_vals] || DEFAULT_MIN_Y_VALS
|
53
|
+
y_span = (self.max_yval - self.min_yval).to_f
|
54
|
+
|
55
|
+
step_size = self.nearest_step( y_span.to_f / (self.data.size + 1) )
|
56
|
+
|
57
|
+
if @all_ints && (step_size < 1)
|
58
|
+
step_size = 1
|
59
|
+
else
|
60
|
+
while (y_span / step_size) < min_y_vals
|
61
|
+
candidate_step_size = self.next_step_down(step_size)
|
62
|
+
if @all_ints && (candidate_step_size < 1) ## don't go below one
|
63
|
+
break
|
64
|
+
end
|
65
|
+
step_size = candidate_step_size
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
#go up if we undershot, or were never over
|
70
|
+
while (y_span / step_size) > max_y_vals
|
71
|
+
step_size = self.next_step_up(step_size)
|
72
|
+
end
|
73
|
+
@step_size = step_size
|
74
|
+
end
|
75
|
+
if !@all_ints && @step_size.is_a?(Integer)
|
76
|
+
@step_size = @step_size.to_f
|
77
|
+
end
|
78
|
+
end
|
79
|
+
@step_size
|
80
|
+
end
|
81
|
+
|
82
|
+
STEPS = [1, 2, 5]
|
83
|
+
|
84
|
+
def from_step(val)
|
85
|
+
if 0 == val
|
86
|
+
[0, 0]
|
87
|
+
else
|
88
|
+
order = Math.log10(val).floor.to_i
|
89
|
+
num = (val / (10 ** order))
|
90
|
+
[num, order]
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def to_step(num, order)
|
95
|
+
s = num * (10 ** order)
|
96
|
+
if order < 0
|
97
|
+
s.to_f
|
98
|
+
else
|
99
|
+
s
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def nearest_step(val)
|
104
|
+
num, order = self.from_step(val)
|
105
|
+
self.to_step(2, order) ##@@
|
106
|
+
end
|
107
|
+
|
108
|
+
def next_step_up(val)
|
109
|
+
num, order = self.from_step(val)
|
110
|
+
next_index = STEPS.index(num.to_i) + 1
|
111
|
+
if STEPS.size == next_index
|
112
|
+
next_index = 0
|
113
|
+
order += 1
|
114
|
+
end
|
115
|
+
self.to_step(STEPS[next_index], order)
|
116
|
+
end
|
117
|
+
|
118
|
+
def next_step_down(val)
|
119
|
+
num, order = self.from_step(val)
|
120
|
+
next_index = STEPS.index(num.to_i) - 1
|
121
|
+
if -1 == next_index
|
122
|
+
STEPS.size - 1
|
123
|
+
order -= 1
|
124
|
+
end
|
125
|
+
self.to_step(STEPS[next_index], order)
|
126
|
+
end
|
127
|
+
|
128
|
+
#round to nearest step size, making sure we curtail precision to same order of magnitude as the step size to avoid 0.4 + 0.2 = 0.6000000000000001
|
129
|
+
def round_value(val)
|
130
|
+
remainder = val % self.step_size
|
131
|
+
unprecised = if (remainder * 2) >= self.step_size
|
132
|
+
(val - remainder) + self.step_size
|
133
|
+
else
|
134
|
+
val - remainder
|
135
|
+
end
|
136
|
+
if self.step_size < 1
|
137
|
+
precision = -Math.log10(self.step_size).floor
|
138
|
+
(unprecised * (10 ** precision)).to_i.to_f / (10 ** precision)
|
139
|
+
else
|
140
|
+
unprecised
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def max_yval
|
145
|
+
if !defined? @max_yval
|
146
|
+
scan_data
|
147
|
+
end
|
148
|
+
@max_yval
|
149
|
+
end
|
150
|
+
|
151
|
+
def min_yval
|
152
|
+
if !defined? @min_yval
|
153
|
+
scan_data
|
154
|
+
end
|
155
|
+
@min_yval
|
156
|
+
end
|
157
|
+
|
158
|
+
def all_ints
|
159
|
+
if !defined? @all_ints
|
160
|
+
scan_data
|
161
|
+
end
|
162
|
+
@all_ints
|
163
|
+
end
|
164
|
+
|
165
|
+
def scan_data
|
166
|
+
@max_yval = 0
|
167
|
+
@min_yval = 0
|
168
|
+
@all_ints = true
|
169
|
+
|
170
|
+
@max_xval_width = 1
|
171
|
+
|
172
|
+
self.data.each do |point|
|
173
|
+
if (xw = point[0].to_s.length) > @max_xval_width
|
174
|
+
@max_xval_width = xw
|
175
|
+
end
|
176
|
+
|
177
|
+
point[1..-1].each do |yval|
|
178
|
+
if yval > @max_yval
|
179
|
+
@max_yval = yval
|
180
|
+
end
|
181
|
+
if yval < @min_yval
|
182
|
+
@min_yval = yval
|
183
|
+
end
|
184
|
+
if @all_ints && !yval.is_a?(Integer)
|
185
|
+
@all_ints = false
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def max_xval_width
|
192
|
+
if !defined? @max_xval_width
|
193
|
+
scan_data
|
194
|
+
end
|
195
|
+
@max_xval_width
|
196
|
+
end
|
197
|
+
|
198
|
+
def max_yval_width
|
199
|
+
if !defined? @max_yval_width
|
200
|
+
scan_y_range
|
201
|
+
end
|
202
|
+
@max_yval_width
|
203
|
+
end
|
204
|
+
|
205
|
+
def scan_y_range
|
206
|
+
@max_yval_width = 1
|
207
|
+
|
208
|
+
self.y_range.each do |yval|
|
209
|
+
if (yw = yval.to_s.length) > @max_yval_width
|
210
|
+
@max_yval_width = yw
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
def y_range
|
216
|
+
if !defined? @y_range
|
217
|
+
@y_range = []
|
218
|
+
first_y = self.round_value(self.min_yval)
|
219
|
+
if first_y > self.min_yval
|
220
|
+
first_y = first_y - self.step_size
|
221
|
+
end
|
222
|
+
last_y = self.round_value(self.max_yval)
|
223
|
+
if last_y < self.max_yval
|
224
|
+
last_y = last_y + self.step_size
|
225
|
+
end
|
226
|
+
current_y = first_y
|
227
|
+
while current_y <= last_y
|
228
|
+
@y_range << current_y
|
229
|
+
current_y = self.round_value(current_y + self.step_size) ## to avoid fp arithmetic oddness
|
230
|
+
end
|
231
|
+
end
|
232
|
+
@y_range
|
233
|
+
end
|
234
|
+
|
235
|
+
def lines
|
236
|
+
raise "lines must be overridden"
|
237
|
+
end
|
238
|
+
|
239
|
+
def draw
|
240
|
+
lines.join("\n")
|
241
|
+
end
|
242
|
+
|
243
|
+
def to_string
|
244
|
+
draw
|
245
|
+
end
|
246
|
+
|
247
|
+
end
|
248
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe AsciiCharts::Cartesian do
|
4
|
+
it 'displays a graph of series' do
|
5
|
+
xs1 = (1..10).to_a
|
6
|
+
ys1 = (1..10).to_a
|
7
|
+
ys2 = (1..10).to_a.reverse
|
8
|
+
|
9
|
+
graph = AsciiCharts::Cartesian.new(
|
10
|
+
[
|
11
|
+
xs1,
|
12
|
+
ys1,
|
13
|
+
ys2
|
14
|
+
],
|
15
|
+
markers: ['👋', '👍', '👌']
|
16
|
+
)
|
17
|
+
|
18
|
+
expect(graph.lines.size).to be(15)
|
19
|
+
|
20
|
+
drawing = graph.draw
|
21
|
+
expect(drawing).to include('👋')
|
22
|
+
expect(drawing).to include('👍')
|
23
|
+
expect(drawing).to_not include('👌')
|
24
|
+
end
|
25
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'ascii_charts'
|
metadata
CHANGED
@@ -1,22 +1,76 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ascii-charts
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.9.
|
4
|
+
version: 0.9.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ben Lund
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
12
|
-
dependencies:
|
11
|
+
date: 2017-09-02 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rake
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rspec
|
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'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: pry
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
13
55
|
description: Library to draw simple ASCII charts (x,y graph plots and histograms)
|
14
56
|
email: ben@benlund.com
|
15
57
|
executables: []
|
16
58
|
extensions: []
|
17
59
|
extra_rdoc_files: []
|
18
60
|
files:
|
61
|
+
- ".gitignore"
|
62
|
+
- CHANGELOG.md
|
63
|
+
- Gemfile
|
64
|
+
- LICENSE
|
65
|
+
- README.md
|
66
|
+
- Rakefile
|
67
|
+
- ascii_charts.gemspec
|
19
68
|
- lib/ascii_charts.rb
|
69
|
+
- lib/ascii_charts/cartesian.rb
|
70
|
+
- lib/ascii_charts/chart.rb
|
71
|
+
- lib/ascii_charts/version.rb
|
72
|
+
- spec/lib/ascii_charts/cartesian_spec.rb
|
73
|
+
- spec/spec_helper.rb
|
20
74
|
homepage: http://github.com/benlund/ascii_charts
|
21
75
|
licenses:
|
22
76
|
- MIT
|