unicode_plot 0.0.1 → 0.0.2

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
  SHA256:
3
- metadata.gz: 7870c250eab339403fa566e04bb0361d0d9bc56f655e54dde037e559124d8956
4
- data.tar.gz: 8a01e8f5466020b081418c14c373c8fbc6a0c9a8d860c056bbfc31ba8fe1f820
3
+ metadata.gz: 30cc04427f90664c0157ae18002b8e241cb54d232b33f66edc72b0e6b2dac3d7
4
+ data.tar.gz: 05c0bfc5b3d2c910bb39f655fb1b10f44c76a552b6e87ede46b4b3f3d04055ca
5
5
  SHA512:
6
- metadata.gz: 6548d8b2a016fb442407253908e1e57835c45e19cc9d2137dedcbbe1e52e41092f5864c3f36c322122b282c4f8d77d04e6e6afbc403b959b6819b8067eaccf1e
7
- data.tar.gz: b813d2620b12c050c318868f02c7bf14bec7c3f44dedf9a5aeaaaaca5f98703a3b7cfab89f3fd45f7dffec2dfbe4f4ff9788b425c02501103fb41169a6ca3634
6
+ metadata.gz: 6111ed2d7cc47821286ff5da5a33660c24298a04eaeb56377717aa548b9ba150a4c2b67e0ccf62b4aa36c577413babf0bf7fdc50fd37b6945142628ad0d9ed5a
7
+ data.tar.gz: 0d11730310f093b679a71ca4bb00b760d10e127030cbe797cbf7a171094e192908a3bb3cf7d9de2ba83527ccd8b60ce8f5dbb1956870741aa4a4f55b5c45ac95
data/README.md CHANGED
@@ -29,6 +29,7 @@ You can get the results below by running the above script:
29
29
  ## Supported charts
30
30
 
31
31
  - barplot
32
+ - boxplot
32
33
  - lineplot
33
34
 
34
35
  ## Acknowledgement
data/lib/unicode_plot.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require 'unicode_plot/version'
2
2
 
3
+ require 'unicode_plot/utils'
3
4
  require 'unicode_plot/styled_printer'
4
5
  require 'unicode_plot/value_transformer'
5
6
  require 'unicode_plot/renderer'
@@ -11,4 +12,5 @@ require 'unicode_plot/braille_canvas'
11
12
  require 'unicode_plot/plot'
12
13
 
13
14
  require 'unicode_plot/barplot'
15
+ require 'unicode_plot/boxplot'
14
16
  require 'unicode_plot/lineplot'
@@ -1,6 +1,100 @@
1
+ require 'enumerable/statistics'
2
+
1
3
  module UnicodePlot
2
4
  class Boxplot < Plot
