unicode_plot 0.0.1 → 0.0.2

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
  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: {}