unicode_plot 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7870c250eab339403fa566e04bb0361d0d9bc56f655e54dde037e559124d8956
4
+ data.tar.gz: 8a01e8f5466020b081418c14c373c8fbc6a0c9a8d860c056bbfc31ba8fe1f820
5
+ SHA512:
6
+ metadata.gz: 6548d8b2a016fb442407253908e1e57835c45e19cc9d2137dedcbbe1e52e41092f5864c3f36c322122b282c4f8d77d04e6e6afbc403b959b6819b8067eaccf1e
7
+ data.tar.gz: b813d2620b12c050c318868f02c7bf14bec7c3f44dedf9a5aeaaaaca5f98703a3b7cfab89f3fd45f7dffec2dfbe4f4ff9788b425c02501103fb41169a6ca3634
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org/"
2
+
3
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+ Copyright © 2019 Kenta Murata
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the “Software”), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in
12
+ all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # UnicodePlot - Plot your data by Unicode characters
2
+
3
+ UnicodePlot provides the feature to make charts with Unicode characters.
4
+
5
+ ## Install
6
+
7
+ ```console
8
+ $ gem install unicode_plot
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```
14
+ require 'unicode_plot'
15
+
16
+ x = 0.step(3*Math::PI, by: 3*Math::PI / 30)
17
+ y_sin = x.map {|xi| Math.sin(xi) }
18
+ y_cos = x.map {|xi| Math.cos(xi) }
19
+ plot = UnicodePlot.lineplot(x, y_sin, name: "sin(x)", width: 40, height: 10)
20
+ UnicodePlot.lineplot!(plot, x, y_cos, name: "cos(x)")
21
+ plot.render($stdout)
22
+ puts
23
+ ```
24
+
25
+ You can get the results below by running the above script:
26
+
27
+ <img src="img/lineplot.png" width="50%" />
28
+
29
+ ## Supported charts
30
+
31
+ - barplot
32
+ - lineplot
33
+
34
+ ## Acknowledgement
35
+
36
+ This library is strongly inspired by [UnicodePlot.jl](https://github.com/Evizero/UnicodePlots.jl).
37
+
38
+ ## License
39
+
40
+ MIT License
41
+
42
+ ## Author
43
+
44
+ - [Kenta Murata](https://github.com/mrkn)
data/Rakefile ADDED
@@ -0,0 +1,13 @@
1
+ require "bundler/gem_helper"
2
+
3
+ base_dir = File.expand_path("..", __FILE__)
4
+ helper = Bundler::GemHelper.new(base_dir)
5
+ helper.install
6
+ spec = helper.gemspec
7
+
8
+ desc "Run test"
9
+ task :test do
10
+ ruby("test/run-test.rb")
11
+ end
12
+
13
+ task default: :test
@@ -0,0 +1,14 @@
1
+ require 'unicode_plot/version'
2
+
3
+ require 'unicode_plot/styled_printer'
4
+ require 'unicode_plot/value_transformer'
5
+ require 'unicode_plot/renderer'
6
+
7
+ require 'unicode_plot/canvas'
8
+ require 'unicode_plot/ascii_canvas'
9
+ require 'unicode_plot/braille_canvas'
10
+
11
+ require 'unicode_plot/plot'
12
+
13
+ require 'unicode_plot/barplot'
14
+ require 'unicode_plot/lineplot'
@@ -0,0 +1,157 @@
1
+ module UnicodePlot
2
+ class AsciiCanvas < Canvas
3
+ ASCII_SIGNS = [
4
+ [ 0b100_000_000, 0b000_100_000, 0b000_000_100 ].freeze,
5
+ [ 0b010_000_000, 0b000_010_000, 0b000_000_010 ].freeze,
6
+ [ 0b001_000_000, 0b000_001_000, 0b000_000_001 ].freeze
7
+ ].freeze
8
+
9
+ ASCII_LOOKUP = {
10
+ 0b101_000_000 => '"',
11
+ 0b111_111_111 => '@',
12
+ #0b011_110_011 => '$',
13
+ 0b010_000_000 => '\'',
14
+ 0b010_100_010 => '(',
15
+ 0b010_001_010 => ')',
16
+ 0b000_010_000 => '*',
17
+ 0b010_111_010 => '+',
18
+ 0b000_010_010 => ',',
19
+ 0b000_100_100 => ',',
20
+ 0b000_001_001 => ',',
21
+ 0b000_111_000 => '-',
22
+ 0b000_000_010 => '.',
23
+ 0b000_000_100 => '.',
24
+ 0b000_000_001 => '.',
25
+ 0b001_010_100 => '/',
26
+ 0b010_100_000 => '/',
27
+ 0b001_010_110 => '/',
28
+ 0b011_010_010 => '/',
29
+ 0b001_010_010 => '/',
30
+ 0b110_010_111 => '1',
31
+ #0b111_010_100 => '7',
32
+ 0b010_000_010 => ':',
33
+ 0b111_000_111 => '=',
34
+ #0b010_111_101 => 'A',
35
+ #0b011_100_011 => 'C',
36
+ #0b110_101_110 => 'D',
37
+ #0b111_110_100 => 'F',
38
+ #0b011_101_011 => 'G',
39
+ #0b101_111_101 => 'H',
40
+ 0b111_010_111 => 'I',
41
+ #0b011_001_111 => 'J',
42
+ #0b101_110_101 => 'K',
43
+ 0b100_100_111 => 'L',
44
+ #0b111_111_101 => 'M',
45
+ #0b101_101_101 => 'N',
46
+ #0b111_101_111 => 'O',
47
+ #0b111_111_100 => 'P',
48
+ 0b111_010_010 => 'T',
49
+ #0b101_101_111 => 'U',
50
+ 0b101_101_010 => 'V',
51
+ #0b101_111_111 => 'W',
52
+ 0b101_010_101 => 'X',
53
+ 0b101_010_010 => 'Y',
54
+ 0b110_100_110 => '[',
55
+ 0b010_001_000 => '\\',
56
+ 0b100_010_001 => '\\',
57
+ 0b110_010_010 => '\\',
58
+ 0b100_010_011 => '\\',
59
+ 0b100_010_010 => '\\',
60
+ 0b011_001_011 => ']',
61
+ 0b010_101_000 => '^',
62
+ 0b000_000_111 => '_',
63
+ 0b100_000_000 => '`',
64
+ #0b000_111_111 => 'a',
65
+ #0b100_111_111 => 'b',
66
+ #0b001_111_111 => 'd',
67
+ #0b001_111_010 => 'f',
68
+ #0b100_111_101 => 'h',
69
+ #0b100_101_101 => 'k',
70
+ 0b110_010_011 => 'l',
71
+ #0b000_111_101 => 'n',
72
+ 0b000_111_100 => 'r',
73
+ #0b000_101_111 => 'u',
74
+ 0b000_101_010 => 'v',
75
+ 0b011_110_011 => '{',
76
+ 0b010_010_010 => '|',
77
+ 0b100_100_100 => '|',
78
+ 0b001_001_001 => '|',
79
+ 0b110_011_110 => '}',
80
+ }.freeze
81
+
82
+ ascii_lookup_key_order = [
83
+ 0x0002, 0x00d2, 0x0113, 0x00a0, 0x0088,
84
+ 0x002a, 0x0100, 0x0197, 0x0012, 0x0193,
85
+ 0x0092, 0x0082, 0x008a, 0x0054, 0x0004,
86
+ 0x01d2, 0x01ff, 0x0124, 0x00a8, 0x0056,
87
+ 0x0001, 0x01c7, 0x0052, 0x0080, 0x0009,
88
+ 0x00cb, 0x0007, 0x003c, 0x0111, 0x0140,
89
+ 0x0024, 0x0127, 0x0192, 0x0010, 0x019e,
90
+ 0x01a6, 0x01d7, 0x0155, 0x00a2, 0x00ba,
91
+ 0x0112, 0x0049, 0x00f3, 0x0152, 0x0038,
92
+ 0x016a
93
+ ]
94
+
95
+ ASCII_DECODE = [' ']
96
+
97
+ 1.upto(0b111_111_111) do |i|
98
+ min_key = ascii_lookup_key_order.min_by {|k| (i ^ k).digits(2).sum }
99
+ ASCII_DECODE[i] = ASCII_LOOKUP[min_key]
100
+ end
101
+
102
+ ASCII_DECODE.freeze
103
+
104
+ PIXEL_PER_CHAR = 3
105
+
106
+ def initialize(width, height, **kw)
107
+ super(width, height,
108
+ width * PIXEL_PER_CHAR,
109
+ height * PIXEL_PER_CHAR,
110
+ 0,
111
+ x_pixel_per_char: PIXEL_PER_CHAR,
112
+ y_pixel_per_char: PIXEL_PER_CHAR,
113
+ **kw)
114
+ end
115
+
116
+ def pixel!(pixel_x, pixel_y, color)
117
+ unless 0 <= pixel_x && pixel_x <= pixel_width &&
118
+ 0 <= pixel_y && pixel_y <= pixel_height
119
+ return color
120
+ end
121
+ pixel_x -= 1 unless pixel_x < pixel_width
122
+ pixel_y -= 1 unless pixel_y < pixel_height
123
+
124
+ tx = pixel_x.fdiv(pixel_width) * width
125
+ char_x = tx.floor + 1
126
+ char_x_off = pixel_x % PIXEL_PER_CHAR + 1
127
+ char_x += 1 if char_x < tx.round + 1 && char_x_off == 1
128
+
129
+ char_y = (pixel_y.fdiv(pixel_height) * height).floor + 1
130
+ char_y_off = pixel_y % PIXEL_PER_CHAR + 1
131
+
132
+ index = index_at(char_x - 1, char_y - 1)
133
+ if index
134
+ @grid[index] |= lookup_encode(char_x_off - 1, char_y_off - 1)
135
+ @colors[index] |= COLOR_ENCODE[color]
136
+ end
137
+ end
138
+
139
+ def print_row(out, row_index)
140
+ unless 0 <= row_index && row_index < height
141
+ raise ArgumentError, "row_index out of bounds"
142
+ end
143
+ y = row_index
144
+ (0 ... width).each do |x|
145
+ print_color(out, color_at(x, y), lookup_decode(char_at(x, y)))
146
+ end
147
+ end
148
+
149
+ def lookup_encode(x, y)
150
+ ASCII_SIGNS[x][y]
151
+ end
152
+
153
+ def lookup_decode(code)
154
+ ASCII_DECODE[code]
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,104 @@
1
+ module UnicodePlot
2
+ class Barplot < Plot
3
+ include ValueTransformer
4
+
5
+ MIN_WIDTH = 10
6
+ DEFAULT_WIDTH = 40
7
+ DEFAULT_COLOR = :green
8
+ DEFAULT_SYMBOL = "■"
9
+
10
+ def initialize(bars, width, color, symbol, transform, **kw)
11
+ if symbol.length > 1
12
+ raise ArgumentError, "symbol must be a single character"
13
+ end
14
+ @bars = bars
15
+ @symbol = symbol
16
+ @max_freq, i = find_max(transform_values(transform, bars))
17
+ @max_len = bars[i].to_s.length
18
+ @width = [width, max_len + 7, MIN_WIDTH].max
19
+ @color = color
20
+ @symbol = symbol
21
+ @transform = transform
22
+ super(**kw)
23
+ end
24
+
25
+ attr_reader :max_freq
26
+ attr_reader :max_len
27
+ attr_reader :width
28
+
29
+ def n_rows
30
+ @bars.length
31
+ end
32
+
33
+ def n_columns
34
+ @width
35
+ end
36
+
37
+ def print_row(out, row_index)
38
+ check_row_index(row_index)
39
+ bar = @bars[row_index]
40
+ max_bar_width = [width - 2 - max_len, 1].max
41
+ val = transform_values(@transform, bar)
42
+ bar_len = max_freq > 0 ?
43
+ ([val, 0.0].max.fdiv(max_freq) * max_bar_width).round :
44
+ 0
45
+ bar_str = max_freq > 0 ? @symbol * bar_len : ""
46
+ bar_lbl = bar.to_s
47
+ print_styled(out, bar_str, color: @color)
48
+ print_styled(out, " ", bar_lbl, color: :normal)
49
+ pan_len = [max_bar_width + 1 + max_len - bar_len - bar_lbl.length, 0].max
50
+ pad = " " * pan_len.round
51
+ out.print(pad)
52
+ end
53
+
54
+ private def find_max(values)
55
+ i = j = 0
56
+ max = values[i]
57
+ while j < values.length
58
+ if values[j] > max
59
+ i, max = j, values[j]
60
+ end
61
+ j += 1
62
+ end
63
+ [max, i]
64
+ end
65
+
66
+ private def check_row_index(row_index)
67
+ unless 0 <= row_index && row_index < n_rows
68
+ raise ArgumentError, "row_index is out of range"
69
+ end
70
+ end
71
+ end
72
+
73
+ module_function def barplot(*args,
74
+ width: Barplot::DEFAULT_WIDTH,
75
+ color: Barplot::DEFAULT_COLOR,
76
+ symbol: Barplot::DEFAULT_SYMBOL,
77
+ border: :barplot,
78
+ xscale: nil,
79
+ xlabel: nil,
80
+ data: nil,
81
+ **kw)
82
+ case args.length
83
+ when 0
84
+ data = Hash(args[0] || data)
85
+ keys = data.keys.map(&:to_s)
86
+ heights = data.values
87
+ when 2
88
+ keys = Array(args[0])
89
+ heights = Array(args[1])
90
+ else
91
+ raise ArgumentError, "invalid arguments"
92
+ end
93
+
94
+ xlabel ||= ValueTransformer.transform_name(xscale)
95
+ plot = Barplot.new(heights, width, color, symbol, xscale,
96
+ border: border, xlabel: xlabel,
97
+ **kw)
98
+ keys.each_with_index do |key, i|
99
+ plot.annotate_row!(:l, i, key)
100
+ end
101
+
102
+ plot
103
+ end
104
+ end
@@ -0,0 +1,28 @@
1
+ module UnicodePlot
2
+ class Boxplot < Plot
3
+ def initialize(
4
+ end
5
+
6
+ module_function def boxplot(*args,
7
+ data: nil,
8
+ border: :corners,
9
+ color: Boxplot::DEFAULT_COLOR,
10
+ width: Boxplot::DEFAULT_WIDTH,
11
+ xlim: [0, 0],
12
+ **kw)
13
+ case args.length
14
+ when 0
15
+ data = Hash(data)
16
+ text = data.keys
17
+ data = data.values
18
+ when 2
19
+ text, data = *args
20
+ text = Array(args[0])
21
+ data = Array(args[1])
22
+ else
23
+ raise ArgumentError, "wrong number of arguments"
24
+ end
25
+
26
+ plot = Boxplot.new(text, data)
27
+ end
28
+ end
@@ -0,0 +1,65 @@
1
+ module UnicodePlot
2
+ class BrailleCanvas < Canvas
3
+ X_PIXEL_PER_CHAR = 2
4
+ Y_PIXEL_PER_CHAR = 4
5
+
6
+ BRAILLE_SIGNS = [
7
+ [
8
+ 0x2801,
9
+ 0x2802,
10
+ 0x2804,
11
+ 0x2840,
12
+ ].freeze,
13
+ [
14
+ 0x2808,
15
+ 0x2810,
16
+ 0x2820,
17
+ 0x2880
18
+ ].freeze
19
+ ].freeze
20
+
21
+ def initialize(width, height, **kw)
22
+ super(width, height,
23
+ width * X_PIXEL_PER_CHAR,
24
+ height * Y_PIXEL_PER_CHAR,
25
+ "\u{2800}",
26
+ x_pixel_per_char: X_PIXEL_PER_CHAR,
27
+ y_pixel_per_char: Y_PIXEL_PER_CHAR,
28
+ **kw)
29
+ end
30
+
31
+ def pixel!(pixel_x, pixel_y, color)
32
+ unless 0 <= pixel_x && pixel_x <= pixel_width &&
33
+ 0 <= pixel_y && pixel_y <= pixel_height
34
+ return color
35
+ end
36
+ pixel_x -= 1 unless pixel_x < pixel_width
37
+ pixel_y -= 1 unless pixel_y < pixel_height
38
+ tx = pixel_x.fdiv(pixel_width) * width
39
+ char_x = tx.floor + 1
40
+ char_x_off = pixel_x % X_PIXEL_PER_CHAR + 1
41
+ char_x += 1 if char_x < tx.round + 1 && char_x_off == 1
42
+
43
+ char_y = (pixel_y.fdiv(pixel_height) * height).floor + 1
44
+ char_y_off = pixel_y % Y_PIXEL_PER_CHAR + 1
45
+
46
+ index = index_at(char_x - 1, char_y - 1)
47
+ if index
48
+ @grid[index] = (@grid[index].ord | BRAILLE_SIGNS[char_x_off - 1][char_y_off - 1]).chr(Encoding::UTF_8)
49
+ @colors[index] |= COLOR_ENCODE[color]
50
+ end
51
+ color
52
+ end
53
+
54
+ def print_row(out, row_index)
55
+ unless 0 <= row_index && row_index < height
56
+ $stderr.puts [row_index, height].inspect
57
+ raise ArgumentError, "row_index out of bounds"
58
+ end
59
+ y = row_index
60
+ (0 ... width).each do |x|
61
+ print_color(out, color_at(x, y), char_at(x, y))
62
+ end
63
+ end
64
+ end
65
+ end