unicode_plot 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +20 -0
- data/README.md +44 -0
- data/Rakefile +13 -0
- data/lib/unicode_plot.rb +14 -0
- data/lib/unicode_plot/ascii_canvas.rb +157 -0
- data/lib/unicode_plot/barplot.rb +104 -0
- data/lib/unicode_plot/boxplot.rb +28 -0
- data/lib/unicode_plot/braille_canvas.rb +65 -0
- data/lib/unicode_plot/canvas.rb +160 -0
- data/lib/unicode_plot/lineplot.rb +249 -0
- data/lib/unicode_plot/plot.rb +139 -0
- data/lib/unicode_plot/renderer.rb +234 -0
- data/lib/unicode_plot/styled_printer.rb +91 -0
- data/lib/unicode_plot/value_transformer.rb +43 -0
- data/lib/unicode_plot/version.rb +9 -0
- data/test/helper.rb +2 -0
- data/test/helper/fixture.rb +14 -0
- data/test/helper/with_term.rb +23 -0
- data/test/run-test.rb +12 -0
- data/test/test-barplot.rb +59 -0
- data/test/test-boxplot.rb +48 -0
- data/test/test-canvas.rb +124 -0
- data/test/test-lineplot.rb +132 -0
- data/unicode_plot.gemspec +35 -0
- metadata +118 -0
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
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
|
data/lib/unicode_plot.rb
ADDED
@@ -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
|