unicode_plot 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +3 -0
- data/lib/unicode_plot/ascii_canvas.rb +2 -39
- data/lib/unicode_plot/barplot.rb +46 -2
- data/lib/unicode_plot/braille_canvas.rb +0 -1
- data/lib/unicode_plot/canvas.rb +12 -3
- data/lib/unicode_plot/density_canvas.rb +56 -0
- data/lib/unicode_plot/densityplot.rb +10 -0
- data/lib/unicode_plot/dot_canvas.rb +34 -0
- data/lib/unicode_plot/grid_plot.rb +101 -0
- data/lib/unicode_plot/histogram.rb +48 -0
- data/lib/unicode_plot/lineplot.rb +1 -105
- data/lib/unicode_plot/lookup_canvas.rb +46 -0
- data/lib/unicode_plot/renderer.rb +5 -5
- data/lib/unicode_plot/scatterplot.rb +49 -0
- data/lib/unicode_plot/utils.rb +14 -1
- data/lib/unicode_plot/version.rb +1 -1
- data/lib/unicode_plot.rb +8 -1
- data/test/test-barplot.rb +151 -20
- data/test/test-canvas.rb +15 -1
- data/test/test-densityplot.rb +38 -0
- data/test/test-histogram.rb +110 -0
- data/test/test-lineplot.rb +67 -0
- data/test/test-scatterplot.rb +146 -0
- metadata +15 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b33753adec2204f206dff50057dbf735be77c5d1596d8386b103058e76355ba2
|
4
|
+
data.tar.gz: 3209b35c716a2e8293735a6f774185f12c6627996707fb28c59fec3ac02debd7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dc5c517d8bdbce3d622767d03e4775365d51ac8a1e882e96d4aaba16c05345c66f0431be9f1eb79ad5cbcbfdfb92e395f06dba5f8f18c8a98b9f93491c25235d
|
7
|
+
data.tar.gz: b5aa69592cf09b140c1dff55edeafbc8204929fe99125dd67d31218286157f4beaa93051da151e5efddcefd24054ec08d458b1aeeee0ac0acabe26813b348b48
|
data/README.md
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
module UnicodePlot
|
2
|
-
class AsciiCanvas <
|
2
|
+
class AsciiCanvas < LookupCanvas
|
3
3
|
ASCII_SIGNS = [
|
4
4
|
[ 0b100_000_000, 0b000_100_000, 0b000_000_100 ].freeze,
|
5
5
|
[ 0b010_000_000, 0b000_010_000, 0b000_000_010 ].freeze,
|
@@ -105,47 +105,10 @@ module UnicodePlot
|
|
105
105
|
|
106
106
|
def initialize(width, height, **kw)
|
107
107
|
super(width, height,
|
108
|
-
|
109
|
-
height * PIXEL_PER_CHAR,
|
110
|
-
0,
|
111
|
-
x_pixel_per_char: PIXEL_PER_CHAR,
|
112
|
-
y_pixel_per_char: PIXEL_PER_CHAR,
|
108
|
+
PIXEL_PER_CHAR, PIXEL_PER_CHAR,
|
113
109
|
**kw)
|
114
110
|
end
|
115
111
|
|
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
112
|
def lookup_encode(x, y)
|
150
113
|
ASCII_SIGNS[x][y]
|
151
114
|
end
|
data/lib/unicode_plot/barplot.rb
CHANGED
@@ -34,13 +34,19 @@ module UnicodePlot
|
|
34
34
|
@width
|
35
35
|
end
|
36
36
|
|
37
|
+
def add_row!(bars)
|
38
|
+
@bars.concat(bars)
|
39
|
+
@max_freq, i = find_max(transform_values(@transform, bars))
|
40
|
+
@max_len = @bars[i].to_s.length
|
41
|
+
end
|
42
|
+
|
37
43
|
def print_row(out, row_index)
|
38
44
|
check_row_index(row_index)
|
39
45
|
bar = @bars[row_index]
|
40
46
|
max_bar_width = [width - 2 - max_len, 1].max
|
41
47
|
val = transform_values(@transform, bar)
|
42
48
|
bar_len = max_freq > 0 ?
|
43
|
-
([val, 0
|
49
|
+
([val, 0].max.fdiv(max_freq) * max_bar_width).round :
|
44
50
|
0
|
45
51
|
bar_str = max_freq > 0 ? @symbol * bar_len : ""
|
46
52
|
bar_lbl = bar.to_s
|
@@ -81,7 +87,7 @@ module UnicodePlot
|
|
81
87
|
**kw)
|
82
88
|
case args.length
|
83
89
|
when 0
|
84
|
-
data = Hash(
|
90
|
+
data = Hash(data)
|
85
91
|
keys = data.keys.map(&:to_s)
|
86
92
|
heights = data.values
|
87
93
|
when 2
|
@@ -91,6 +97,13 @@ module UnicodePlot
|
|
91
97
|
raise ArgumentError, "invalid arguments"
|
92
98
|
end
|
93
99
|
|
100
|
+
unless keys.length == heights.length
|
101
|
+
raise ArgumentError, "The given vectors must be of the same length"
|
102
|
+
end
|
103
|
+
unless heights.min >= 0
|
104
|
+
raise ArgumentError, "All values have to be positive. Negative bars are not supported."
|
105
|
+
end
|
106
|
+
|
94
107
|
xlabel ||= ValueTransformer.transform_name(xscale)
|
95
108
|
plot = Barplot.new(heights, width, color, symbol, xscale,
|
96
109
|
border: border, xlabel: xlabel,
|
@@ -101,4 +114,35 @@ module UnicodePlot
|
|
101
114
|
|
102
115
|
plot
|
103
116
|
end
|
117
|
+
|
118
|
+
module_function def barplot!(plot,
|
119
|
+
*args,
|
120
|
+
data: nil,
|
121
|
+
**kw)
|
122
|
+
case args.length
|
123
|
+
when 0
|
124
|
+
data = Hash(data)
|
125
|
+
keys = data.keys.map(&:to_s)
|
126
|
+
heights = data.values
|
127
|
+
when 2
|
128
|
+
keys = Array(args[0])
|
129
|
+
heights = Array(args[1])
|
130
|
+
else
|
131
|
+
raise ArgumentError, "invalid arguments"
|
132
|
+
end
|
133
|
+
|
134
|
+
unless keys.length == heights.length
|
135
|
+
raise ArgumentError, "The given vectors must be of the same length"
|
136
|
+
end
|
137
|
+
if keys.empty?
|
138
|
+
raise ArgumentError, "Can't append empty array to barplot"
|
139
|
+
end
|
140
|
+
|
141
|
+
cur_idx = plot.n_rows
|
142
|
+
plot.add_row!(heights)
|
143
|
+
keys.each_with_index do |key, i|
|
144
|
+
plot.annotate_row!(:l, cur_idx + i, key)
|
145
|
+
end
|
146
|
+
plot
|
147
|
+
end
|
104
148
|
end
|
data/lib/unicode_plot/canvas.rb
CHANGED
@@ -8,6 +8,10 @@ module UnicodePlot
|
|
8
8
|
AsciiCanvas.new(width, height, **kw)
|
9
9
|
when :braille
|
10
10
|
BrailleCanvas.new(width, height, **kw)
|
11
|
+
when :density
|
12
|
+
DensityCanvas.new(width, height, **kw)
|
13
|
+
when :dot
|
14
|
+
DotCanvas.new(width, height, **kw)
|
11
15
|
else
|
12
16
|
raise ArgumentError, "unknown canvas type: #{canvas_type}"
|
13
17
|
end
|
@@ -22,8 +26,8 @@ module UnicodePlot
|
|
22
26
|
y_pixel_per_char: 1)
|
23
27
|
@width = width
|
24
28
|
@height = height
|
25
|
-
@pixel_width = pixel_width
|
26
|
-
@pixel_height = pixel_height
|
29
|
+
@pixel_width = check_positive(pixel_width, :pixel_width)
|
30
|
+
@pixel_height = check_positive(pixel_height, :pixel_height)
|
27
31
|
@origin_x = origin_x
|
28
32
|
@origin_y = origin_y
|
29
33
|
@plot_width = plot_width
|
@@ -83,7 +87,7 @@ module UnicodePlot
|
|
83
87
|
def point!(x, y, color)
|
84
88
|
unless origin_x <= x && x <= origin_x + plot_width &&
|
85
89
|
origin_y <= y && y <= origin_y + plot_height
|
86
|
-
return
|
90
|
+
return color
|
87
91
|
end
|
88
92
|
|
89
93
|
plot_offset_x = x - origin_x
|
@@ -156,5 +160,10 @@ module UnicodePlot
|
|
156
160
|
line!(x[i], y[i], x[i+1], y[i+1], color)
|
157
161
|
end
|
158
162
|
end
|
163
|
+
|
164
|
+
private def check_positive(value, name)
|
165
|
+
return value if value > 0
|
166
|
+
raise ArgumentError, "#{name} has to be positive"
|
167
|
+
end
|
159
168
|
end
|
160
169
|
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module UnicodePlot
|
2
|
+
class DensityCanvas < Canvas
|
3
|
+
DENSITY_SIGNS = [" ", "░", "▒", "▓", "█"].freeze
|
4
|
+
|
5
|
+
MIN_WIDTH = 5
|
6
|
+
MIN_HEIGHT = 5
|
7
|
+
|
8
|
+
X_PIXEL_PER_CHAR = 1
|
9
|
+
Y_PIXEL_PER_CHAR = 2
|
10
|
+
|
11
|
+
def initialize(width, height, **kw)
|
12
|
+
width = [width, MIN_WIDTH].max
|
13
|
+
height = [height, MIN_HEIGHT].max
|
14
|
+
@max_density = 1
|
15
|
+
super(width, height,
|
16
|
+
width * X_PIXEL_PER_CHAR,
|
17
|
+
height * Y_PIXEL_PER_CHAR,
|
18
|
+
0,
|
19
|
+
x_pixel_per_char: X_PIXEL_PER_CHAR,
|
20
|
+
y_pixel_per_char: Y_PIXEL_PER_CHAR,
|
21
|
+
**kw)
|
22
|
+
end
|
23
|
+
|
24
|
+
def pixel!(pixel_x, pixel_y, color)
|
25
|
+
unless 0 <= pixel_x && pixel_x <= pixel_width &&
|
26
|
+
0 <= pixel_y && pixel_y <= pixel_height
|
27
|
+
return color
|
28
|
+
end
|
29
|
+
|
30
|
+
pixel_x -= 1 unless pixel_x < pixel_width
|
31
|
+
pixel_y -= 1 unless pixel_y < pixel_height
|
32
|
+
|
33
|
+
char_x = (pixel_x.fdiv(pixel_width) * width).floor
|
34
|
+
char_y = (pixel_y.fdiv(pixel_height) * height).floor
|
35
|
+
|
36
|
+
index = index_at(char_x, char_y)
|
37
|
+
@grid[index] += 1
|
38
|
+
@max_density = [@max_density, @grid[index]].max
|
39
|
+
@colors[index] |= COLOR_ENCODE[color]
|
40
|
+
color
|
41
|
+
end
|
42
|
+
|
43
|
+
def print_row(out, row_index)
|
44
|
+
unless 0 <= row_index && row_index < height
|
45
|
+
raise ArgumentError, "row_index out of bounds"
|
46
|
+
end
|
47
|
+
y = row_index
|
48
|
+
den_sign_count = DENSITY_SIGNS.length
|
49
|
+
val_scale = (den_sign_count - 1).fdiv(@max_density)
|
50
|
+
(0 ... width).each do |x|
|
51
|
+
den_index = (char_at(x, y) * val_scale).round
|
52
|
+
print_color(out, color_at(x, y), DENSITY_SIGNS[den_index])
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module UnicodePlot
|
2
|
+
module_function def densityplot(x, y, color: :auto, grid: false, name: "", **kw)
|
3
|
+
plot = GridPlot.new(x, y, :density, grid: grid, **kw)
|
4
|
+
scatterplot!(plot, x, y, color: color, name: name)
|
5
|
+
end
|
6
|
+
|
7
|
+
module_function def densityplot!(plot, x, y, **kw)
|
8
|
+
scatterplot!(plot, x, y, **kw)
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module UnicodePlot
|
2
|
+
class DotCanvas < LookupCanvas
|
3
|
+
DOT_SIGNS = [
|
4
|
+
[
|
5
|
+
0b10,
|
6
|
+
0b01
|
7
|
+
].freeze
|
8
|
+
].freeze
|
9
|
+
|
10
|
+
DOT_DECODE = [
|
11
|
+
-' ', # 0b00
|
12
|
+
-'.', # 0b01
|
13
|
+
-"'", # 0b10
|
14
|
+
-':', # 0b11
|
15
|
+
].freeze
|
16
|
+
|
17
|
+
X_PIXEL_PER_CHAR = 1
|
18
|
+
Y_PIXEL_PER_CHAR = 2
|
19
|
+
|
20
|
+
def initialize(width, height, **kw)
|
21
|
+
super(width, height,
|
22
|
+
X_PIXEL_PER_CHAR, Y_PIXEL_PER_CHAR,
|
23
|
+
**kw)
|
24
|
+
end
|
25
|
+
|
26
|
+
def lookup_encode(x, y)
|
27
|
+
DOT_SIGNS[x][y]
|
28
|
+
end
|
29
|
+
|
30
|
+
def lookup_decode(code)
|
31
|
+
DOT_DECODE[code]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module UnicodePlot
|
2
|
+
class GridPlot < Plot
|
3
|
+
MIN_WIDTH = 5
|
4
|
+
DEFAULT_WIDTH = 40
|
5
|
+
MIN_HEIGHT = 2
|
6
|
+
DEFAULT_HEIGHT = 15
|
7
|
+
|
8
|
+
def initialize(x, y, canvas,
|
9
|
+
width: DEFAULT_WIDTH,
|
10
|
+
height: DEFAULT_HEIGHT,
|
11
|
+
xlim: [0, 0],
|
12
|
+
ylim: [0, 0],
|
13
|
+
grid: true,
|
14
|
+
**kw)
|
15
|
+
if x.length != y.length
|
16
|
+
raise ArgumentError, "x and y must be the same length"
|
17
|
+
end
|
18
|
+
unless x.length > 0
|
19
|
+
raise ArgumentError, "x and y must not be empty"
|
20
|
+
end
|
21
|
+
unless xlim.length == 2 && ylim.length == 2
|
22
|
+
raise ArgumentError, "xlim and ylim must be 2-length arrays"
|
23
|
+
end
|
24
|
+
width = [width, MIN_WIDTH].max
|
25
|
+
height = [height, MIN_HEIGHT].max
|
26
|
+
min_x, max_x = Utils.extend_limits(x, xlim)
|
27
|
+
min_y, max_y = Utils.extend_limits(y, ylim)
|
28
|
+
origin_x = min_x
|
29
|
+
origin_y = min_y
|
30
|
+
plot_width = max_x - origin_x
|
31
|
+
plot_height = max_y - origin_y
|
32
|
+
@canvas = Canvas.create(canvas, width, height,
|
33
|
+
origin_x: origin_x,
|
34
|
+
origin_y: origin_y,
|
35
|
+
plot_width: plot_width,
|
36
|
+
plot_height: plot_height)
|
37
|
+
super(**kw)
|
38
|
+
|
39
|
+
min_x_str = (Utils.roundable?(min_x) ? min_x.round : min_x).to_s
|
40
|
+
max_x_str = (Utils.roundable?(max_x) ? max_x.round : max_x).to_s
|
41
|
+
min_y_str = (Utils.roundable?(min_y) ? min_y.round : min_y).to_s
|
42
|
+
max_y_str = (Utils.roundable?(max_y) ? max_y.round : max_y).to_s
|
43
|
+
|
44
|
+
annotate_row!(:l, 0, max_y_str, color: :light_black)
|
45
|
+
annotate_row!(:l, height-1, min_y_str, color: :light_black)
|
46
|
+
annotate!(:bl, min_x_str, color: :light_black)
|
47
|
+
annotate!(:br, max_x_str, color: :light_black)
|
48
|
+
|
49
|
+
if grid
|
50
|
+
if min_y < 0 && 0 < max_y
|
51
|
+
step = plot_width.fdiv(width * @canvas.x_pixel_per_char - 1)
|
52
|
+
min_x.step(max_x, by: step) do |i|
|
53
|
+
@canvas.point!(i, 0, :normal)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
if min_x < 0 && 0 < max_x
|
57
|
+
step = plot_height.fdiv(height * @canvas.y_pixel_per_char - 1)
|
58
|
+
min_y.step(max_y, by: step) do |i|
|
59
|
+
@canvas.point!(0, i, :normal)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def origin_x
|
66
|
+
@canvas.origin_x
|
67
|
+
end
|
68
|
+
|
69
|
+
def origin_y
|
70
|
+
@canvas.origin_y
|
71
|
+
end
|
72
|
+
|
73
|
+
def plot_width
|
74
|
+
@canvas.plot_width
|
75
|
+
end
|
76
|
+
|
77
|
+
def plot_height
|
78
|
+
@canvas.plot_height
|
79
|
+
end
|
80
|
+
|
81
|
+
def n_rows
|
82
|
+
@canvas.height
|
83
|
+
end
|
84
|
+
|
85
|
+
def n_columns
|
86
|
+
@canvas.width
|
87
|
+
end
|
88
|
+
|
89
|
+
def points!(x, y, color)
|
90
|
+
@canvas.points!(x, y, color)
|
91
|
+
end
|
92
|
+
|
93
|
+
def lines!(x, y, color)
|
94
|
+
@canvas.lines!(x, y, color)
|
95
|
+
end
|
96
|
+
|
97
|
+
def print_row(out, row_index)
|
98
|
+
@canvas.print_row(out, row_index)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'enumerable/statistics'
|
2
|
+
|
3
|
+
module UnicodePlot
|
4
|
+
module_function def histogram(x,
|
5
|
+
nbins: nil,
|
6
|
+
closed: :left,
|
7
|
+
symbol: "▇",
|
8
|
+
**kw)
|
9
|
+
hist = x.histogram(*[nbins].compact, closed: closed)
|
10
|
+
edge, counts = hist.edge, hist.weights
|
11
|
+
labels = []
|
12
|
+
bin_width = edge[1] - edge[0]
|
13
|
+
pad_left, pad_right = 0, 0
|
14
|
+
(0 ... edge.length).each do |i|
|
15
|
+
val1 = Utils.float_round_log10(edge[i], bin_width)
|
16
|
+
val2 = Utils.float_round_log10(val1 + bin_width, bin_width)
|
17
|
+
a1 = val1.to_s.split('.', 2).map(&:length)
|
18
|
+
a2 = val2.to_s.split('.', 2).map(&:length)
|
19
|
+
pad_left = [pad_left, a1[0], a2[0]].max
|
20
|
+
pad_right = [pad_right, a1[1], a2[1]].max
|
21
|
+
end
|
22
|
+
l_str = hist.closed == :right ? "(" : "["
|
23
|
+
r_str = hist.closed == :right ? "]" : ")"
|
24
|
+
counts.each_with_index do |n, i|
|
25
|
+
val1 = Utils.float_round_log10(edge[i], bin_width)
|
26
|
+
val2 = Utils.float_round_log10(val1 + bin_width, bin_width)
|
27
|
+
a1 = val1.to_s.split('.', 2).map(&:length)
|
28
|
+
a2 = val2.to_s.split('.', 2).map(&:length)
|
29
|
+
labels[i] = "\e[90m#{l_str}\e[0m" +
|
30
|
+
(" " * (pad_left - a1[0])) +
|
31
|
+
val1.to_s +
|
32
|
+
(" " * (pad_right - a1[1])) +
|
33
|
+
"\e[90m, \e[0m" +
|
34
|
+
(" " * (pad_left - a2[0])) +
|
35
|
+
val2.to_s +
|
36
|
+
(" " * (pad_right - a2[1])) +
|
37
|
+
"\e[90m#{r_str}\e[0m"
|
38
|
+
end
|
39
|
+
xscale = kw.delete(:xscale)
|
40
|
+
xlabel = kw.delete(:xlabel) ||
|
41
|
+
ValueTransformer.transform_name(xscale, "Frequency")
|
42
|
+
barplot(labels, counts,
|
43
|
+
symbol: symbol,
|
44
|
+
xscale: xscale,
|
45
|
+
xlabel: xlabel,
|
46
|
+
**kw)
|
47
|
+
end
|
48
|
+
end
|