unicode_plot 0.0.4 → 0.0.5

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