Skip to content

Commit 537ac48

Browse files
committed
Add scatterplot support
1 parent c7a7558 commit 537ac48

22 files changed

Lines changed: 574 additions & 107 deletions

lib/unicode_plot.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@
1010
require 'unicode_plot/braille_canvas'
1111

1212
require 'unicode_plot/plot'
13+
require 'unicode_plot/grid_plot'
1314

1415
require 'unicode_plot/barplot'
1516
require 'unicode_plot/boxplot'
1617
require 'unicode_plot/lineplot'
1718
require 'unicode_plot/histogram'
19+
require 'unicode_plot/scatterplot'

lib/unicode_plot/grid_plot.rb

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

lib/unicode_plot/lineplot.rb

Lines changed: 1 addition & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -1,111 +1,7 @@
11
require 'date'
22

33
module UnicodePlot
4-
class GridCanvas < Plot
5-
MIN_WIDTH = 5
6-
DEFAULT_WIDTH = 40
7-
MIN_HEIGHT = 2
8-
DEFAULT_HEIGHT = 15
9-
10-
def initialize(x, y, canvas,
11-
width: DEFAULT_WIDTH,
12-
height: DEFAULT_HEIGHT,
13-
xlim: [0, 0],
14-
ylim: [0, 0],
15-
grid: true,
16-
**kw)
17-
unless xlim.length == 2 && ylim.length == 2
18-
raise ArgumentError, "xlim and ylim must be 2-length arrays"
19-
end
20-
width = [width, MIN_WIDTH].max
21-
height = [height, MIN_HEIGHT].max
22-
min_x, max_x = Utils.extend_limits(x, xlim)
23-
min_y, max_y = Utils.extend_limits(y, ylim)
24-
origin_x = min_x
25-
origin_y = min_y
26-
plot_width = max_x - origin_x
27-
plot_height = max_y - origin_y
28-
@canvas = Canvas.create(canvas, width, height,
29-
origin_x: origin_x,
30-
origin_y: origin_y,
31-
plot_width: plot_width,
32-
plot_height: plot_height)
33-
super(**kw)
34-
35-
min_x_str = (Utils.roundable?(min_x) ? min_x.round : min_x).to_s
36-
max_x_str = (Utils.roundable?(max_x) ? max_x.round : max_x).to_s
37-
min_y_str = (Utils.roundable?(min_y) ? min_y.round : min_y).to_s
38-
max_y_str = (Utils.roundable?(max_y) ? max_y.round : max_y).to_s
39-
40-
annotate_row!(:l, 0, max_y_str, color: :light_black)
41-
annotate_row!(:l, height-1, min_y_str, color: :light_black)
42-
annotate!(:bl, min_x_str, color: :light_black)
43-
annotate!(:br, max_x_str, color: :light_black)
44-
45-
if grid
46-
if min_y < 0 && 0 < max_y
47-
step = plot_width.fdiv(width * @canvas.x_pixel_per_char - 1)
48-
min_x.step(max_x, by: step) do |i|
49-
@canvas.point!(i, 0, :normal)
50-
end
51-
end
52-
if min_x < 0 && 0 < max_x
53-
step = plot_height.fdiv(height * @canvas.y_pixel_per_char - 1)
54-
min_y.step(max_y, by: step) do |i|
55-
@canvas.point!(0, i, :normal)
56-
end
57-
end
58-
end
59-
end
60-
61-
def origin_x
62-
@canvas.origin_x
63-
end
64-
65-
def origin_y
66-
@canvas.origin_y
67-
end
68-
69-
def plot_width
70-
@canvas.plot_width
71-
end
72-
73-
def plot_height
74-
@canvas.plot_height
75-
end
76-
77-
def n_rows
78-
@canvas.height
79-
end
80-
81-
def n_columns
82-
@canvas.width
83-
end
84-
85-
def points!(x, y, color)
86-
@canvas.points!(x, y, color)
87-
end
88-
89-
def lines!(x, y, color)
90-
@canvas.lines!(x, y, color)
91-
end
92-
93-
def print_row(out, row_index)
94-
@canvas.print_row(out, row_index)
95-
end
96-
end
97-
98-
class Lineplot < GridCanvas
99-
def initialize(x, y, canvas,
100-
**kw)
101-
if x.length != y.length
102-
raise ArgumentError, "x and y must be the same length"
103-
end
104-
unless x.length > 0
105-
raise ArgumentError, "x and y must not be empty"
106-
end
107-
super(x, y, canvas, **kw)
108-
end
4+
class Lineplot < GridPlot
1095
end
1106