3
- def initialize(
5
+ MIN_WIDTH = 10
6
+ DEFAULT_COLOR = :green
7
+ DEFAULT_WIDTH = 40
8
+
9
+ def initialize(data, width, color, min_x, max_x, **kw)
10
+ if min_x == max_x
11
+ min_x -= 1
12
+ max_x += 1
13
+ end
14
+ width = [width, MIN_WIDTH].max
15
+ @data = [data.percentile([0, 25, 50, 75, 100])]
16
+ @color = color
17
+ @width = [width, MIN_WIDTH].max
18
+ @min_x = min_x
19
+ @max_x = max_x
20
+ super(**kw)
21
+ end
22
+
23
+ attr_reader :min_x, :max_x
24
+
25
+ def n_data
26
+ @data.length
27
+ end
28
+
29
+ def n_rows
30
+ 3 * @data.length
31
+ end
32
+
33
+ def n_columns
34
+ @width
35
+ end
36
+
37
+ def add_series!(data)
38
+ mi, ma = data.minmax
39
+ @data << data.percentile([0, 25, 50, 75, 100])
40
+ @min_x = [mi, @min_x].min
41
+ @max_x = [ma, @max_x].max
42
+ end
43
+
44
+ def print_row(out, row_index)
45
+ check_row_index(row_index)
46
+ series = @data[(row_index / 3.0).to_i]
47
+
48
+ series_row = row_index % 3
49
+
50
+ min_char = ['╷', '├' , '╵'][series_row]
51
+ line_char = [' ', '─' , ' '][series_row]
52
+ left_box_char = ['┌', '┤' , '└'][series_row]
53
+ line_box_char = ['─', ' ' , '─'][series_row]
54
+ median_char = ['┬', '│' , '┴'][series_row]
55
+ right_box_char = ['┐', '├' , '┘'][series_row]
56
+ max_char = ['╷', '┤' , '╵'][series_row]
57
+
58
+ line = (0 ... @width).map { ' ' }
59
+
60
+ # Draw shapes first - this is most important,
61
+ # so they'll always be drawn even if there's not enough space
62
+
63
+ transformed = transform(series)
64
+ line[transformed[0] - 1] = min_char
65
+ line[transformed[1] - 1] = left_box_char
66
+ line[transformed[2] - 1] = median_char
67
+ line[transformed[3] - 1] = right_box_char
68
+ line[transformed[4] - 1] = max_char
69
+
70
+ (transformed[0] ... (transformed[1] - 1)).each do |i|
71
+ line[i] = line_char
72
+ end
73
+ (transformed[1] ... (transformed[2] - 1)).each do |i|
74
+ line[i] = line_box_char
75
+ end
76
+ (transformed[2] ... (transformed[3] - 1)).each do |i|
77
+ line[i] = line_box_char
78
+ end
79
+ (transformed[3] ... (transformed[4] - 1)).each do |i|
80
+ line[i] = line_char
81
+ end
82
+
83
+ print_styled(out, line.join(''), color: @color)
84
+ end
85
+
86
+ private def transform(values)
87
+ values.map do |val|
88
+ val = (val - @min_x).fdiv(@max_x - @min_x) * @width
89
+ val.round(half: :even).clamp(1, @width).to_i
90
+ end
91
+ end
92
+
93
+ private def check_row_index(row_index)
94
+ unless 0 <= row_index && row_index < n_rows
95
+ raise ArgumentError, "row_index out of bounds"
96
+ end
97
+ end
4
98
  end
5
99
 
6
100
  module_function def boxplot(*args,
@@ -15,14 +109,87 @@ module UnicodePlot
15
109
  data = Hash(data)
16
110
  text = data.keys
17
111
  data = data.values
112
+ when 1
113
+ data = args[0]
18
114
  when 2
19
- text, data = *args
20
115
  text = Array(args[0])
21
- data = Array(args[1])
116
+ data = args[1]
22
117
  else
23
118
  raise ArgumentError, "wrong number of arguments"
24
119
  end
25
120
 
26
- plot = Boxplot.new(text, data)
121
+ case data[0]
122
+ when Numeric
123
+ data = [data]
124
+ when Array
125
+ # do nothing
126
+ else
127
+ data = data.to_ary
128
+ end
129
+ text ||= Array.new(data.length, "")
130
+
131
+ unless text.length == data.length
132
+ raise ArgumentError, "wrong number of text"
133
+ end
134
+
135
+ unless xlim.length == 2
136
+ raise ArgumentError, "xlim must be a length 2 array"
137
+ end
138
+
139
+ min_x, max_x = Utils.extend_limits(data.map(&:minmax).flatten, xlim)
140
+ width = [width, Boxplot::MIN_WIDTH].max
141
+
142
+ plot = Boxplot.new(data[0], width, color, min_x, max_x,
143
+ border: border, **kw)
144
+ (1 ... data.length).each do |i|
145
+ plot.add_series!(data[i])
146
+ end
147
+
148
+ mean_x = (min_x + max_x) / 2.0
149
+ min_x_str = (Utils.roundable?(min_x) ? min_x.round : min_x).to_s
150
+ mean_x_str = (Utils.roundable?(mean_x) ? mean_x.round : mean_x).to_s
151
+ max_x_str = (Utils.roundable?(max_x) ? max_x.round : max_x).to_s
152
+ plot.annotate!(:bl, min_x_str, color: :light_black)
153
+ plot.annotate!(:b, mean_x_str, color: :light_black)
154
+ plot.annotate!(:br, max_x_str, color: :light_black)
155
+
156
+ text.each_with_index do |name, i|
157
+ plot.annotate_row!(:l, i*3+1, name) if name.length > 0
158
+ end
159
+
160
+ plot
161
+ end
162
+
163
+ module_function def boxplot!(plot, *args, **kw)
164
+ case args.length
165
+ when 1
166
+ data = args[0]
167
+ name = kw[:name] || ""
168
+ when 2
169
+ name = args[0]
170
+ data = args[1]
171
+ else
172
+ raise ArgumentError, "worng number of arguments"
173
+ end
174
+
175
+ if data.empty?
176
+ raise ArgumentError, "Can't append empty array to boxplot"
177
+ end
178
+
179
+ plot.add_series!(data)
180
+
181
+ plot.annotate_row!(:l, (plot.n_data - 1)*3+1, name) if name && name != ""
182
+
183
+ min_x = plot.min_x
184
+ max_x = plot.max_x
185
+ mean_x = (min_x + max_x) / 2.0
186
+ min_x_str = (Utils.roundable?(min_x) ? min_x.round : min_x).to_s
187
+ mean_x_str = (Utils.roundable?(mean_x) ? mean_x.round : mean_x).to_s
188
+ max_x_str = (Utils.roundable?(max_x) ? max_x.round : max_x).to_s
189
+ plot.annotate!(:bl, min_x_str, color: :light_black)
190
+ plot.annotate!(:b, mean_x_str, color: :light_black)
191
+ plot.annotate!(:br, max_x_str, color: :light_black)
192
+
193
+ plot
27
194
  end
28
195
  end
@@ -19,8 +19,8 @@ module UnicodePlot
19
19
  end
20
20
  width = [width, MIN_WIDTH].max
21
21
  height = [height, MIN_HEIGHT].max
22
- min_x, max_x = extend_limits(x, xlim)
23
- min_y, max_y = extend_limits(y, ylim)
22
+ min_x, max_x = Utils.extend_limits(x, xlim)
23
+ min_y, max_y = Utils.extend_limits(y, ylim)
24
24
  origin_x = min_x
25
25
  origin_y = min_y
26
26
  plot_width = max_x - origin_x
@@ -32,10 +32,10 @@ module UnicodePlot
32
32
  plot_height: plot_height)
33
33
  super(**kw)
34
34
 
35
- min_x_str = (roundable?(min_x) ? min_x.round : min_x).to_s
36
- max_x_str = (roundable?(max_x) ? max_x.round : max_x).to_s
37
- min_y_str = (roundable?(min_y) ? min_y.round : min_y).to_s
38
- max_y_str = (roundable?(max_y) ? max_y.round : max_y).to_s
35
+ min_x_str = (Utils.roundable?(min_x) ? min_x.round : min_x).to_s
36
+ max_x_str = (Utils.roundable?(max_x) ? max_x.round : max_x).to_s
37
+ min_y_str = (Utils.roundable?(min_y) ? min_y.round : min_y).to_s
38
+ max_y_str = (Utils.roundable?(max_y) ? max_y.round : max_y).to_s
39
39
 
40
40
  annotate_row!(:l, 0, max_y_str, color: :light_black)
41
41
  annotate_row!(:l, height-1, min_y_str, color: :light_black)
@@ -93,62 +93,6 @@ module UnicodePlot
93
93
  def print_row(out, row_index)
94
94
  @canvas.print_row(out, row_index)
95
95
  end
96
-
97
- def extend_limits(values, limits)
98
- mi, ma = limits.minmax.map(&:to_f)
99
- if mi == 0 && ma == 0
100
- mi, ma = values.minmax.map(&:to_f)
101
- end
102
- diff = ma - mi
103
- if diff == 0
104
- ma = mi + 1
105
- mi = mi - 1
106
- end
107
- if limits == [0, 0]
108
- plotting_range_narrow(mi, ma)
109
- else
110
- [mi, ma]
111
- end
112
- end
113
-
114
- def plotting_range_narrow(xmin, xmax)
115
- diff = xmax - xmin
116
- xmax = round_up_subtick(xmax, diff)
117
- xmin = round_down_subtick(xmin, diff)
118
- [xmin.to_f, xmax.to_f]
119
- end
120
-
121
- def round_up_subtick(x, m)
122
- if x == 0
123
- 0.0
124
- elsif x > 0
125
- x.ceil(ceil_neg_log10(m) + 1)
126
- else
127
- -(-x).floor(ceil_neg_log10(m) + 1)
128
- end
129
- end
130
-
131
- def round_down_subtick(x, m)
132
- if x == 0
133
- 0.0
134
- elsif x > 0
135
- x.floor(ceil_neg_log10(m) + 1)
136
- else
137
- -(-x).ceil(ceil_neg_log10(m) + 1)
138
- end
139
- end
140
-
141
- def ceil_neg_log10(x)
142
- if roundable?(-Math.log10(x))
143
- (-Math.log10(x)).ceil
144
- else
145
- (-Math.log10(x)).floor
146
- end
147
- end
148
-
149
- def roundable?(x)
150
- x.to_i == x
151
- end
152
96
  end
153
97
 
154
98
  class Lineplot < GridCanvas
@@ -11,6 +11,17 @@ module UnicodePlot
11
11
  r: "│"
12
12
  }.freeze
