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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 37f2687ef985f077a6fd0c418dc272f315011659
4
- data.tar.gz: 4c41ca6725e12c7e54545b7e8bbed86e9117d0aa
3
+ metadata.gz: 5e7cee4bd8b2d6d35b1a04a0a0d8bcadeb3be218
4
+ data.tar.gz: 6e5df74b6d593cf8890153cfcf7c083d48d416b6
5
5
  SHA512:
6
- metadata.gz: 100e0e015ced299ab784f67d95c9e89b49bfd49ff975a72dde4df35e1e9147e8be13547d2d3113408dd2ee00177d40e197ad6bb8fec89cb947dce6960e29eee9
7
- data.tar.gz: 3a04b4a533974a996f864be098a67fa2d7ca57b2526c22b22c9b13d9bb650db962866321202b113ddd51105503ec8c132365f8581d8f887beb111640803facfc
6
+ metadata.gz: 6475a1999e18dc3d726324993878011182f5d7f8b24ec62843f494a167b0901044dce98b52ef5e41b9cf9a193055eeb5f623a84d301ea274fd1c3072aa088c15
7
+ data.tar.gz: a9ccc7f7e54a70bb607c7cbe2e6e42ac4b9c38a393baad4579371d3da994c88ebaa32427cee66a4c67d30bfb1c236a9c8ad2f45a1036f0caf38af495e70c64d8
@@ -0,0 +1 @@
1
+ Gemfile.lock
@@ -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
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
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.
@@ -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
@@ -0,0 +1,7 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new
5
+
6
+ task :default => :spec
7
+ task :test => :spec
@@ -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
@@ -1,280 +1 @@
1
- module AsciiCharts
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,3 @@
1
+ module AsciiCharts
2
+ VERSION = '0.9.3'
3
+ 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
@@ -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.2
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-08-29 00:00:00.000000000 Z
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