unicode_plot 0.0.2 → 0.0.3
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 +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
|