charty 0.2.0 → 0.2.6
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/.github/workflows/ci.yml +71 -0
- data/.github/workflows/nmatrix.yml +67 -0
- data/.github/workflows/pycall.yml +86 -0
- data/Dockerfile.dev +9 -1
- data/Gemfile +18 -0
- data/README.md +177 -9
- data/Rakefile +4 -5
- data/charty.gemspec +10 -4
- data/examples/Gemfile +1 -0
- data/examples/active_record.ipynb +1 -1
- data/examples/daru.ipynb +1 -1
- data/examples/iris_dataset.ipynb +1 -1
- data/examples/nmatrix.ipynb +1 -1
- data/examples/{numo-narray.ipynb → numo_narray.ipynb} +1 -1
- data/examples/palette.rb +71 -0
- data/examples/sample.png +0 -0
- data/examples/sample_images/hist_gruff.png +0 -0
- data/examples/sample_pyplot.ipynb +40 -38
- data/images/penguins_body_mass_g_flipper_length_mm_scatter_plot.png +0 -0
- data/images/penguins_body_mass_g_flipper_length_mm_species_scatter_plot.png +0 -0
- data/images/penguins_body_mass_g_flipper_length_mm_species_sex_scatter_plot.png +0 -0
- data/images/penguins_species_body_mass_g_bar_plot_h.png +0 -0
- data/images/penguins_species_body_mass_g_bar_plot_v.png +0 -0
- data/images/penguins_species_body_mass_g_box_plot_h.png +0 -0
- data/images/penguins_species_body_mass_g_box_plot_v.png +0 -0
- data/images/penguins_species_body_mass_g_sex_bar_plot_v.png +0 -0
- data/images/penguins_species_body_mass_g_sex_box_plot_v.png +0 -0
- data/lib/charty.rb +13 -1
- data/lib/charty/backend_methods.rb +8 -0
- data/lib/charty/backends.rb +26 -1
- data/lib/charty/backends/bokeh.rb +31 -31
- data/lib/charty/backends/{google_chart.rb → google_charts.rb} +75 -33
- data/lib/charty/backends/gruff.rb +14 -3
- data/lib/charty/backends/plotly.rb +774 -9
- data/lib/charty/backends/pyplot.rb +611 -34
- data/lib/charty/backends/rubyplot.rb +2 -2
- data/lib/charty/backends/unicode_plot.rb +79 -0
- data/lib/charty/dash_pattern_generator.rb +57 -0
- data/lib/charty/index.rb +213 -0
- data/lib/charty/linspace.rb +1 -1
- data/lib/charty/plot_methods.rb +254 -0
- data/lib/charty/plotter.rb +10 -10
- data/lib/charty/plotters.rb +12 -0
- data/lib/charty/plotters/abstract_plotter.rb +243 -0
- data/lib/charty/plotters/bar_plotter.rb +201 -0
- data/lib/charty/plotters/box_plotter.rb +79 -0
- data/lib/charty/plotters/categorical_plotter.rb +380 -0
- data/lib/charty/plotters/count_plotter.rb +7 -0
- data/lib/charty/plotters/estimation_support.rb +84 -0
- data/lib/charty/plotters/line_plotter.rb +300 -0
- data/lib/charty/plotters/random_support.rb +25 -0
- data/lib/charty/plotters/relational_plotter.rb +635 -0
- data/lib/charty/plotters/scatter_plotter.rb +80 -0
- data/lib/charty/plotters/vector_plotter.rb +6 -0
- data/lib/charty/statistics.rb +114 -0
- data/lib/charty/table.rb +161 -15
- data/lib/charty/table_adapters.rb +2 -0
- data/lib/charty/table_adapters/active_record_adapter.rb +17 -9
- data/lib/charty/table_adapters/base_adapter.rb +166 -0
- data/lib/charty/table_adapters/daru_adapter.rb +41 -3
- data/lib/charty/table_adapters/datasets_adapter.rb +17 -2
- data/lib/charty/table_adapters/hash_adapter.rb +143 -16
- data/lib/charty/table_adapters/narray_adapter.rb +25 -6
- data/lib/charty/table_adapters/nmatrix_adapter.rb +15 -5
- data/lib/charty/table_adapters/pandas_adapter.rb +163 -0
- data/lib/charty/util.rb +28 -0
- data/lib/charty/vector.rb +69 -0
- data/lib/charty/vector_adapters.rb +187 -0
- data/lib/charty/vector_adapters/array_adapter.rb +101 -0
- data/lib/charty/vector_adapters/daru_adapter.rb +163 -0
- data/lib/charty/vector_adapters/narray_adapter.rb +182 -0
- data/lib/charty/vector_adapters/nmatrix_adapter.rb +37 -0
- data/lib/charty/vector_adapters/numpy_adapter.rb +168 -0
- data/lib/charty/vector_adapters/pandas_adapter.rb +199 -0
- data/lib/charty/version.rb +1 -1
- metadata +121 -22
- data/.travis.yml +0 -10
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
data/lib/charty.rb
CHANGED
@@ -1,8 +1,20 @@
|
|
1
1
|
require_relative "charty/version"
|
2
2
|
|
3
|
+
require "colors"
|
4
|
+
require "palette"
|
5
|
+
|
6
|
+
require_relative "charty/util"
|
7
|
+
require_relative "charty/dash_pattern_generator"
|
3
8
|
require_relative "charty/backends"
|
9
|
+
require_relative "charty/backend_methods"
|
4
10
|
require_relative "charty/plotter"
|
11
|
+
require_relative "charty/index"
|
5
12
|
require_relative "charty/layout"
|
6
13
|
require_relative "charty/linspace"
|
7
|
-
require_relative "charty/
|
14
|
+
require_relative "charty/plotters"
|
15
|
+
require_relative "charty/plot_methods"
|
8
16
|
require_relative "charty/table"
|
17
|
+
require_relative "charty/table_adapters"
|
18
|
+
require_relative "charty/statistics"
|
19
|
+
require_relative "charty/vector_adapters"
|
20
|
+
require_relative "charty/vector"
|
data/lib/charty/backends.rb
CHANGED
@@ -6,6 +6,30 @@ module Charty
|
|
6
6
|
module Backends
|
7
7
|
@backends = {}
|
8
8
|
|
9
|
+
@current = nil
|
10
|
+
|
11
|
+
def self.current
|
12
|
+
@current
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.current=(backend_name)
|
16
|
+
backend_class = Backends.find_backend_class(backend_name)
|
17
|
+
@current = backend_class.new
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.use(backend)
|
21
|
+
if block_given?
|
22
|
+
begin
|
23
|
+
saved, self.current = self.current, backend
|
24
|
+
yield
|
25
|
+
ensure
|
26
|
+
self.current = saved
|
27
|
+
end
|
28
|
+
else
|
29
|
+
self.current = backend
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
9
33
|
def self.names
|
10
34
|
@backends.keys
|
11
35
|
end
|
@@ -48,8 +72,9 @@ module Charty
|
|
48
72
|
end
|
49
73
|
|
50
74
|
require "charty/backends/bokeh"
|
51
|
-
require "charty/backends/
|
75
|
+
require "charty/backends/google_charts"
|
52
76
|
require "charty/backends/gruff"
|
53
77
|
require "charty/backends/plotly"
|
54
78
|
require "charty/backends/pyplot"
|
55
79
|
require "charty/backends/rubyplot"
|
80
|
+
require "charty/backends/unicode_plot"
|
@@ -17,13 +17,13 @@ module Charty
|
|
17
17
|
@series = series
|
18
18
|
end
|
19
19
|
|
20
|
-
def
|
20
|
+
def old_style_render(context, filename)
|
21
21
|
plot = plot(context)
|
22
22
|
save(plot, context, filename)
|
23
23
|
PyCall.import_module('bokeh.io').show(plot)
|
24
24
|
end
|
25
25
|
|
26
|
-
def
|
26
|
+
def old_style_save(plot, context, filename)
|
27
27
|
if filename
|
28
28
|
PyCall.import_module('bokeh.io').save(plot, filename)
|
29
29
|
end
|
@@ -37,42 +37,42 @@ module Charty
|
|
37
37
|
plot.yaxis[0].axis_label = context&.ylabel
|
38
38
|
|
39
39
|
case context.method
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
40
|
+
when :bar
|
41
|
+
context.series.each do |data|
|
42
|
+
diffs = data.xs.to_a.each_cons(2).map {|n, m| (n - m).abs }
|
43
|
+
width = diffs.min * 0.8
|
44
|
+
plot.vbar(data.xs.to_a, width, data.ys.to_a)
|
45
|
+
end
|
46
46
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
47
|
+
when :barh
|
48
|
+
context.series.each do |data|
|
49
|
+
diffs = data.xs.to_a.each_cons(2).map {|n, m| (n - m).abs }
|
50
|
+
height = diffs.min * 0.8
|
51
|
+
plot.hbar(data.xs.to_a, height, data.ys.to_a)
|
52
|
+
end
|
53
53
|
|
54
|
-
|
55
|
-
|
54
|
+
when :boxplot
|
55
|
+
raise NotImplementedError
|
56
56
|
|
57
|
-
|
58
|
-
|
57
|
+
when :bubble
|
58
|
+
raise NotImplementedError
|
59
59
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
60
|
+
when :curve
|
61
|
+
context.series.each do |data|
|
62
|
+
plot.line(data.xs.to_a, data.ys.to_a)
|
63
|
+
end
|
64
64
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
65
|
+
when :scatter
|
66
|
+
context.series.each do |data|
|
67
|
+
plot.scatter(data.xs.to_a, data.ys.to_a)
|
68
|
+
end
|
69
69
|
|
70
|
-
|
71
|
-
|
70
|
+
when :error_bar
|
71
|
+
raise NotImplementedError
|
72
72
|
|
73
|
-
|
74
|
-
|
75
|
-
|
73
|
+
when :hist
|
74
|
+
raise NotImplementedError
|
75
|
+
end
|
76
76
|
plot
|
77
77
|
end
|
78
78
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
module Charty
|
2
2
|
module Backends
|
3
|
-
class
|
4
|
-
Backends.register(:
|
3
|
+
class GoogleCharts
|
4
|
+
Backends.register(:google_charts, self)
|
5
5
|
|
6
6
|
attr_reader :context
|
7
7
|
|
@@ -33,7 +33,7 @@ module Charty
|
|
33
33
|
@series = series
|
34
34
|
end
|
35
35
|
|
36
|
-
def
|
36
|
+
def old_style_render(context, filename)
|
37
37
|
plot(nil, context)
|
38
38
|
end
|
39
39
|
|
@@ -70,47 +70,65 @@ module Charty
|
|
70
70
|
def data_column_js
|
71
71
|
case context.method
|
72
72
|
when :bubble
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
73
|
+
schema = [
|
74
|
+
["string", "ID"],
|
75
|
+
["number", "X"],
|
76
|
+
["number", "Y"],
|
77
|
+
["string", "GROUP"],
|
78
|
+
["number", "SIZE"],
|
79
|
+
]
|
80
80
|
when :curve
|
81
|
-
|
81
|
+
schema = []
|
82
|
+
schema << [detect_type(context.series.first.xs), context.xlabel]
|
82
83
|
context.series.to_a.each_with_index do |series_data, index|
|
83
|
-
|
84
|
+
schema << ["number", series_data.label || index]
|
84
85
|
end
|
85
86
|
else
|
86
|
-
|
87
|
+
schema = ["string", context.xlabel]
|
87
88
|
context.series.to_a.each_with_index do |series_data, index|
|
88
|
-
|
89
|
+
schema << ["number", series_data.label || index]
|
89
90
|
end
|
90
91
|
end
|
91
92
|
|
92
|
-
|
93
|
+
columns = schema.collect do |type, label|
|
94
|
+
"data.addColumn(#{type.to_json}, #{label.to_s.to_json});"
|
95
|
+
end
|
96
|
+
columns.join
|
97
|
+
end
|
98
|
+
|
99
|
+
def detect_type(values)
|
100
|
+
case values.first
|
101
|
+
when Time
|
102
|
+
"date"
|
103
|
+
when String
|
104
|
+
"string"
|
105
|
+
else
|
106
|
+
"number"
|
107
|
+
end
|
93
108
|
end
|
94
109
|
|
95
110
|
def x_labels
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
xs_series.each do |xs_data|
|
104
|
-
label << xs_data unless label.any? { |label| label == xs_data }
|
105
|
-
end
|
111
|
+
labels = {}
|
112
|
+
have_string = false
|
113
|
+
context.series.each do |series|
|
114
|
+
series.xs.each do |x|
|
115
|
+
next if labels.key?(x)
|
116
|
+
have_string = true if x.is_a?(String)
|
117
|
+
labels[x] = true
|
106
118
|
end
|
107
119
|
end
|
120
|
+
if have_string
|
121
|
+
labels.keys.sort_by {|label| "%10s" % x.to_s}
|
122
|
+
else
|
123
|
+
labels.keys.sort
|
124
|
+
end
|
108
125
|
end
|
109
126
|
|
110
127
|
def data_hash
|
111
128
|
{}.tap do |hash|
|
129
|
+
_x_labels = x_labels
|
112
130
|
context.series.to_a.each_with_index do |series_data, series_index|
|
113
|
-
|
131
|
+
_x_labels.each do |x_label|
|
114
132
|
unless hash[x_label]
|
115
133
|
hash[x_label] = []
|
116
134
|
end
|
@@ -118,14 +136,14 @@ module Charty
|
|
118
136
|
if data_index = series_data.xs.to_a.index(x_label)
|
119
137
|
hash[x_label] << series_data.ys.to_a[data_index]
|
120
138
|
else
|
121
|
-
hash[x_label] <<
|
139
|
+
hash[x_label] << nil
|
122
140
|
end
|
123
141
|
end
|
124
142
|
end
|
125
143
|
end
|
126
144
|
end
|
127
145
|
|
128
|
-
def
|
146
|
+
def rows
|
129
147
|
case context.method
|
130
148
|
when :bubble
|
131
149
|
[].tap do |data_array|
|
@@ -133,10 +151,10 @@ module Charty
|
|
133
151
|
series_data.xs.to_a.each_with_index do |data, data_index|
|
134
152
|
data_array << [
|
135
153
|
"",
|
136
|
-
series_data.xs.to_a[data_index]
|
137
|
-
series_data.ys.to_a[data_index]
|
154
|
+
series_data.xs.to_a[data_index],
|
155
|
+
series_data.ys.to_a[data_index],
|
138
156
|
series_data[:label] || series_index.to_s,
|
139
|
-
series_data.zs.to_a[data_index]
|
157
|
+
series_data.zs.to_a[data_index],
|
140
158
|
]
|
141
159
|
end
|
142
160
|
end
|
@@ -150,12 +168,36 @@ module Charty
|
|
150
168
|
else
|
151
169
|
[].tap do |data_array|
|
152
170
|
data_hash.each do |k, v|
|
153
|
-
data_array << [k
|
171
|
+
data_array << [k, v].flatten
|
154
172
|
end
|
155
173
|
end
|
156
174
|
end
|
157
175
|
end
|
158
176
|
|
177
|
+
def convert_to_javascript(data)
|
178
|
+
case data
|
179
|
+
when Array
|
180
|
+
converted_data = data.collect do |element|
|
181
|
+
convert_to_javascript(element)
|
182
|
+
end
|
183
|
+
"[#{converted_data.join(", ")}]"
|
184
|
+
when Time
|
185
|
+
time = data.dup.utc
|
186
|
+
args = [
|
187
|
+
time.year,
|
188
|
+
time.month - 1,
|
189
|
+
time.day,
|
190
|
+
time.hour,
|
191
|
+
time.min,
|
192
|
+
time.sec,
|
193
|
+
time.nsec / 1000 / 1000,
|
194
|
+
]
|
195
|
+
"new Date(Date.UTC(#{args.join(", ")}))"
|
196
|
+
else
|
197
|
+
data.to_json
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
159
201
|
def x_range_option
|
160
202
|
x_range = if context.method != :barh
|
161
203
|
context&.range&.fetch(:x, nil)
|
@@ -189,7 +231,7 @@ module Charty
|
|
189
231
|
function drawChart() {
|
190
232
|
const data = new google.visualization.DataTable();
|
191
233
|
#{data_column_js}
|
192
|
-
data.addRows(#{
|
234
|
+
data.addRows(#{convert_to_javascript(rows)})
|
193
235
|
|
194
236
|
const view = new google.visualization.DataView(data);
|
195
237
|
|
@@ -26,7 +26,7 @@ module Charty
|
|
26
26
|
raise NotImplementedError
|
27
27
|
end
|
28
28
|
|
29
|
-
def
|
29
|
+
def old_style_render(context, filename="")
|
30
30
|
FileUtils.mkdir_p(File.dirname(filename))
|
31
31
|
plot(@plot, context).write(filename)
|
32
32
|
end
|
@@ -83,7 +83,7 @@ module Charty
|
|
83
83
|
p.x_axis_label = context.xlabel if context.xlabel
|
84
84
|
p.y_axis_label = context.ylabel if context.ylabel
|
85
85
|
context.series.each do |data|
|
86
|
-
p.
|
86
|
+
p.dataxy(data.label, data.xs.to_a, data.ys.to_a)
|
87
87
|
end
|
88
88
|
p
|
89
89
|
when :scatter
|
@@ -99,7 +99,18 @@ module Charty
|
|
99
99
|
# refs. https://github.com/topfunky/gruff/issues/163
|
100
100
|
raise NotImplementedError
|
101
101
|
when :hist
|
102
|
-
|
102
|
+
p = plot::Histogram.new
|
103
|
+
p.title = context.title if context.title
|
104
|
+
p.x_axis_label = context.xlabel if context.xlabel
|
105
|
+
p.y_axis_label = context.ylabel if context.ylabel
|
106
|
+
if context.range_x
|
107
|
+
p.minimum_bin = context.range_x.first
|
108
|
+
p.maximum_bin = context.range_x.last
|
109
|
+
end
|
110
|
+
context.data.each do |data|
|
111
|
+
p.data('', data.to_a)
|
112
|
+
end
|
113
|
+
p
|
103
114
|
end
|
104
115
|
end
|
105
116
|
end
|
@@ -1,4 +1,6 @@
|
|
1
|
-
require
|
1
|
+
require "json"
|
2
|
+
require "securerandom"
|
3
|
+
require "tmpdir"
|
2
4
|
|
3
5
|
module Charty
|
4
6
|
module Backends
|
@@ -35,7 +37,7 @@ module Charty
|
|
35
37
|
@series = series
|
36
38
|
end
|
37
39
|
|
38
|
-
def
|
40
|
+
def old_style_render(context, filename)
|
39
41
|
plot(nil, context)
|
40
42
|
end
|
41
43
|
|
@@ -55,24 +57,22 @@ module Charty
|
|
55
57
|
end
|
56
58
|
end
|
57
59
|
|
58
|
-
private
|
59
|
-
|
60
|
-
def plotly_load_tag
|
60
|
+
private def plotly_load_tag
|
61
61
|
if self.class.with_api_load_tag
|
62
62
|
"<script type='text/javascript' src='#{self.class.plotly_src}'></script>"
|
63
63
|
else
|
64
64
|
end
|
65
65
|
end
|
66
66
|
|
67
|
-
def div_id
|
67
|
+
private def div_id
|
68
68
|
"charty-plotly-#{self.class.chart_id}"
|
69
69
|
end
|
70
70
|
|
71
|
-
def div_style
|
71
|
+
private def div_style
|
72
72
|
"width: 100%;height: 100%;"
|
73
73
|
end
|
74
74
|
|
75
|
-
def render_graph(context, type, options: {})
|
75
|
+
private def render_graph(context, type, options: {})
|
76
76
|
data = context.series.map do |series|
|
77
77
|
{
|
78
78
|
type: type,
|
@@ -95,7 +95,7 @@ module Charty
|
|
95
95
|
render_html(data, layout)
|
96
96
|
end
|
97
97
|
|
98
|
-
def render_html(data, layout)
|
98
|
+
private def render_html(data, layout)
|
99
99
|
<<~FRAGMENT
|
100
100
|
#{plotly_load_tag unless self.class.chart_id > 1}
|
101
101
|
<div id="#{div_id}" style="#{div_style}"></div>
|
@@ -104,6 +104,771 @@ module Charty
|
|
104
104
|
</script>
|
105
105
|
FRAGMENT
|
106
106
|
end
|
107
|
+
|
108
|
+
# ==== NEW PLOTTING API ====
|
109
|
+
|
110
|
+
class HTML
|
111
|
+
def initialize(html)
|
112
|
+
@html = html
|
113
|
+
end
|
114
|
+
|
115
|
+
def to_iruby
|
116
|
+
["text/html", @html]
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def begin_figure
|
121
|
+
@traces = []
|
122
|
+
@layout = {showlegend: false}
|
123
|
+
end
|
124
|
+
|
125
|
+
def bar(bar_pos, group_names, values, colors, orient, label: nil, width: 0.8r,
|
126
|
+
align: :center, conf_int: nil, error_colors: nil, error_width: nil, cap_size: nil)
|
127
|
+
bar_pos = Array(bar_pos)
|
128
|
+
values = Array(values)
|
129
|
+
colors = Array(colors).map(&:to_hex_string)
|
130
|
+
|
131
|
+
if orient == :v
|
132
|
+
x, y = bar_pos, values
|
133
|
+
x = group_names unless group_names.nil?
|
134
|
+
else
|
135
|
+
x, y = values, bar_pos
|
136
|
+
y = group_names unless group_names.nil?
|
137
|
+
end
|
138
|
+
|
139
|
+
trace = {
|
140
|
+
type: :bar,
|
141
|
+
orientation: orient,
|
142
|
+
x: x,
|
143
|
+
y: y,
|
144
|
+
width: width,
|
145
|
+
marker: {color: colors}
|
146
|
+
}
|
147
|
+
trace[:name] = label unless label.nil?
|
148
|
+
|
149
|
+
unless conf_int.nil?
|
150
|
+
errors_low = conf_int.map.with_index {|(low, _), i| values[i] - low }
|
151
|
+
errors_high = conf_int.map.with_index {|(_, high), i| high - values[i] }
|
152
|
+
|
153
|
+
error_bar = {
|
154
|
+
type: :data,
|
155
|
+
visible: true,
|
156
|
+
symmetric: false,
|
157
|
+
array: errors_high,
|
158
|
+
arrayminus: errors_low,
|
159
|
+
color: error_colors[0].to_hex_string
|
160
|
+
}
|
161
|
+
error_bar[:thickness] = error_width unless error_width.nil?
|
162
|
+
error_bar[:width] = cap_size unless cap_size.nil?
|
163
|
+
|
164
|
+
error_bar_key = orient == :v ? :error_y : :error_x
|
165
|
+
trace[error_bar_key] = error_bar
|
166
|
+
end
|
167
|
+
|
168
|
+
@traces << trace
|
169
|
+
|
170
|
+
if group_names
|
171
|
+
@layout[:barmode] = :group
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def box_plot(plot_data, group_names,
|
176
|
+
orient:, colors:, gray:, dodge:, width: 0.8r,
|
177
|
+
flier_size: 5, whisker: 1.5, notch: false)
|
178
|
+
colors = Array(colors).map(&:to_hex_string)
|
179
|
+
gray = gray.to_hex_string
|
180
|
+
width = Float(width)
|
181
|
+
flier_size = Float(width)
|
182
|
+
whisker = Float(whisker)
|
183
|
+
|
184
|
+
traces = plot_data.map.with_index do |group_data, i|
|
185
|
+
group_data = Array(group_data)
|
186
|
+
trace = {
|
187
|
+
type: :box,
|
188
|
+
orientation: orient,
|
189
|
+
name: group_names[i],
|
190
|
+
marker: {color: colors[i]}
|
191
|
+
}
|
192
|
+
if orient == :v
|
193
|
+
trace.update(y: group_data)
|
194
|
+
else
|
195
|
+
trace.update(x: group_data)
|
196
|
+
end
|
197
|
+
|
198
|
+
trace
|
199
|
+
end
|
200
|
+
|
201
|
+
traces.reverse! if orient == :h
|
202
|
+
|
203
|
+
@traces.concat(traces)
|
204
|
+
end
|
205
|
+
|
206
|
+
def grouped_box_plot(plot_data, group_names, color_names,
|
207
|
+
orient:, colors:, gray:, dodge:, width: 0.8r,
|
208
|
+
flier_size: 5, whisker: 1.5, notch: false)
|
209
|
+
colors = Array(colors).map(&:to_hex_string)
|
210
|
+
gray = gray.to_hex_string
|
211
|
+
width = Float(width)
|
212
|
+
flier_size = Float(width)
|
213
|
+
whisker = Float(whisker)
|
214
|
+
|
215
|
+
@layout[:boxmode] = :group
|
216
|
+
|
217
|
+
if orient == :h
|
218
|
+
@layout[:xaxis] ||= {}
|
219
|
+
@layout[:xaxis][:zeroline] = false
|
220
|
+
|
221
|
+
plot_data = plot_data.map {|d| d.reverse }
|
222
|
+
group_names = group_names.reverse
|
223
|
+
end
|
224
|
+
|
225
|
+
traces = color_names.map.with_index do |color_name, i|
|
226
|
+
group_keys = group_names.flat_map.with_index { |name, j|
|
227
|
+
Array.new(plot_data[i][j].length, name)
|
228
|
+
}.flatten
|
229
|
+
|
230
|
+
values = plot_data[i].flat_map {|d| Array(d) }
|
231
|
+
|
232
|
+
trace = {
|
233
|
+
type: :box,
|
234
|
+
orientation: orient,
|
235
|
+
name: color_name,
|
236
|
+
marker: {color: colors[i]}
|
237
|
+
}
|
238
|
+
|
239
|
+
if orient == :v
|
240
|
+
trace.update(y: values, x: group_keys)
|
241
|
+
else
|
242
|
+
trace.update(x: values, y: group_keys)
|
243
|
+
end
|
244
|
+
|
245
|
+
trace
|
246
|
+
end
|
247
|
+
|
248
|
+
@traces.concat(traces)
|
249
|
+
end
|
250
|
+
|
251
|
+
def scatter(x, y, variables, color:, color_mapper:,
|
252
|
+
style:, style_mapper:, size:, size_mapper:)
|
253
|
+
orig_x, orig_y = x, y
|
254
|
+
|
255
|
+
x = case x
|
256
|
+
when Charty::Vector
|
257
|
+
x.to_a
|
258
|
+
else
|
259
|
+
Array.try_convert(x)
|
260
|
+
end
|
261
|
+
if x.nil?
|
262
|
+
raise ArgumentError, "Invalid value for x: %p" % orig_x
|
263
|
+
end
|
264
|
+
|
265
|
+
y = case y
|
266
|
+
when Charty::Vector
|
267
|
+
y.to_a
|
268
|
+
else
|
269
|
+
Array.try_convert(y)
|
270
|
+
end
|
271
|
+
if y.nil?
|
272
|
+
raise ArgumentError, "Invalid value for y: %p" % orig_y
|
273
|
+
end
|
274
|
+
|
275
|
+
unless color.nil? && style.nil?
|
276
|
+
grouped_scatter(x, y, variables,
|
277
|
+
color: color, color_mapper: color_mapper,
|
278
|
+
style: style, style_mapper: style_mapper,
|
279
|
+
size: size, size_mapper: size_mapper)
|
280
|
+
return
|
281
|
+
end
|
282
|
+
|
283
|
+
trace = {
|
284
|
+
type: :scatter,
|
285
|
+
mode: :markers,
|
286
|
+
x: x,
|
287
|
+
y: y,
|
288
|
+
marker: {
|
289
|
+
line: {
|
290
|
+
width: 1,
|
291
|
+
color: "#fff"
|
292
|
+
},
|
293
|
+
size: 10
|
294
|
+
}
|
295
|
+
}
|
296
|
+
|
297
|
+
unless size.nil?
|
298
|
+
trace[:marker][:size] = size_mapper[size].map {|x| 6.0 + x * 6.0 }
|
299
|
+
end
|
300
|
+
|
301
|
+
@traces << trace
|
302
|
+
end
|
303
|
+
|
304
|
+
private def grouped_scatter(x, y, variables, color:, color_mapper:,
|
305
|
+
style:, style_mapper:, size:, size_mapper:)
|
306
|
+
@layout[:showlegend] = true
|
307
|
+
|
308
|
+
groups = (0 ... x.length).group_by do |i|
|
309
|
+
key = {}
|
310
|
+
key[:color] = color[i] unless color.nil?
|
311
|
+
key[:style] = style[i] unless style.nil?
|
312
|
+
key
|
313
|
+
end
|
314
|
+
|
315
|
+
groups.each do |group_key, indices|
|
316
|
+
trace = {
|
317
|
+
type: :scatter,
|
318
|
+
mode: :markers,
|
319
|
+
x: x.values_at(*indices),
|
320
|
+
y: y.values_at(*indices),
|
321
|
+
marker: {
|
322
|
+
line: {
|
323
|
+
width: 1,
|
324
|
+
color: "#fff"
|
325
|
+
},
|
326
|
+
size: 10
|
327
|
+
}
|
328
|
+
}
|
329
|
+
|
330
|
+
unless size.nil?
|
331
|
+
vals = size.values_at(*indices)
|
332
|
+
trace[:marker][:size] = size_mapper[vals].map do |x|
|
333
|
+
scale_scatter_point_size(x).to_f
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
name = []
|
338
|
+
legend_title = []
|
339
|
+
|
340
|
+
if group_key.key?(:color)
|
341
|
+
trace[:marker][:color] = color_mapper[group_key[:color]].to_hex_string
|
342
|
+
name << group_key[:color]
|
343
|
+
legend_title << variables[:color]
|
344
|
+
end
|
345
|
+
|
346
|
+
if group_key.key?(:style)
|
347
|
+
trace[:marker][:symbol] = style_mapper[group_key[:style], :marker]
|
348
|
+
name << group_key[:style]
|
349
|
+
legend_title << variables[:style]
|
350
|
+
end
|
351
|
+
|
352
|
+
trace[:name] = name.uniq.join(", ") unless name.empty?
|
353
|
+
|
354
|
+
@traces << trace
|
355
|
+
|
356
|
+
unless legend_title.empty?
|
357
|
+
@layout[:legend] ||= {}
|
358
|
+
@layout[:legend][:title] = {text: legend_title.uniq.join(", ")}
|
359
|
+
end
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
363
|
+
def add_scatter_plot_legend(variables, color_mapper, size_mapper, style_mapper, legend)
|
364
|
+
if legend == :full
|
365
|
+
warn("Plotly backend does not support full verbosity legend")
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
private def scale_scatter_point_size(x)
|
370
|
+
min = 6
|
371
|
+
max = 12
|
372
|
+
|
373
|
+
min + x * (max - min)
|
374
|
+
end
|
375
|
+
|
376
|
+
def line(x, y, variables, color:, color_mapper:, size:, size_mapper:, style:, style_mapper:, ci_params:)
|
377
|
+
x = case x
|
378
|
+
when Charty::Vector
|
379
|
+
x.to_a
|
380
|
+
else
|
381
|
+
orig_x, x = x, Array.try_convert(x)
|
382
|
+
if x.nil?
|
383
|
+
raise ArgumentError, "Invalid value for x: %p" % orig_x
|
384
|
+
end
|
385
|
+
end
|
386
|
+
|
387
|
+
y = case y
|
388
|
+
when Charty::Vector
|
389
|
+
y.to_a
|
390
|
+
else
|
391
|
+
orig_y, y = y, Array.try_convert(y)
|
392
|
+
if y.nil?
|
393
|
+
raise ArgumentError, "Invalid value for y: %p" % orig_y
|
394
|
+
end
|
395
|
+
end
|
396
|
+
|
397
|
+
name = []
|
398
|
+
legend_title = []
|
399
|
+
|
400
|
+
if color.nil?
|
401
|
+
# TODO: do not hard code this
|
402
|
+
line_color = Colors["#1f77b4"] # the first color of D3's category10 palette
|
403
|
+
else
|
404
|
+
line_color = color_mapper[color].to_rgb
|
405
|
+
name << color
|
406
|
+
legend_title << variables[:color]
|
407
|
+
end
|
408
|
+
|
409
|
+
unless style.nil?
|
410
|
+
marker, dashes = style_mapper[style].values_at(:marker, :dashes)
|
411
|
+
name << style
|
412
|
+
legend_title << variables[:style]
|
413
|
+
end
|
414
|
+
|
415
|
+
trace = {
|
416
|
+
type: :scatter,
|
417
|
+
mode: marker.nil? ? "lines" : "lines+markers",
|
418
|
+
x: x,
|
419
|
+
y: y,
|
420
|
+
line: {
|
421
|
+
shape: :linear,
|
422
|
+
color: line_color.to_hex_string
|
423
|
+
}
|
424
|
+
}
|
425
|
+
|
426
|
+
default_line_width = 2.0
|
427
|
+
unless size.nil?
|
428
|
+
line_width = default_line_width + 2.0 * size_mapper[size]
|
429
|
+
trace[:line][:width] = line_width
|
430
|
+
end
|
431
|
+
|
432
|
+
unless dashes.nil?
|
433
|
+
trace[:line][:dash] = convert_dash_pattern(dashes, line_width || default_line_width)
|
434
|
+
end
|
435
|
+
|
436
|
+
unless marker.nil?
|
437
|
+
trace[:marker] = {
|
438
|
+
line: {
|
439
|
+
width: 1,
|
440
|
+
color: "#fff"
|
441
|
+
},
|
442
|
+
symbol: marker,
|
443
|
+
size: 10
|
444
|
+
}
|
445
|
+
end
|
446
|
+
|
447
|
+
unless ci_params.nil?
|
448
|
+
case ci_params[:style]
|
449
|
+
when :band
|
450
|
+
y_min = ci_params[:y_min].to_a
|
451
|
+
y_max = ci_params[:y_max].to_a
|
452
|
+
@traces << {
|
453
|
+
type: :scatter,
|
454
|
+
x: x,
|
455
|
+
y: y_max,
|
456
|
+
mode: :lines,
|
457
|
+
line: { shape: :linear, width: 0 },
|
458
|
+
showlegend: false
|
459
|
+
}
|
460
|
+
@traces << {
|
461
|
+
type: :scatter,
|
462
|
+
x: x,
|
463
|
+
y: y_min,
|
464
|
+
mode: :lines,
|
465
|
+
line: { shape: :linear, width: 0 },
|
466
|
+
fill: :tonexty,
|
467
|
+
fillcolor: line_color.to_rgba(alpha: 0.2).to_hex_string,
|
468
|
+
showlegend: false
|
469
|
+
}
|
470
|
+
when :bars
|
471
|
+
y_min = ci_params[:y_min].map.with_index {|v, i| y[i] - v }
|
472
|
+
y_max = ci_params[:y_max].map.with_index {|v, i| v - y[i] }
|
473
|
+
trace[:error_y] = {
|
474
|
+
visible: true,
|
475
|
+
type: :data,
|
476
|
+
array: y_max,
|
477
|
+
arrayminus: y_min
|
478
|
+
}
|
479
|
+
unless line_color.nil?
|
480
|
+
trace[:error_y][:color] = line_color
|
481
|
+
end
|
482
|
+
unless line_width.nil?
|
483
|
+
trace[:error_y][:thickness] = line_width
|
484
|
+
end
|
485
|
+
end
|
486
|
+
end
|
487
|
+
|
488
|
+
trace[:name] = name.uniq.join(", ") unless name.empty?
|
489
|
+
|
490
|
+
@traces << trace
|
491
|
+
|
492
|
+
unless legend_title.empty?
|
493
|
+
@layout[:showlegend] = true
|
494
|
+
@layout[:legend] ||= {}
|
495
|
+
@layout[:legend][:title] = {text: legend_title.uniq.join(", ")}
|
496
|
+
end
|
497
|
+
end
|
498
|
+
|
499
|
+
def add_line_plot_legend(variables, color_mapper, size_mapper, style_mapper, legend)
|
500
|
+
if legend == :full
|
501
|
+
warn("Plotly backend does not support full verbosity legend")
|
502
|
+
end
|
503
|
+
|
504
|
+
legend_order = if variables.key?(:color)
|
505
|
+
if variables.key?(:style)
|
506
|
+
# both color and style
|
507
|
+
color_mapper.levels.product(style_mapper.levels)
|
508
|
+
else
|
509
|
+
# only color
|
510
|
+
color_mapper.levels
|
511
|
+
end
|
512
|
+
elsif variables.key?(:style)
|
513
|
+
# only style
|
514
|
+
style_mapper.levels
|
515
|
+
else
|
516
|
+
# no legend entries
|
517
|
+
nil
|
518
|
+
end
|
519
|
+
|
520
|
+
if legend_order
|
521
|
+
# sort traces
|
522
|
+
legend_index = legend_order.map.with_index { |name, i|
|
523
|
+
[Array(name).uniq.join(", "), i]
|
524
|
+
}.to_h
|
525
|
+
@traces = @traces.each_with_index.sort_by { |trace, trace_index|
|
526
|
+
index = legend_index.fetch(trace[:name], legend_order.length)
|
527
|
+
[index, trace_index]
|
528
|
+
}.map(&:first)
|
529
|
+
|
530
|
+
# remove duplicated legend entries
|
531
|
+
names = {}
|
532
|
+
@traces.each do |trace|
|
533
|
+
if trace[:showlegend] != false
|
534
|
+
name = trace[:name]
|
535
|
+
if name
|
536
|
+
if names.key?(name)
|
537
|
+
# Hide duplications
|
538
|
+
trace[:showlegend] = false
|
539
|
+
else
|
540
|
+
trace[:showlegend] = true
|
541
|
+
names[name] = true
|
542
|
+
end
|
543
|
+
else
|
544
|
+
# Hide no name trace in legend
|
545
|
+
trace[:showlegend] = false
|
546
|
+
end
|
547
|
+
end
|
548
|
+
end
|
549
|
+
end
|
550
|
+
end
|
551
|
+
|
552
|
+
private def convert_dash_pattern(pattern, line_width)
|
553
|
+
case pattern
|
554
|
+
when ""
|
555
|
+
:solid
|
556
|
+
else
|
557
|
+
pattern.map {|d| "#{line_width * d}px" }.join(",")
|
558
|
+
end
|
559
|
+
end
|
560
|
+
|
561
|
+
def set_xlabel(label)
|
562
|
+
@layout[:xaxis] ||= {}
|
563
|
+
@layout[:xaxis][:title] = label
|
564
|
+
end
|
565
|
+
|
566
|
+
def set_ylabel(label)
|
567
|
+
@layout[:yaxis] ||= {}
|
568
|
+
@layout[:yaxis][:title] = label
|
569
|
+
end
|
570
|
+
|
571
|
+
def set_xticks(values)
|
572
|
+
@layout[:xaxis] ||= {}
|
573
|
+
@layout[:xaxis][:tickmode] = "array"
|
574
|
+
@layout[:xaxis][:tickvals] = values
|
575
|
+
end
|
576
|
+
|
577
|
+
def set_yticks(values)
|
578
|
+
@layout[:yaxis] ||= {}
|
579
|
+
@layout[:yaxis][:tickmode] = "array"
|
580
|
+
@layout[:yaxis][:tickvals] = values
|
581
|
+
end
|
582
|
+
|
583
|
+
def set_xtick_labels(labels)
|
584
|
+
@layout[:xaxis] ||= {}
|
585
|
+
@layout[:xaxis][:tickmode] = "array"
|
586
|
+
@layout[:xaxis][:ticktext] = labels
|
587
|
+
end
|
588
|
+
|
589
|
+
def set_ytick_labels(labels)
|
590
|
+
@layout[:yaxis] ||= {}
|
591
|
+
@layout[:yaxis][:tickmode] = "array"
|
592
|
+
@layout[:yaxis][:ticktext] = labels
|
593
|
+
end
|
594
|
+
|
595
|
+
def set_xlim(min, max)
|
596
|
+
@layout[:xaxis] ||= {}
|
597
|
+
@layout[:xaxis][:range] = [min, max]
|
598
|
+
end
|
599
|
+
|
600
|
+
def set_ylim(min, max)
|
601
|
+
@layout[:yaxis] ||= {}
|
602
|
+
@layout[:yaxis][:range] = [min, max]
|
603
|
+
end
|
604
|
+
|
605
|
+
def disable_xaxis_grid
|
606
|
+
# do nothing
|
607
|
+
end
|
608
|
+
|
609
|
+
def disable_yaxis_grid
|
610
|
+
# do nothing
|
611
|
+
end
|
612
|
+
|
613
|
+
def invert_yaxis
|
614
|
+
@traces.each do |trace|
|
615
|
+
case trace[:type]
|
616
|
+
when :bar
|
617
|
+
trace[:y].reverse!
|
618
|
+
end
|
619
|
+
end
|
620
|
+
|
621
|
+
if @layout[:boxmode] == :group
|
622
|
+
@traces.reverse!
|
623
|
+
end
|
624
|
+
|
625
|
+
if @layout[:yaxis] && @layout[:yaxis][:ticktext]
|
626
|
+
@layout[:yaxis][:ticktext].reverse!
|
627
|
+
end
|
628
|
+
end
|
629
|
+
|
630
|
+
def legend(loc:, title:)
|
631
|
+
@layout[:showlegend] = true
|
632
|
+
@layout[:legend] = {
|
633
|
+
title: {
|
634
|
+
text: title
|
635
|
+
}
|
636
|
+
}
|
637
|
+
# TODO: Handle loc
|
638
|
+
end
|
639
|
+
|
640
|
+
def save(filename, format: nil, title: nil, width: 700, height: 500, **kwargs)
|
641
|
+
format = detect_format(filename) if format.nil?
|
642
|
+
|
643
|
+
case format
|
644
|
+
when nil, :html, "text/html"
|
645
|
+
save_html(filename, title: title, **kwargs)
|
646
|
+
when :png, "png", "image/png",
|
647
|
+
:jpeg, "jpeg", "image/jpeg"
|
648
|
+
render_image(format, filename: filename, notebook: false, title: title, width: width, height: height, **kwargs)
|
649
|
+
end
|
650
|
+
nil
|
651
|
+
end
|
652
|
+
|
653
|
+
private def detect_format(filename)
|
654
|
+
case File.extname(filename).downcase
|
655
|
+
when ".htm", ".html"
|
656
|
+
:html
|
657
|
+
when ".png"
|
658
|
+
:png
|
659
|
+
when ".jpg", ".jpeg"
|
660
|
+
:jpeg
|
661
|
+
else
|
662
|
+
raise ArgumentError,
|
663
|
+
"Unable to infer file type from filename: %p" % filename
|
664
|
+
end
|
665
|
+
end
|
666
|
+
|
667
|
+
private def save_html(filename, title:, element_id: nil)
|
668
|
+
html = <<~HTML
|
669
|
+
<!DOCTYPE html>
|
670
|
+
<html>
|
671
|
+
<head>
|
672
|
+
<meta charset="utf-8">
|
673
|
+
<title>%{title}</title>
|
674
|
+
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
|
675
|
+
</head>
|
676
|
+
<body>
|
677
|
+
<div id="%{id}" style="width: 100%%; height:100%%;"></div>
|
678
|
+
<script type="text/javascript">
|
679
|
+
Plotly.newPlot("%{id}", %{data}, %{layout});
|
680
|
+
</script>
|
681
|
+
</body>
|
682
|
+
</html>
|
683
|
+
HTML
|
684
|
+
|
685
|
+
element_id = SecureRandom.uuid if element_id.nil?
|
686
|
+
|
687
|
+
html %= {
|
688
|
+
title: title || default_html_title,
|
689
|
+
id: element_id,
|
690
|
+
data: JSON.dump(@traces),
|
691
|
+
layout: JSON.dump(@layout)
|
692
|
+
}
|
693
|
+
File.write(filename, html)
|
694
|
+
end
|
695
|
+
|
696
|
+
private def default_html_title
|
697
|
+
"Charty plot"
|
698
|
+
end
|
699
|
+
|
700
|
+
def render(element_id: nil, format: nil, notebook: false)
|
701
|
+
case format
|
702
|
+
when :html, "html"
|
703
|
+
format = "text/html"
|
704
|
+
when :png, "png"
|
705
|
+
format = "image/png"
|
706
|
+
when :jpeg, "jpeg"
|
707
|
+
format = "image/jpeg"
|
708
|
+
end
|
709
|
+
|
710
|
+
case format
|
711
|
+
when "text/html", nil
|
712
|
+
# render html after this case cause
|
713
|
+
when "image/png", "image/jpeg"
|
714
|
+
image_data = render_image(format, element_id: element_id, notebook: false)
|
715
|
+
if notebook
|
716
|
+
return [format, image_data]
|
717
|
+
else
|
718
|
+
return image_data
|
719
|
+
end
|
720
|
+
else
|
721
|
+
raise ArgumentError,
|
722
|
+
"Unsupported mime type to render: %p" % format
|
723
|
+
end
|
724
|
+
|
725
|
+
# TODO: size should be customizable
|
726
|
+
html = <<~HTML
|
727
|
+
<div id="%{id}" style="width: 100%%; height:525px;"></div>
|
728
|
+
<script type="text/javascript">
|
729
|
+
requirejs(["plotly"], function (Plotly) {
|
730
|
+
Plotly.newPlot("%{id}", %{data}, %{layout});
|
731
|
+
});
|
732
|
+
</script>
|
733
|
+
HTML
|
734
|
+
|
735
|
+
element_id = SecureRandom.uuid if element_id.nil?
|
736
|
+
|
737
|
+
html %= {
|
738
|
+
id: element_id,
|
739
|
+
data: JSON.dump(@traces),
|
740
|
+
layout: JSON.dump(@layout)
|
741
|
+
}
|
742
|
+
|
743
|
+
if notebook
|
744
|
+
IRubyOutput.prepare
|
745
|
+
["text/html", html]
|
746
|
+
else
|
747
|
+
html
|
748
|
+
end
|
749
|
+
end
|
750
|
+
|
751
|
+
private def render_image(format=nil, filename: nil, element_id: nil, notebook: false,
|
752
|
+
title: nil, width: nil, height: nil)
|
753
|
+
format = "image/png" if format.nil?
|
754
|
+
case format
|
755
|
+
when :png, "png", :jpeg, "jpeg"
|
756
|
+
image_type = format.to_s
|
757
|
+
when "image/png", "image/jpeg"
|
758
|
+
image_type = format.split("/").last
|
759
|
+
else
|
760
|
+
raise ArgumentError,
|
761
|
+
"Unsupported mime type to render image: %p" % format
|
762
|
+
end
|
763
|
+
|
764
|
+
height = 525 if height.nil?
|
765
|
+
width = (height * Math.sqrt(2)).to_i if width.nil?
|
766
|
+
title = "Charty plot" if title.nil?
|
767
|
+
|
768
|
+
element_id = SecureRandom.uuid if element_id.nil?
|
769
|
+
element_id = "charty-plotly-#{element_id}"
|
770
|
+
Dir.mktmpdir do |tmpdir|
|
771
|
+
html_filename = File.join(tmpdir, "%s.html" % element_id)
|
772
|
+
save_html(html_filename, title: title, element_id: element_id)
|
773
|
+
return self.class.render_image(html_filename, filename, image_type, element_id, width, height)
|
774
|
+
end
|
775
|
+
end
|
776
|
+
|
777
|
+
module IRubyOutput
|
778
|
+
@prepared = false
|
779
|
+
|
780
|
+
def self.prepare
|
781
|
+
return if @prepared
|
782
|
+
|
783
|
+
html = <<~HTML
|
784
|
+
<script type="text/javascript">
|
785
|
+
%{win_config}
|
786
|
+
%{mathjax_config}
|
787
|
+
require.config({
|
788
|
+
paths: {
|
789
|
+
plotly: "https://cdn.plot.ly/plotly-latest.min"
|
790
|
+
}
|
791
|
+
});
|
792
|
+
</script>
|
793
|
+
HTML
|
794
|
+
|
795
|
+
html %= {
|
796
|
+
win_config: window_plotly_config,
|
797
|
+
mathjax_config: mathjax_config
|
798
|
+
}
|
799
|
+
|
800
|
+
IRuby.display(html, mime: "text/html")
|
801
|
+
@prepared = true
|
802
|
+
end
|
803
|
+
|
804
|
+
def self.window_plotly_config
|
805
|
+
<<~END
|
806
|
+
window.PlotlyConfig = {MathJaxConfig: 'local'};
|
807
|
+
END
|
808
|
+
end
|
809
|
+
|
810
|
+
|
811
|
+
def self.mathjax_config
|
812
|
+
<<~END
|
813
|
+
if (window.MathJax) {MathJax.Hub.Config({SVG: {font: "STIX-Web"}});}
|
814
|
+
END
|
815
|
+
end
|
816
|
+
end
|
817
|
+
|
818
|
+
@playwright_fiber = nil
|
819
|
+
|
820
|
+
def self.ensure_playwright
|
821
|
+
if @playwright_fiber.nil?
|
822
|
+
begin
|
823
|
+
require "playwright"
|
824
|
+
rescue LoadError
|
825
|
+
$stderr.puts "ERROR: You need to install playwright and playwright-ruby-client before using Plotly renderer"
|
826
|
+
raise
|
827
|
+
end
|
828
|
+
|
829
|
+
@playwright_fiber = Fiber.new do
|
830
|
+
playwright_cli_executable_path = ENV.fetch("PLAYWRIGHT_CLI_EXECUTABLE_PATH", "npx playwright")
|
831
|
+
Playwright.create(playwright_cli_executable_path: playwright_cli_executable_path) do |playwright|
|
832
|
+
playwright.chromium.launch(headless: true) do |browser|
|
833
|
+
request = Fiber.yield
|
834
|
+
loop do
|
835
|
+
result = nil
|
836
|
+
case request.shift
|
837
|
+
when :finish
|
838
|
+
break
|
839
|
+
when :render
|
840
|
+
input, output, format, element_id, width, height = request
|
841
|
+
|
842
|
+
page = browser.new_page
|
843
|
+
page.set_viewport_size(width: width, height: height)
|
844
|
+
page.goto("file://#{input}")
|
845
|
+
element = page.query_selector("\##{element_id}")
|
846
|
+
|
847
|
+
kwargs = {type: format}
|
848
|
+
kwargs[:path] = output unless output.nil?
|
849
|
+
result = element.screenshot(**kwargs)
|
850
|
+
end
|
851
|
+
request = Fiber.yield(result)
|
852
|
+
end
|
853
|
+
end
|
854
|
+
end
|
855
|
+
end
|
856
|
+
@playwright_fiber.resume
|
857
|
+
end
|
858
|
+
end
|
859
|
+
|
860
|
+
def self.terminate_playwright
|
861
|
+
return if @playwright_fiber.nil?
|
862
|
+
|
863
|
+
@playwright_fiber.resume([:finish])
|
864
|
+
end
|
865
|
+
|
866
|
+
at_exit { terminate_playwright }
|
867
|
+
|
868
|
+
def self.render_image(input, output, format, element_id, width, height)
|
869
|
+
ensure_playwright if @playwright_fiber.nil?
|
870
|
+
@playwright_fiber.resume([:render, input, output, format.to_s, element_id, width, height])
|
871
|
+
end
|
107
872
|
end
|
108
873
|
end
|
109
874
|
end
|