ascii_charts 0.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (2) hide show
  1. data/lib/ascii_charts.rb +278 -0
  2. metadata +63 -0
@@ -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
+