unicode_plot 0.0.4 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -40,6 +40,10 @@ module UnicodePlot
40
40
  barplot: BorderMaps::BORDER_BARPLOT,
41
41
  }.freeze
42
42
 
43
+ def self.border_types
44
+ BORDER_MAP.keys
45
+ end
46
+
43
47
  module BorderPrinter
44
48
  include StyledPrinter
45
49
 
@@ -128,7 +132,7 @@ module UnicodePlot
128
132
  left_len = nocolor_string(left_str).length
129
133
  right_len = nocolor_string(right_str).length
130
134
 
131
- unless color?(out)
135
+ unless out.color?
132
136
  left_str = nocolor_string(left_str)
133
137
  right_str = nocolor_string(right_str)
134
138
  end
@@ -0,0 +1,88 @@
1
+ # coding: utf-8
2
+ module UnicodePlot
3
+ # @overload stairs(x, y, style: :post, name: "", title: "", xlabel: "", ylabel: "", labels: true, border: :solid, margin: 3, padding: 1, color: :auto, width: 40, height: 15, xlim: [0, 0], ylim: [0, 0], canvas: :braille, grid: true)
4
+ #
5
+ # Draws a staircase plot on a new canvas.
6
+ #
7
+ # The first vector `x` should contain the horizontal
8
+ # positions for all the points. The second vector `y` should then
9
+ # contain the corresponding vertical positions respectively. This
10
+ # means that the two vectors must be of the same length and
11
+ # ordering.
12
+ #
13
+ # @param x [Array<Numeric>] The horizontal position for each point.
14
+ # @param y [Array<Numeric>] The vertical position for each point.
15
+ # @param style [Symbol] Specifies where the transition of the stair takes place. Can be either `:pre` or `:post`.
16
+ # @param name [String] Annotation of the current drawing to be displayed on the right.
17
+ # @param height [Integer] Number of character rows that should be used for plotting.
18
+ # @param xlim [Array<Numeric>] Plotting range for the x axis. `[0, 0]` stands for automatic.
19
+ # @param ylim [Array<Numeric>] Plotting range for the y axis. `[0, 0]` stands for automatic.
20
+ # @param canvas [Symbol] The type of canvas that should be used for drawing.
21
+ # @param grid [Boolean] If `true`, draws grid-lines at the origin.
22
+ #
23
+ # @return [Plot] A plot object.
24
+ #
25
+ # @example Example usage of stairs on IRB:
26
+ #
27
+ # >> UnicodePlot.stairs([1, 2, 4, 7, 8], [1, 3, 4, 2, 7], style: :post, title: "My Staircase Plot").render
28
+ # My Staircase Plot
29
+ # ┌────────────────────────────────────────┐
30
+ # 7 │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸│
31
+ # │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸│
32
+ # │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸│
33
+ # │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸│
34
+ # │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸│
35
+ # │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸│
36
+ # │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸│
37
+ # │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⡄⠀⠀⠀⠀⢸│
38
+ # │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⢸│
39
+ # │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⢸│
40
+ # │⠀⠀⠀⠀⠀⢸⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⢸│
41
+ # │⠀⠀⠀⠀⠀⢸⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⢸│
42
+ # │⠀⠀⠀⠀⠀⢸⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠧⠤⠤⠤⠤⠼│
43
+ # │⠀⠀⠀⠀⠀⢸⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│
44
+ # 1 │⣀⣀⣀⣀⣀⣸⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│
45
+ # └────────────────────────────────────────┘
46
+ # 1 8
47
+ # => nil
48
+ #
49
+ # @see Plot
50
+ # @see scatterplot
51
+ # @see lineplot
52
+ module_function def stairs(xvec, yvec, style: :post, **kw)
53
+ x_vex, y_vex = compute_stair_lines(xvec, yvec, style: style)
54
+ lineplot(x_vex, y_vex, **kw)
55
+ end
56
+
57
+ # Similar to stairs, but takes an existing plot object as a first argument.
58
+ module_function def stairs!(plot, xvec, yvec, style: :post, **kw)
59
+ x_vex, y_vex = compute_stair_lines(xvec, yvec, style: style)
60
+ lineplot!(plot, x_vex, y_vex, **kw)
61
+ end
62
+
63
+ module_function def compute_stair_lines(x, y, style: :post)
64
+ x_vex = Array.new(x.length * 2 - 1, 0)
65
+ y_vex = Array.new(x.length * 2 - 1, 0)
66
+ x_vex[0] = x[0]
67
+ y_vex[0] = y[0]
68
+ o = 0
69
+ if style == :post
70
+ (1 ... x.length).each do |i|
71
+ x_vex[i + o] = x[i]
72
+ x_vex[i + o + 1] = x[i]
73
+ y_vex[i + o] = y[i-1]
74
+ y_vex[i + o + 1] = y[i]
75
+ o += 1
76
+ end
77
+ elsif style == :pre
78
+ (1 ... x.length).each do |i|
79
+ x_vex[i + o] = x[i-1]
80
+ x_vex[i + o + 1] = x[i]
81
+ y_vex[i + o] = y[i]
82
+ y_vex[i + o + 1] = y[i]
83
+ o += 1
84
+ end
85
+ end
86
+ return [x_vex, y_vex]
87
+ end
88
+ end
@@ -0,0 +1,344 @@
1
+ # coding: utf-8
2
+
3
+ module UnicodePlot
4
+
5
+ # ## Description
6
+ #
7
+ # Draw a stem-leaf plot of the given vector +vec+.
8
+ #
9
+ # ```
10
+ # stemplot(vec, **kwargs)
11
+ # ```
12
+ #
13
+ # Draw a back-to-back stem-leaf plot of the given vectors +vec1+ and +vec2+.
14
+ #
15
+ # ```
16
+ # stemplot(vec1, vec2, **kwargs)
17
+ # ```
18
+ #
19
+ # The vectors can be any object that converts to an Array, e.g. an Array, Range, etc.
20
+ # If all elements of the vector are Numeric, the stem-leaf plot is classified as a
21
+ # {NumericStemplot}, otherwise it is classified as a {StringStemplot}. Back-to-back
22
+ # stem-leaf plots must be the same type, i.e. String and Numeric stem-leaf plots cannot
23
+ # be mixed in a back-to-back plot.
24
+ #
25
+ # ## Usage
26
+ #
27
+ # stemplot(vec, [vec2], scale:, divider:, padchar:, trim: )
28
+ #
29
+ # ## Arguments
30
+ #
31
+ # - +vec+: Vector for which the stem leaf plot should be computed.
32
+ # - +vec2+: Optional secondary vector, will be used to create a back-to-back stem-leaf plot.
33
+ # - +scale+: Set scale of plot. Default = 10. Scale is changed via orders of magnitude. Common values are 0.1, 1, and 10. For String stems, the default value of 10 is a one character stem, 100 is a two character stem.
34
+ # - +divider+: Character for break between stem and leaf. Default = "|"
35
+ # - +padchar+: Character(s) to separate stems, leaves and dividers. Default = " "
36
+ # - +trim+: Trims the stem labels when there are no leaves. This can be useful if your data is sparse. Default = +false+
37
+ # - +string_padchar+: Character used to replace missing position for input strings shorter than the stem-size. Default = "_"
38
+ #
39
+ # ## Result
40
+ # A plot of object type is sent to <tt>$stdout</tt>
41
+ #
42
+ # @example Examples using Numbers
43
+ # # Generate some numbers
44
+ # fifty_floats = 50.times.map { rand(-1000..1000)/350.0 }
45
+ # eighty_ints = 80.times.map { rand(1..100) }
46
+ # another_eighty_ints = 80.times.map { rand(1..100) }
47
+ # three_hundred_ints = 300.times.map { rand(-100..100) }
48
+ #
49
+ # # Single sided stem-plot
50
+ # UnicodePlot.stemplot(eighty_ints)
51
+ #
52
+ # # Single sided stem-plot with positive and negative values
53
+ # UnicodePlot.stemplot(three_hundred_ints)
54
+ #
55
+ # # Single sided stem-plot using floating point values, scaled
56
+ # UnicodePlot.stemplot(fifty_floats, scale: 1)
57
+ #
58
+ # # Single sided stem-plot using floating point values, scaled with new divider
59
+ # UnicodePlot.stemplot(fifty_floats, scale: 1, divider: "😄")
60
+ #
61
+ # # Back to back stem-plot
62
+ # UnicodePlot.stemplot(eighty_ints, another_eighty_ints)
63
+ #
64
+ # @example Examples using Strings
65
+ # # Generate some strings
66
+ # words_1 = %w[apple junk ant age bee bar baz dog egg a]
67
+ # words_2 = %w[ape flan can cat juice elf gnome child fruit]
68
+ #
69
+ # # Single sided stem-plot
70
+ # UnicodePlot.stemplot(words_1)
71
+ #
72
+ # # Back to back stem-plot
73
+ # UnicodePlot.stemplot(words_1, words_2)
74
+ #
75
+ # # Scaled stem plot using scale=100 (two letters for the stem) and trimmed stems
76
+ # UnicodePlot.stemplot(words_1, scale: 100, trim: true)
77
+ #
78
+ # # Above, but changing the string_padchar
79
+ # UnicodePlot.stemplot(words_1, scale: 100, trim: true, string_padchar: '?')
80
+
81
+ class Stemplot
82
+
83
+ # Use {factory} method -- should not be directly called.
84
+ def initialize(*_args, **_kw)
85
+ @stemleafs = {}
86
+ end
87
+
88
+ # Factory method to create a Stemplot, creates either a NumericStemplot
89
+ # or StringStemplot depending on input.
90
+ #
91
+ # @param vector [Array] An array of elements to stem-leaf plot
92
+ # @return [NumericStemplot] If all elements are Numeric
93
+ # @return [StringStemplot] If any elements are not Numeric
94
+ def self.factory(vector, **kw)
95
+ vec = Array(vector)
96
+ if vec.all? { |item| item.is_a?(Numeric) }
97
+ NumericStemplot.new(vec, **kw)
98
+ else
99
+ StringStemplot.new(vec, **kw)
100
+ end
101
+ end
102
+
103
+ # Insert a stem and leaf
104
+ def insert(stem, leaf)
105
+ @stemleafs[stem] ||= []
106
+ @stemleafs[stem] << leaf
107
+ end
108
+
109
+ # Returns an unsorted list of stems
110
+ # @return [Array] Unsorted list of stems
111
+ def raw_stems
112
+ @stemleafs.keys
113
+ end
114
+
115
+ # Returns a list of leaves for a given stem
116
+ # @param stem [Object] The stem
117
+ # @return [Array] Unsorted list of leaves
118
+ def leaves(stem)
119
+ @stemleafs[stem] || []
120
+ end
121
+
122
+ # Determines largest length of any stem
123
+ # @return [Integer] Length value
124
+ def max_stem_length
125
+ @stemleafs.values.map(&:length).max
126
+ end
127
+
128
+ # Returns a sorted list of stems
129
+ # @param all [Boolean] Return all stems if true, otherwise only return stems if a leaf exists for a stem
130
+ # @return [Array] Sorted list of stems
131
+ def stems(all: true)
132
+ self.class.sorted_stem_list(raw_stems, all: all)
133
+ end
134
+
135
+ end
136
+
137
+ class NumericStemplot < Stemplot
138
+ def initialize(vector, scale: 10, **kw)
139
+ super
140
+ Array(vector).each do |value|
141
+ fvalue = value.to_f.fdiv(scale/10.0)
142
+ stemnum = (fvalue/10).to_i
143
+ leafnum = (fvalue - (stemnum*10)).to_i
144
+ stemsign = value.negative? ? "-" : ''
145
+ stem = stemsign + stemnum.abs.to_s
146
+ leaf = leafnum.abs.to_s
147
+ self.insert(stem, leaf)
148
+ end
149
+ end
150
+
151
+ # Print key to STDOUT
152
+ # @param scale [Integer] Scale, should be a power of 10
153
+ # @param divider [String] Divider character between stem and leaf
154
+ def print_key(scale, divider)
155
+ # First print the key
156
+ puts "Key: 1#{divider}0 = #{scale}"
157
+ # Description of where the decimal is
158
+ trunclog = Math.log10(scale).truncate
159
+ ndigits = trunclog.abs
160
+ right_or_left = (trunclog < 0) ? "left" : "right"
161
+ puts "The decimal is #{ndigits} digit(s) to the #{right_or_left} of #{divider}"
162
+ end
163
+
164
+ # Used when we have stems from a back-to-back stemplot and a combined list of stems is given
165
+ # @param stems [Array] Concatenated list of stems from two plots
166
+ # @param all [Boolean] Return all stems if true, otherwise only return stems if a leaf exists for a stem
167
+ # @return [Array] Sorted list of stems
168
+ def self.sorted_stem_list(stems, all: true)
169
+ negkeys, poskeys = stems.partition { |str| str[0] == '-'}
170
+ if all
171
+ negmin, negmax = negkeys.map(&:to_i).map(&:abs).minmax
172
+ posmin, posmax = poskeys.map(&:to_i).minmax
173
+ negrange = negmin ? (negmin..negmax).to_a.reverse.map { |s| "-"+s.to_s } : []
174
+ posrange = posmin ? (posmin..posmax).to_a.map(&:to_s) : []
175
+ return negrange + posrange
176
+ else
177
+ negkeys.sort! { |a,b| a.to_i <=> b.to_i }
178
+ poskeys.sort! { |a,b| a.to_i <=> b.to_i }
179
+ return negkeys + poskeys
180
+ end
181
+ end
182
+ end
183
+
184
+ class StringStemplot < Stemplot
185
+
186
+ def initialize(vector, scale: 10, string_padchar: '_', **_kw)
187
+ super
188
+ stem_places = Math.log10(scale).floor
189
+ raise ArgumentError, "Cannot take fewer than 1 place from stem. Scale parameter should be greater than or equal to 10." if stem_places < 1
190
+ vector.each do |value|
191
+ # Strings may be shorter than the number of places we desire,
192
+ # so we will pad them with a string-pad-character.
193
+ padded_value = value.ljust(stem_places+1, string_padchar)
194
+ stem = padded_value[0...stem_places]
195
+ leaf = padded_value[stem_places]
196
+ self.insert(stem, leaf)
197
+ end
198
+ end
199
+
200
+ # Function prototype to provide same interface as {NumericStemplot}.
201
+ # This function does not do anything.
202
+ # @return [false]
203
+ def print_key(_scale, _divider)
204
+ # intentionally empty
205
+ return false
206
+ end
207
+
208
+ # Used when we have stems from a back-to-back stemplot and a combined list of stems is given
209
+ # @param stems [Array] Concatenated list of stems from two plots
210
+ # @param all [Boolean] Return all stems if true, otherwise only return stems if a leaf exists for a stem
211
+ # @return [Array] Sorted list of stems
212
+ def self.sorted_stem_list(stems, all: true)
213
+ if all
214
+ rmin, rmax = stems.minmax
215
+ return (rmin .. rmax).to_a
216
+ else
217
+ stems.sort
218
+ end
219
+ end
220
+
221
+ end
222
+
223
+ # Print a Single-Vector stemplot to STDOUT.
224
+ #
225
+ # - Stem data is printed on the left.
226
+ # - Leaf data is printed on the right.
227
+ # - Key is printed at the bottom.
228
+ # @param plt [Stemplot] Stemplot object
229
+ # @param scale [Integer] Scale, should be a power of 10
230
+ # @param divider [String] Divider character between stem and leaf
231
+ # @param padchar [String] Padding character
232
+ # @param trim [Boolean] Trim missing stems from the plot
233
+ def stemplot1!(plt,
234
+ scale: 10,
235
+ divider: "|",
236
+ padchar: " ",
237
+ trim: false,
238
+ **_kw
239
+ )
240
+
241
+ stem_labels = plt.stems(all: !trim)
242
+ label_len = stem_labels.map(&:length).max
243
+ column_len = label_len + 1
244
+
245
+ stem_labels.each do |stem|
246
+ leaves = plt.leaves(stem).sort
247
+ stemlbl = stem.rjust(label_len, padchar).ljust(column_len, padchar)
248
+ puts stemlbl + divider + padchar + leaves.join
249
+ end
250
+ plt.print_key(scale, divider)
251
+ end
252
+
253
+ # Print a Back-to-Back Stemplot to STDOUT
254
+ #
255
+ # - +plt1+ Leaf data is printed on the left.
256
+ # - Common stem data is printed in the center.
257
+ # - +plt2+ Leaf data is printed on the right.
258
+ # - Key is printed at the bottom.
259
+ # @param plt1 [Stemplot] Stemplot object for the left side
260
+ # @param plt2 [Stemplot] Stemplot object for the right side
261
+ # @param scale [Integer] Scale, should be a power of 10
262
+ # @param divider [String] Divider character between stem and leaf
263
+ # @param padchar [String] Padding character
264
+ # @param trim [Boolean] Trim missing stems from the plot
265
+ def stemplot2!(plt1, plt2,
266
+ scale: 10,
267
+ divider: "|",
268
+ padchar: " ",
269
+ trim: false,
270
+ **_kw
271
+ )
272
+ stem_labels = plt1.class.sorted_stem_list( (plt1.raw_stems + plt2.raw_stems).uniq, all: !trim )
273
+ label_len = stem_labels.map(&:length).max
274
+ column_len = label_len + 1
275
+
276
+ leftleaf_len = plt1.max_stem_length
277
+
278
+ stem_labels.each do |stem|
279
+ left_leaves = plt1.leaves(stem).sort.join('')
280
+ right_leaves = plt2.leaves(stem).sort.join('')
281
+ left_leaves_just = left_leaves.reverse.rjust(leftleaf_len, padchar)
282
+ stem = stem.rjust(column_len, padchar).ljust(column_len+1, padchar)
283
+ puts left_leaves_just + padchar + divider + stem + divider + padchar + right_leaves
284
+ end
285
+
286
+ plt1.print_key(scale, divider)
287
+
288
+ end
289
+
290
+ # Generates one or more {Stemplot} objects from the input data
291
+ # and prints a Single or Double stemplot using {stemplot1!} or {stemplot2!}
292
+ # @see Stemplot
293
+ # @example Single sided stemplot
294
+ # >> UnicodePlot.stemplot(eighty_ints)
295
+ # 0 | 257
296
+ # 1 | 00335679
297
+ # 2 | 034455899
298
+ # 3 | 145588
299
+ # 4 | 0022223
300
+ # 5 | 0223399
301
+ # 6 | 012345568889
302
+ # 7 | 01133334466777888
303
+ # 8 | 013689
304
+ # 9 | 22667
305
+ # Key: 1|0 = 10
306
+ # The decimal is 1 digit(s) to the right of |
307
+ #
308
+ # @example Back-to-back stemplot
309
+ # >> UnicodePlot.stemplot(eighty_ints, another_eighty_ints)
310
+ # 752 | 0 | 1244457899
311
+ # 97653300 | 1 | 4799
312
+ # 998554430 | 2 | 015668
313
+ # 885541 | 3 | 0144557888899
314
+ # 3222200 | 4 | 00268
315
+ # 9933220 | 5 | 0234778
316
+ # 988865543210 | 6 | 122222357889
317
+ # 88877766443333110 | 7 | 134556689
318
+ # 986310 | 8 | 24589
319
+ # 76622 | 9 | 022234468
320
+ # Key: 1|0 = 10
321
+ # The decimal is 1 digit(s) to the right of |
322
+ #
323
+ def stemplot(*args, scale: 10, **kw)
324
+ case args.length
325
+ when 1
326
+ # Stemplot object
327
+ plt = Stemplot.factory(args[0], scale: scale, **kw)
328
+ # Dispatch to plot routine
329
+ stemplot1!(plt, scale: scale, **kw)
330
+ when 2
331
+ # Stemplot object
332
+ plt1 = Stemplot.factory(args[0], scale: scale)
333
+ plt2 = Stemplot.factory(args[1], scale: scale)
334
+ raise ArgumentError, "Plot types must be the same for back-to-back stemplot " +
335
+ "#{plt1.class} != #{plt2.class}" unless plt1.class == plt2.class
336
+ # Dispatch to plot routine
337
+ stemplot2!(plt1, plt2, scale: scale, **kw)
338
+ else
339
+ raise ArgumentError, "Expecting one or two arguments"
340
+ end
341
+ end
342
+
343
+ module_function :stemplot, :stemplot1!, :stemplot2!
344
+ end