unicode_plot 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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