13
13
 
14
+ BORDER_CORNERS = {
15
+ tl: "┌",
16
+ tr: "┐",
17
+ bl: "└",
18
+ br: "┘",
19
+ t: " ",
20
+ l: " ",
21
+ b: " ",
22
+ r: " ",
23
+ }.freeze
24
+
14
25
  BORDER_BARPLOT = {
15
26
  tl: "┌",
16
27
  tr: "┐",
@@ -25,6 +36,7 @@ module UnicodePlot
25
36
 
26
37
  BORDER_MAP = {
27
38
  solid: BorderMaps::BORDER_SOLID,
39
+ corners: BorderMaps::BORDER_CORNERS,
28
40
  barplot: BorderMaps::BORDER_BARPLOT,
29
41
  }.freeze
30
42
 
@@ -0,0 +1,61 @@
1
+ module UnicodePlot
2
+ module Utils
3
+ module_function
4
+
5
+ def extend_limits(values, limits)
6
+ mi, ma = limits.minmax.map(&:to_f)
7
+ if mi == 0 && ma == 0
8
+ mi, ma = values.minmax.map(&:to_f)
9
+ end
10
+ diff = ma - mi
11
+ if diff == 0
12
+ ma = mi + 1
13
+ mi = mi - 1
14
+ end
15
+ if limits == [0, 0]
16
+ plotting_range_narrow(mi, ma)
17
+ else
18
+ [mi, ma]
19
+ end
20
+ end
21
+
22
+ def plotting_range_narrow(xmin, xmax)
23
+ diff = xmax - xmin
24
+ xmax = round_up_subtick(xmax, diff)
25
+ xmin = round_down_subtick(xmin, diff)
26
+ [xmin.to_f, xmax.to_f]
27
+ end
28
+
29
+ def round_up_subtick(x, m)
30
+ if x == 0
31
+ 0.0
32
+ elsif x > 0
33
+ x.ceil(ceil_neg_log10(m) + 1)
34
+ else
35
+ -(-x).floor(ceil_neg_log10(m) + 1)
36
+ end
37
+ end
38
+
39
+ def round_down_subtick(x, m)
40
+ if x == 0
41
+ 0.0
42
+ elsif x > 0
43
+ x.floor(ceil_neg_log10(m) + 1)
44
+ else
45
+ -(-x).ceil(ceil_neg_log10(m) + 1)
46
+ end
47
+ end
48
+
49
+ def ceil_neg_log10(x)
50
+ if roundable?(-Math.log10(x))
51
+ (-Math.log10(x)).ceil
52
+ else
53
+ (-Math.log10(x)).floor
54
+ end
55
+ end
56
+
57
+ def roundable?(x)
58
+ x.to_i == x
59
+ end
60
+ end
61
+ end
@@ -1,5 +1,5 @@
1
1
  module UnicodePlot
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
 
4
4
  module Version
5
5
  numbers, TAG = VERSION.split("-", 2)
data/test/test-boxplot.rb CHANGED
@@ -1,4 +1,3 @@
1
- __END__
2
1
  class BoxplotTest < Test::Unit::TestCase
3
2
  include Helper::Fixture
4
3
  include Helper::WithTerm
@@ -8,7 +7,7 @@ class BoxplotTest < Test::Unit::TestCase
8
7
  test("without name") do
9
8
  plot = UnicodePlot.boxplot([1, 2, 3, 4, 5])
10
9
  _, output = with_term { plot.render($stdout) }
11
- assert_equal(fixture_path("boxplot/default_name.txt").read,
10
+ assert_equal(fixture_path("boxplot/default.txt").read,
12
11
  output)
13
12
  end
14
13
 
@@ -29,7 +28,7 @@ class BoxplotTest < Test::Unit::TestCase
29
28
  end
30
29
 
31
30
  test("print to tty") do
32
- _, output = with_term { plot.render($stdout) }
31
+ _, output = with_term { @plot.render($stdout) }
33
32
  assert_equal(fixture_path("boxplot/default_parameters.txt").read,
34
33
  output)
35
34
  end
@@ -44,5 +43,41 @@ class BoxplotTest < Test::Unit::TestCase
44
43
  output)
45
44
  end
46
45
  end
46
+
47
+ data([5, 6, 10, 20, 40].map.with_index {|max_x, i|
48
+ ["max_x: #{max_x}", [i + 1, max_x]] }.to_h)
49
+ test("with scaling") do
50
+ i, max_x = data
51
+ plot = UnicodePlot.boxplot([1, 2, 3, 4, 5], xlim: [0, max_x])
52
+ _, output = with_term { plot.render($stdout) }
53
+ assert_equal(fixture_path("boxplot/scale#{i}.txt").read,
54
+ output)
55
+ end
56
+
57
+ test("multi-series") do
58
+ plot = UnicodePlot.boxplot(["one", "two"],
59
+ [
60
+ [1, 2, 3, 4, 5],
61
+ [2, 3, 4, 5, 6, 7, 8, 9]
62
+ ],
63
+ title: "Multi-series",
64
+ xlabel: "foo",
65
+ color: :yellow)
66
+ _, output = with_term { plot.render($stdout) }
67
+ assert_equal(fixture_path("boxplot/multi1.txt").read,
68
+ output)
69
+
70
+ assert_same(plot,
71
+ UnicodePlot.boxplot!(plot, "one more", [-1, 2, 3, 4, 11]))
72
+ _, output = with_term { plot.render($stdout) }
73
+ assert_equal(fixture_path("boxplot/multi2.txt").read,
74
+ output)
75
+
76
+ assert_same(plot,
77
+ UnicodePlot.boxplot!(plot, [4, 2, 2.5, 4, 14], name: "last one"))
78
+ _, output = with_term { plot.render($stdout) }
79
+ assert_equal(fixture_path("boxplot/multi3.txt").read,
80
+ output)
81
+ end
47
82
  end
48
83
  end
data/unicode_plot.gemspec CHANGED
@@ -16,7 +16,7 @@ Gem::Specification.new do |spec|
16
16
 
17
17
  spec.summary = %q{Plot your data by Unicode characters}
18
18
  spec.description = %q{Plot your data by Unicode characters}
19
- spec.homepage = "https://github.com/mrkn/unicode_plot.rb"
19
+ spec.homepage = "https://github.com/red-data-tools/unicode_plot.rb"
20
20
  spec.license = "MIT"
21
21
 
22
22
  spec.files = ["README.md", "Rakefile", "Gemfile", "#{spec.name}.gemspec"]
@@ -29,6 +29,8 @@ Gem::Specification.new do |spec|
29
29
  spec.executables = spec.files.grep(%r{^exe/}) {|f| File.basename(f) }
30
30
  spec.require_paths = ["lib"]
31
31
 
32
+ spec.add_runtime_dependency "enumerable-statistics", ">= 2.0.0.pre"
33
+
32
34
  spec.add_development_dependency "bundler", ">= 1.17"
33
35
  spec.add_development_dependency "rake"
34
36
  spec.add_development_dependency "test-unit"
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: unicode_plot
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - mrkn
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-06-09 00:00:00.000000000 Z
11
+ date: 2019-06-14 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: enumerable-statistics
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 2.0.0.pre
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 2.0.0.pre
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: bundler
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -73,6 +87,7 @@ files:
73
87
  - lib/unicode_plot/plot.rb
74
88
  - lib/unicode_plot/renderer.rb
75
89
  - lib/unicode_plot/styled_printer.rb
90
+ - lib/unicode_plot/utils.rb
76
91
  - lib/unicode_plot/value_transformer.rb
77
92
  - lib/unicode_plot/version.rb
78
93
  - test/helper.rb
@@ -84,7 +99,7 @@ files:
84
99
  - test/test-canvas.rb
85
100
  - test/test-lineplot.rb
86
101
  - unicode_plot.gemspec
87
- homepage: https://github.com/mrkn/unicode_plot.rb
102
+ homepage: https://github.com/red-data-tools/unicode_plot.rb
88
103
  licenses:
89
104
  - MIT
90
105
  metadata: {}