Skip to content

Commit ef94340

Browse files
authored
Merge pull request #40 from nanobowers/stemplot
adding support for stemplot and tests
2 parents 60558d9 + 023d7f8 commit ef94340

15 files changed

Lines changed: 547 additions & 0 deletions

example/ex_stemplot.rb

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#!/bin/env ruby
2+
# coding: utf-8
3+
$LOAD_PATH << "#{__dir__}/../lib"
4+
require "unicode_plot"
5+
6+
fifty_floats = 50.times.map { rand(-1000..1000)/350.0 }
7+
eighty_ints = 80.times.map { rand(1..100) }
8+
another_eighty_ints = 80.times.map { rand(1..100) }
9+
three_hundred_ints = 300.times.map { rand(-100..100) }
10+
11+
UnicodePlot.stemplot(eighty_ints)
12+
13+
UnicodePlot.stemplot(three_hundred_ints)
14+
15+
UnicodePlot.stemplot(fifty_floats, scale: 1)
16+
17+
UnicodePlot.stemplot(fifty_floats, scale: 1, divider: "😄")
18+
19+
UnicodePlot.stemplot(eighty_ints, another_eighty_ints)
20+
21+
# Examples with strings
22+
23+
words_1 = %w[apple junk ant age bee bar baz dog egg a]
24+
words_2 = %w[ape flan can cat juice elf gnome child fruit]
25+
26+
UnicodePlot.stemplot(words_1)
27+
28+
UnicodePlot.stemplot(words_1, words_2)
29+
30+
31+
UnicodePlot.stemplot(words_1, scale: 100, trim: true)
32+
33+
UnicodePlot.stemplot(words_1, scale: 100, trim: true, string_padchar: '?')
34+
35+
floats = (-8..8).to_a.map { |a| a / 2.0 }
36+
UnicodePlot.stemplot(floats, scale: 1)

lib/unicode_plot.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,4 @@
2525
require 'unicode_plot/histogram'
2626
require 'unicode_plot/scatterplot'
2727
require 'unicode_plot/stairs'
28+
require 'unicode_plot/stemplot'

