ascii_charts 0.9
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.
- data/lib/ascii_charts.rb +278 -0
- metadata +63 -0
data/lib/ascii_charts.rb
ADDED
@@ -0,0 +1,278 @@
|
|
1
|
+
module AsciiCharts
|
2
|
+
|
3
|
+
VERSION = '0.9'
|
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
|
+
|
20
|
+
def rounded_data
|
21
|
+
@rounded_data ||= self.data.map{|pair| [pair[0], self.round_value(pair[1])]}
|
22
|
+
end
|
23
|
+
|
24
|
+
def step_size
|
25
|
+
if !defined? @step_size
|
26
|
+
if self.options[:y_step_size]
|
27
|
+
@step_size = self.options[:y_step_size]
|
28
|
+
else
|
29
|
+
max_y_vals = self.options[:max_y_vals] || DEFAULT_MAX_Y_VALS
|
30
|
+
min_y_vals = self.options[:max_y_vals] || DEFAULT_MIN_Y_VALS
|
31
|
+
y_span = (self.max_yval - self.min_yval).to_f
|
32
|
+
|
33
|
+
step_size = self.nearest_step( y_span.to_f / (self.data.size + 1) )
|
34
|
+
|
35
|
+
if @all_ints && (step_size < 1)
|
36
|
+
step_size = 1
|
37
|
+
else
|
38
|
+
while (y_span / step_size) < min_y_vals
|
39
|
+
candidate_step_size = self.next_step_down(step_size)
|
40
|
+
if @all_ints && (candidate_step_size < 1) ## don't go below one
|
41
|
+
break
|
42
|
+
end
|
43
|
+
step_size = candidate_step_size
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
#go up if we undershot, or were never over
|
48
|
+
while (y_span / step_size) > max_y_vals
|
49
|
+
step_size = self.next_step_up(step_size)
|
50
|
+
end
|
51
|
+
@step_size = step_size
|
52
|
+
end
|
53
|
+
if !@all_ints && @step_size.is_a?(Integer)
|
54
|
+
@step_size = @step_size.to_f
|
55
|
+
end
|
56
|
+
end
|
57
|
+
@step_size
|
58
|
+
end
|
59
|
+
|
60
|
+
STEPS = [1, 2, 5]
|
61
|
+
|
62
|
+
def from_step(val)
|
63
|
+
order = Math.log10(val).floor.to_i
|
64
|
+
num = (val / (10 ** order))
|
65
|
+
[num, order]
|
66
|
+
end
|
67
|
+
|
68
|
+
def to_step(num, order)
|
69
|
+
s = num * (10 ** order)
|
70
|
+
if order < 0
|
71
|
+
s.to_f
|
72
|
+
else
|
73
|
+
s
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def nearest_step(val)
|
78
|
+
num, order = self.from_step(val)
|
79
|
+
self.to_step(2, order) ##@@
|
80
|
+
end
|
81
|
+
|
82
|
+
def next_step_up(val)
|
83
|
+
num, order = self.from_step(val)
|
84
|
+
next_index = STEPS.index(num.to_i) + 1
|
85
|
+
if STEPS.size == next_index
|
86
|
+
next_index = 0
|
87
|
+
order += 1
|
88
|
+
end
|
89
|
+
self.to_step(STEPS[next_index], order)
|
90
|
+
end
|
91
|
+
|
92
|
+
def next_step_down(val)
|
93
|
+
num, order = self.from_step(val)
|
94
|
+
next_index = STEPS.index(num.to_i) - 1
|
95
|
+
if -1 == next_index
|
96
|
+
STEPS.size - 1
|
97
|
+
order -= 1
|
98
|
+
end
|
99
|
+
self.to_step(STEPS[next_index], order)
|
100
|
+
end
|
101
|
+
|
102
|
+
#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
|
103
|
+
def round_value(val)
|
104
|
+
remainder = val % self.step_size
|
105
|
+
unprecised = if (remainder * 2) >= self.step_size
|
106
|
+
(val - remainder) + self.step_size
|
107
|
+
else
|
108
|
+
val - remainder
|
109
|
+
end
|
110
|
+
if self.step_size < 1
|
111
|
+
precision = -Math.log10(self.step_size).floor
|
112
|
+
(unprecised * (10 ** precision)).to_i.to_f / (10 ** precision)
|
113
|
+
else
|
114
|
+
unprecised
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def max_yval
|
119
|
+
if !defined? @max_yval
|
120
|
+
scan_data
|
121
|
+
end
|
122
|
+
@max_yval
|
123
|
+
end
|
124
|
+
|
125
|
+
def min_yval
|
126
|
+
if !defined? @min_yval
|
127
|
+
scan_data
|
128
|
+
end
|
129
|
+
@min_yval
|
130
|
+
end
|
131
|
+
|
132
|
+
def all_ints
|
133
|
+
if !defined? @all_ints
|
134
|
+
scan_data
|
135
|
+
end
|
136
|
+
@all_ints
|
137
|
+
end
|
138
|
+
|
139
|
+
def scan_data
|
140
|
+
@max_yval = 0
|
141
|
+
@min_yval = 0
|
142
|
+
@all_ints = true
|
143
|
+
|
144
|
+
@max_xval_width = 1
|
145
|
+
|
146
|
+
self.data.each do |pair|
|
147
|
+
if pair[1] > @max_yval
|
148
|
+
@max_yval = pair[1]
|
149
|
+
end
|
150
|
+
if pair[1] < @min_yval
|
151
|
+
@min_yval = pair[1]
|
152
|
+
end
|
153
|
+
if @all_ints && !pair[1].is_a?(Integer)
|
154
|
+
@all_ints = false
|
155
|
+
end
|
156
|
+
|
157
|
+
if (xw = pair[0].to_s.length) > @max_xval_width
|
158
|
+
@max_xval_width = xw
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def max_xval_width
|
164
|
+
if !defined? @max_xval_width
|
165
|
+
scan_data
|
166
|
+
end
|
167
|
+
@max_xval_width
|
168
|
+
end
|
169
|
+
|
170
|
+
def max_yval_width
|
171
|
+
if !defined? @max_yval_width
|
172
|
+
scan_y_range
|
173
|
+
end
|
174
|
+
@max_yval_width
|
175
|
+
end
|
176
|
+
|
177
|
+
def scan_y_range
|
178
|
+
@max_yval_width = 1
|
179
|
+
|
180
|
+
self.y_range.each do |yval|
|
181
|
+
if (yw = yval.to_s.length) > @max_yval_width
|
182
|
+
@max_yval_width = yw
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
def y_range
|
188
|
+
if !defined? @y_range
|
189
|
+
@y_range = []
|
190
|
+
first_y = self.round_value(self.min_yval)
|
191
|
+
if first_y > self.min_yval
|
192
|
+
first_y = first_y - self.step_size
|
193
|
+
end
|
194
|
+
last_y = self.round_value(self.max_yval)
|
195
|
+
if last_y < self.max_yval
|
196
|
+
last_y = last_y + self.step_size
|
197
|
+
end
|
198
|
+
current_y = first_y
|
199
|
+
while current_y <= last_y
|
200
|
+
@y_range << current_y
|
201
|
+
current_y = self.round_value(current_y + self.step_size) ## to avoid fp arithmetic oddness
|
202
|
+
end
|
203
|
+
end
|
204
|
+
@y_range
|
205
|
+
end
|
206
|
+
|
207
|
+
def lines
|
208
|
+
raise "lines must be overridden"
|
209
|
+
end
|
210
|
+
|
211
|
+
def draw
|
212
|
+
lines.join("\n")
|
213
|
+
end
|
214
|
+
|
215
|
+
def to_string
|
216
|
+
draw
|
217
|
+
end
|
218
|
+
|
219
|
+
end
|
220
|
+
|
221
|
+
class Cartesian < Chart
|
222
|
+
|
223
|
+
def lines
|
224
|
+
if self.data.size == 0
|
225
|
+
return [[' ', self.options[:title], ' ', '|', '+-', ' ']]
|
226
|
+
end
|
227
|
+
|
228
|
+
lines = [' ']
|
229
|
+
|
230
|
+
bar_width = self.max_xval_width + 1
|
231
|
+
|
232
|
+
lines << (' ' * self.max_yval_width) + ' ' + self.rounded_data.map{|pair| pair[0].to_s.center(bar_width)}.join('')
|
233
|
+
|
234
|
+
self.y_range.each_with_index do |current_y, i|
|
235
|
+
yval = current_y.to_s
|
236
|
+
bar = if 0 == i
|
237
|
+
'+'
|
238
|
+
else
|
239
|
+
'|'
|
240
|
+
end
|
241
|
+
current_line = [(' ' * (self.max_yval_width - yval.length) ) + "#{current_y}#{bar}"]
|
242
|
+
|
243
|
+
self.rounded_data.each do |pair|
|
244
|
+
marker = if (0 == i) && options[:hide_zero]
|
245
|
+
'-'
|
246
|
+
else
|
247
|
+
'*'
|
248
|
+
end
|
249
|
+
filler = if 0 == i
|
250
|
+
'-'
|
251
|
+
else
|
252
|
+
' '
|
253
|
+
end
|
254
|
+
comparison = if self.options[:bar]
|
255
|
+
current_y <= pair[1]
|
256
|
+
else
|
257
|
+
current_y == pair[1]
|
258
|
+
end
|
259
|
+
if comparison
|
260
|
+
current_line << marker.center(bar_width, filler)
|
261
|
+
else
|
262
|
+
current_line << filler * bar_width
|
263
|
+
end
|
264
|
+
end
|
265
|
+
lines << current_line.join('')
|
266
|
+
current_y = current_y + self.step_size
|
267
|
+
end
|
268
|
+
lines << ' '
|
269
|
+
if self.options[:title]
|
270
|
+
lines << self.options[:title].center(lines[1].length)
|
271
|
+
end
|
272
|
+
lines << ' '
|
273
|
+
lines.reverse
|
274
|
+
end
|
275
|
+
|
276
|
+
end
|
277
|
+
|
278
|
+
end
|
metadata
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ascii_charts
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 9
|
8
|
+
version: "0.9"
|
9
|
+
platform: ruby
|
10
|
+
authors:
|
11
|
+
- Ben Lund
|
12
|
+
autorequire:
|
13
|
+
bindir: bin
|
14
|
+
cert_chain: []
|
15
|
+
|
16
|
+
date: 2011-05-20 00:00:00 +01:00
|
17
|
+
default_executable:
|
18
|
+
dependencies: []
|
19
|
+
|
20
|
+
description: Library to draw simple ASCII charts (x,y graph plots and histograms)
|
21
|
+
email: ben@benlund.com
|
22
|
+
executables: []
|
23
|
+
|
24
|
+
extensions: []
|
25
|
+
|
26
|
+
extra_rdoc_files: []
|
27
|
+
|
28
|
+
files:
|
29
|
+
- lib/ascii_charts.rb
|
30
|
+
has_rdoc: true
|
31
|
+
homepage: http://github.com/benlund/ascii_charts
|
32
|
+
licenses: []
|
33
|
+
|
34
|
+
post_install_message:
|
35
|
+
rdoc_options: []
|
36
|
+
|
37
|
+
require_paths:
|
38
|
+
- lib
|
39
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ">="
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
segments:
|
45
|
+
- 0
|
46
|
+
version: "0"
|
47
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
48
|
+
none: false
|
49
|
+
requirements:
|
50
|
+
- - ">="
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
segments:
|
53
|
+
- 0
|
54
|
+
version: "0"
|
55
|
+
requirements: []
|
56
|
+
|
57
|
+
rubyforge_project:
|
58
|
+
rubygems_version: 1.3.7
|
59
|
+
signing_key:
|
60
|
+
specification_version: 3
|
61
|
+
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.
|
62
|
+
test_files: []
|
63
|
+
|