1117
module_function def lineplot(*args,

lib/unicode_plot/renderer.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ def init_render
220220
@border_padding = " " * @plot_offset
221221

222222
# compute position of ylabel
223-
@y_lab_row = (plot.n_rows / 2.0).round
223+
@y_lab_row = (plot.n_rows / 2.0).round - 1
224224
end
225225

226226
def print_title(padding, title, p_width: 0, color: :normal)

lib/unicode_plot/scatterplot.rb

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
module UnicodePlot
2+
class Scatterplot < GridPlot
3+
end
4+
5+
module_function def scatterplot(*args,
6+
canvas: :braille,
7+
color: :auto,
8+
name: "",
9+
**kw)
10+
case args.length
11+
when 1
12+
# y only
13+
y = Array(args[0])
14+
x = Array(1 .. y.length)
15+
when 2
16+
# x and y
17+
x = Array(args[0])
18+
y = Array(args[1])
19+
else
20+
raise ArgumentError, "worng number of arguments"
21+
end
22+
23+
plot = Scatterplot.new(x, y, canvas, **kw)
24+
scatterplot!(plot, x, y, color: color, name: name)
25+
end
26+
27+
module_function def scatterplot!(plot,
28+
*args,
29+
color: :auto,
30+
name: "")
31+
case args.length
32+
when 1
33+
# y only
34+
y = Array(args[0])
35+
x = Array(1 .. y.length)
36+
when 2
37+
# x and y
38+
x = Array(args[0])
39+
y = Array(args[1])
40+
else
41+
raise ArgumentError, "worng number of arguments"
42+
end
43+
44+
color = color == :auto ? plot.next_color : color
45+
plot.annotate!(:r, name.to_s, color: color) unless name.nil? || name == ""
46+
plot.points!(x, y, color)
47+
plot
48+
end
49+
end

lib/unicode_plot/utils.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,11 @@ def ceil_neg_log10(x)
6464
end
6565
end
6666

67+
INT64_MIN = -9223372036854775808
68+
INT64_MAX = 9223372036854775807
69+
6770
def roundable?(x)
68-
x.to_i == x
71+
x.to_i == x && INT64_MIN <= x && x < INT64_MAX
6972
end
7073
end
7174
end

test/fixtures/scatterplot/blue.txt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
 ┌────────────────────────────────────────┐
2+
2 │⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈│ points1
3+
 │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│
4+
 │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│
5+
 │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│
6+
 │⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⡗⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒│
7+
 │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│
8+
 │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│
9+
 │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│
10+
 │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│
11+
 │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│
12+
 │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│
13+
 │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│
14+
 │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│
15+
 │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│
16+
-5 │⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀│
17+
 └────────────────────────────────────────┘
18+
 -1  3
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
 Scatter
2+
 ┌──────────┐
3+
2 │' :      '│
4+
 │'':'''''''│
5+
 │  :       │
6+
 │  :       │
7+
-5 │. :      .│
8+
 └──────────┘
9+
 -1  3
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
 ┌────────────────────────────────────────┐
2+
2 │⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈│
3+
 │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│
4+
 │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│
5+
 │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│
6+
 │⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⡗⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒│
7+
 │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│
8+
 │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│
9+
 │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│
10+
 │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│
11+
 │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│
12+
 │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│
13+
 │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│
14+
 │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│
15+
 │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│
16+
-5 │⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀│
17+
 └────────────────────────────────────────┘
18+
 -1  3

0 commit comments

Comments
 (0)