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 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