lib/unicode_plot/stemplot.rb

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
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+
# stemplot(vec, **kwargs)
10+
#
11+
#
12+
# Draw a back-to-back stem-leaf plot of the given vectors +vec1+ and +vec2+.
13+
#
14+
# stemplot(vec, vec2, **kwargs)
15+
#
16+
# The vectors can be any object that converts to an Array, e.g. an Array, Range, etc.
17+
# If all elements of the vector are Numeric, the stem-leaf plot is classified as a
18+
# +NumericStemplot+, otherwise it is classified as a StringStemplot. Back-to-back
19+
# stem-leaf plots must be the same type, i.e. String and Numeric stem-leaf plots cannot
20+
# be mixed in a back-to-back plot.
21+
#
22+
# == Usage
23+
#
24+
# stemplot(vec, [vec2], scale:, divider:, padchar:, trim: )
25+
#
26+
# == Arguments
27+
#
28+
# - +vec+: Vector for which the stem leaf plot should be computed.
29+
# - +vec2+: Optional secondary vector, will be used to create a back-to-back stem-leaf plot.
30+
# - +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.
31+
# - +divider+: Character for break between stem and leaf. Default = "|"
32+
# - +padchar+: Character(s) to separate stems, leaves and dividers. Default = " "
33+
# - +trim+: Trims the stem labels when there are no leaves. This can be useful if your data is sparse. Default = +false+
34+
# - +string_padchar+: Character used to replace missing position for input strings shorter than the stem-size. Default = "_"
35+
#
36+
# == Results
37+
# A plot of object type is sent to $stdout
38+
#
39+
# == Examples using Numbers
40+
#
41+
# # Generate some numbers
42+
# fifty_floats = 50.times.map { rand(-1000..1000)/350.0 }
43+
# eighty_ints = 80.times.map { rand(1..100) }
44+
# another_eighty_ints = 80.times.map { rand(1..100) }
45+
# three_hundred_ints = 300.times.map { rand(-100..100) }
46+
#
47+
# # Single sided stem-plot
48+
# UnicodePlot.stemplot(eighty_ints)
49+
#
50+
# # Single sided stem-plot with positive and negative values
51+
# UnicodePlot.stemplot(three_hundred_ints)
52+
#
53+
# # Single sided stem-plot using floating point values, scaled
54+
# UnicodePlot.stemplot(fifty_floats, scale: 1)
55+
#
56+
# # Single sided stem-plot using floating point values, scaled with new divider
57+
# UnicodePlot.stemplot(fifty_floats, scale: 1, divider: "😄")
58+
#
59+
# # Back to back stem-plot
60+
# UnicodePlot.stemplot(eighty_ints, another_eighty_ints)
61+
#
62+
# == Examples using Strings
63+
#
64+
# # Generate some strings
65+
# words_1 = %w[apple junk ant age bee bar baz dog egg a]
66+
# words_2 = %w[ape flan can cat juice elf gnome child fruit]
67+
#
68+
# # Single sided stem-plot
69+
# UnicodePlot.stemplot(words_1)
70+
#
71+
# # Back to back stem-plot
72+
# UnicodePlot.stemplot(words_1, words_2)
73+
#
74+
# # Scaled stem plot using scale=100 (two letters for the stem) and trimmed stems
75+
# UnicodePlot.stemplot(words_1, scale: 100, trim: true)
76+
#
77+
# # Above, but changing the string_padchar
78+
# UnicodePlot.stemplot(words_1, scale: 100, trim: true, string_padchar: '?')
79+
80+
class Stemplot
81+
82+
def initialize(*_args, **_kw)
83+
@stemleafs = {}
84+
end
85+
86+
def self.factory(vector, **kw)
87+
vec = Array(vector)
88+
if vec.all? { |item| item.is_a?(Numeric) }
89+
NumericStemplot.new(vec, **kw)
90+
else
91+
StringStemplot.new(vec, **kw)
92+
end
93+
end
94+
95+
def insert(stem, leaf)
96+
@stemleafs[stem] ||= []
97+
@stemleafs[stem] << leaf
98+
end
99+
100+
def raw_stems
101+
@stemleafs.keys
102+
end
103+
104+
def leaves(stem)
105+
@stemleafs[stem] || []
106+
end
107+
108+
def max_stem_length
109+
@stemleafs.values.map(&:length).max
110+
end
111+
112+
# instance method to return sorted list of stems.
113+
def stems(all: true)
114+
self.class.sorted_stem_list(raw_stems, all: all)
115+
end
116+
117+
end
118+
119+
class NumericStemplot < Stemplot
120+
def initialize(vector, scale: 10, **kw)
121+
super
122+
Array(vector).each do |value|
123+
fvalue = value.to_f.fdiv(scale/10.0)
124+
stemnum = (fvalue/10).to_i
125+
leafnum = (fvalue - (stemnum*10)).to_i
126+
stemsign = value.negative? ? "-" : ''
127+
stem = stemsign + stemnum.abs.to_s
128+
leaf = leafnum.abs.to_s
129+
self.insert(stem, leaf)
130+
end
131+
end
132+
133+
def print_key(scale, divider)
134+
# First print the key
135+
puts "Key: 1#{divider}0 = #{scale}"
136+
# Description of where the decimal is
137+
trunclog = Math.log10(scale).truncate
138+
ndigits = trunclog.abs
139+
right_or_left = (trunclog < 0) ? "left" : "right"
140+
puts "The decimal is #{ndigits} digit(s) to the #{right_or_left} of #{divider}"
141+
end
142+
143+
# class method to return sorted list of stems.
144+
# used when we have stems from a dual-plot
145+
def self.sorted_stem_list(stems, all: true)
146+
negkeys, poskeys = stems.partition { |str| str[0] == '-'}
147+
if all
148+
negmin, negmax = negkeys.map(&:to_i).map(&:abs).minmax
149+
posmin, posmax = poskeys.map(&:to_i).minmax
150+
negrange = negmin ? (negmin..negmax).to_a.reverse.map { |s| "-"+s.to_s } : []
151+
posrange = posmin ? (posmin..posmax).to_a.map(&:to_s) : []
152+
return negrange + posrange
153+
else
154+
negkeys.sort! { |a,b| a.to_i <=> b.to_i }
155+
poskeys.sort! { |a,b| a.to_i <=> b.to_i }
156+
return negkeys + poskeys
157+
end
158+
end
159+
end
160+
161+
class StringStemplot < Stemplot
162+
163+
def initialize(vector, scale: 10, string_padchar: '_', **_kw)
164+
super
165+
stem_places = Math.log10(scale).floor
166+
raise ArgumentError, "Cannot take fewer than 1 place from stem. Scale parameter should be greater than or equal to 10." if stem_places < 1
167+
vector.each do |value|
168+
# Strings may be shorter than the number of places we desire,
169+
# so we will pad them with a string-pad-character.
170+
padded_value = value.ljust(stem_places+1, string_padchar)
171+
stem = padded_value[0...stem_places]
172+
leaf = padded_value[stem_places]
173+
self.insert(stem, leaf)
174+
end
175+
end
176+
177+
def print_key(scale, divider)
178+
# intentionally empty
179+
return false
180+
# First print the key
181+
puts ""
182+
puts "Key: 1#{divider}0 = #{scale}"
183+
# Description of where the decimal is
184+
trunclog = Math.log10(scale).truncate
185+
ndigits = trunclog.abs
186+
right_or_left = (trunclog < 0) ? "left" : "right"
187+
puts "The decimal is #{ndigits} digit(s) to the #{right_or_left} of #{divider}"
188+
end
189+
190+
def self.sorted_stem_list(stems, all: true)
191+
if all
192+
rmin, rmax = stems.minmax
193+
return (rmin .. rmax).to_a
194+
else
195+
stems.sort
196+
end
197+
end
198+
199+
end
200+
# single-vector stemplot
201+
def stemplot1!(plt,
202+
scale: 10,
203+
divider: "|",
204+
padchar: " ",
205+
trim: false,
206+
**_kw
207+
)
208+
209+
stem_labels = plt.stems(all: !trim)
210+
label_len = stem_labels.map(&:length).max
211+
column_len = label_len + 1
212+
213+
stem_labels.each do |stem|
214+
leaves = plt.leaves(stem).sort
215+
stemlbl = stem.rjust(label_len, padchar).ljust(column_len, padchar)
216+
puts stemlbl + divider + padchar + leaves.join
217+
end
218+
plt.print_key(scale, divider)
219+
end
220+
221+
# back-to-back stemplot
222+
def stemplot2!(plt1, plt2,
223+
scale: 10,
224+
divider: "|",
225+
padchar: " ",
226+
trim: false,
227+
**_kw
228+
)
229+
stem_labels = plt1.class.sorted_stem_list( (plt1.raw_stems + plt2.raw_stems).uniq, all: !trim )
230+
label_len = stem_labels.map(&:length).max
231+
column_len = label_len + 1
232+
233+
leftleaf_len = plt1.max_stem_length
234+
235+
stem_labels.each do |stem|
236+
left_leaves = plt1.leaves(stem).sort.join('')
237+
right_leaves = plt2.leaves(stem).sort.join('')
238+
left_leaves_just = left_leaves.reverse.rjust(leftleaf_len, padchar)
239+
stem = stem.rjust(column_len, padchar).ljust(column_len+1, padchar)
240+
puts left_leaves_just + padchar + divider + stem + divider + padchar + right_leaves
241+
end
242+
243+
plt1.print_key(scale, divider)
244+
245+
end
246+
247+
# Single or Double
248+
def stemplot(*args, scale: 10, **kw)
249+
case args.length
250+
when 1
251+
# Stemplot object
252+
plt = Stemplot.factory(args[0], scale: scale, **kw)
253+
# Dispatch to plot routine
254+
stemplot1!(plt, scale: scale, **kw)
255+
when 2
256+
# Stemplot object
257+
plt1 = Stemplot.factory(args[0], scale: scale)
258+
plt2 = Stemplot.factory(args[1], scale: scale)
259+
raise ArgumentError, "Plot types must be the same for back-to-back stemplot " +
260+
"#{plt1.class} != #{plt2.class}" unless plt1.class == plt2.class
261+
# Dispatch to plot routine
262+
stemplot2!(plt1, plt2, scale: scale, **kw)
263+
else
264+
raise ArgumentError, "Expecting one or two arguments"
265+
end
266+
end
267+
268+
module_function :stemplot, :stemplot1!, :stemplot2!
269+
end
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
98652211 | 0 | 145589
2+
95543000 | 1 | 11344889
3+
4410 | 2 | 23344666788
4+
9743221100 | 3 | 3358888
5+
98866432211110 | 4 | 11189
6+
5310 | 5 | 0133377778
7+
8764331 | 6 | 012356789
8+
8553210 | 7 | 025569
9+
944400 | 8 | 1558999
10+
98755442210 | 9 | 0123466899
11+
0 | 10 | 0
12+
Key: 1|0 = 10
13+
The decimal is 1 digit(s) to the right of |
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
png_ | a | p
2+
eaa | b |
3+
| c | aah
4+
o | d |
5+
g | e | l
6+
| f | lr
7+
| g | n
8+
| h |
9+
| i |
10+
u | j | u

