ascii-charts 0.9.2 → 0.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/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
|