test/fixtures/stemplot/float.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
-1 | 0077
2+
-0 | 0000234556677788999
3+
0 | 01222356788899999
4+
1 | 034889
5+
2 | 127
6+
3 | 1
7+
Key: 1|0 = 10
8+
The decimal is 1 digit(s) to the right of |
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
-4 | 0
2+
-3 | 05
3+
-2 | 05
4+
-1 | 05
5+
-0 | 5
6+
0 | 05
7+
1 | 05
8+
2 | 05
9+
3 | 05
10+
4 | 0
11+
Key: 1|0 = 1
12+
The decimal is 0 digit(s) to the right of |
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
0 😄 11225689
2+
1 😄 00034559
3+
2 😄 0144
4+
3 😄 0011223479
5+
4 😄 01111223466889
6+
5 😄 0135
7+
6 😄 1334678
8+
7 😄 0123558
9+
8 😄 004449
10+
9 😄 01224455789
11+
10 😄 0
12+
Key: 1😄0 = 10
13+
The decimal is 1 digit(s) to the right of 😄
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
0 | 11225689
2+
1 | 00034559
3+
2 | 0144
4+
3 | 0011223479
5+
4 | 01111223466889
6+
5 | 0135
7+
6 | 1334678
8+
7 | 0123558
9+
8 | 004449
10+
9 | 01224455789
11+
10 | 0
12+
Key: 1|0 = 10
13+
The decimal is 1 digit(s) to the right of |

0 commit comments

Comments
 (0)