charty 0.1.5.dev → 0.2.5
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 +176 -9
- data/Rakefile +4 -5
- data/charty.gemspec +10 -1
- 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_bokeh.ipynb +156 -0
- data/examples/sample_google_chart.ipynb +229 -68
- data/examples/sample_images/bar_bokeh.html +85 -0
- data/examples/sample_images/barh_bokeh.html +85 -0
- data/examples/sample_images/box_plot_bokeh.html +85 -0
- data/examples/sample_images/curve_bokeh.html +85 -0
- data/examples/sample_images/curve_with_function_bokeh.html +85 -0
- data/examples/sample_images/hist_gruff.png +0 -0
- data/examples/sample_images/scatter_bokeh.html +85 -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 +14 -1
- data/lib/charty/backend_methods.rb +8 -0
- data/lib/charty/backends.rb +80 -0
- data/lib/charty/backends/bokeh.rb +32 -26
- data/lib/charty/backends/google_charts.rb +267 -0
- data/lib/charty/backends/gruff.rb +102 -83
- data/lib/charty/backends/plotly.rb +685 -0
- data/lib/charty/backends/pyplot.rb +586 -92
- data/lib/charty/backends/rubyplot.rb +82 -74
- data/lib/charty/backends/unicode_plot.rb +79 -0
- data/lib/charty/index.rb +213 -0
- data/lib/charty/linspace.rb +1 -1
- data/lib/charty/missing_value_support.rb +14 -0
- data/lib/charty/plot_methods.rb +184 -0
- data/lib/charty/plotter.rb +48 -40
- data/lib/charty/plotters.rb +11 -0
- data/lib/charty/plotters/abstract_plotter.rb +183 -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/random_support.rb +25 -0
- data/lib/charty/plotters/relational_plotter.rb +518 -0
- data/lib/charty/plotters/scatter_plotter.rb +104 -0
- data/lib/charty/plotters/vector_plotter.rb +6 -0
- data/lib/charty/statistics.rb +114 -0
- data/lib/charty/table.rb +80 -3
- data/lib/charty/table_adapters.rb +25 -0
- data/lib/charty/table_adapters/active_record_adapter.rb +63 -0
- data/lib/charty/table_adapters/base_adapter.rb +69 -0
- data/lib/charty/table_adapters/daru_adapter.rb +70 -0
- data/lib/charty/table_adapters/datasets_adapter.rb +49 -0
- data/lib/charty/table_adapters/hash_adapter.rb +224 -0
- data/lib/charty/table_adapters/narray_adapter.rb +76 -0
- data/lib/charty/table_adapters/nmatrix_adapter.rb +67 -0
- data/lib/charty/table_adapters/pandas_adapter.rb +81 -0
- data/lib/charty/util.rb +20 -0
- data/lib/charty/vector.rb +69 -0
- data/lib/charty/vector_adapters.rb +183 -0
- data/lib/charty/vector_adapters/array_adapter.rb +109 -0
- data/lib/charty/vector_adapters/daru_adapter.rb +171 -0
- data/lib/charty/vector_adapters/narray_adapter.rb +187 -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 +200 -0
- data/lib/charty/version.rb +1 -1
- metadata +179 -10
- data/.travis.yml +0 -11
- data/lib/charty/backends/google_chart.rb +0 -167
- data/lib/charty/plotter_adapter.rb +0 -17
@@ -0,0 +1,685 @@
|
|
1
|
+
require "json"
|
2
|
+
require "securerandom"
|
3
|
+
require "tmpdir"
|
4
|
+
|
5
|
+
module Charty
|
6
|
+
module Backends
|
7
|
+
class Plotly
|
8
|
+
Backends.register(:plotly, self)
|
9
|
+
|
10
|
+
attr_reader :context
|
11
|
+
|
12
|
+
class << self
|
13
|
+
attr_writer :chart_id, :with_api_load_tag, :plotly_src
|
14
|
+
|
15
|
+
def chart_id
|
16
|
+
@chart_id ||= 0
|
17
|
+
end
|
18
|
+
|
19
|
+
def with_api_load_tag
|
20
|
+
return @with_api_load_tag unless @with_api_load_tag.nil?
|
21
|
+
|
22
|
+
@with_api_load_tag = true
|
23
|
+
end
|
24
|
+
|
25
|
+
def plotly_src
|
26
|
+
@plotly_src ||= 'https://cdn.plot.ly/plotly-latest.min.js'
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def initilize
|
31
|
+
end
|
32
|
+
|
33
|
+
def label(x, y)
|
34
|
+
end
|
35
|
+
|
36
|
+
def series=(series)
|
37
|
+
@series = series
|
38
|
+
end
|
39
|
+
|
40
|
+
def old_style_render(context, filename)
|
41
|
+
plot(nil, context)
|
42
|
+
end
|
43
|
+
|
44
|
+
def plot(plot, context)
|
45
|
+
context = context
|
46
|
+
self.class.chart_id += 1
|
47
|
+
|
48
|
+
case context.method
|
49
|
+
when :bar
|
50
|
+
render_graph(context, :bar)
|
51
|
+
when :curve
|
52
|
+
render_graph(context, :scatter)
|
53
|
+
when :scatter
|
54
|
+
render_graph(context, nil, options: {data: {mode: "markers"}})
|
55
|
+
else
|
56
|
+
raise NotImplementedError
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
private def plotly_load_tag
|
61
|
+
if self.class.with_api_load_tag
|
62
|
+
"<script type='text/javascript' src='#{self.class.plotly_src}'></script>"
|
63
|
+
else
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
private def div_id
|
68
|
+
"charty-plotly-#{self.class.chart_id}"
|
69
|
+
end
|
70
|
+
|
71
|
+
private def div_style
|
72
|
+
"width: 100%;height: 100%;"
|
73
|
+
end
|
74
|
+
|
75
|
+
private def render_graph(context, type, options: {})
|
76
|
+
data = context.series.map do |series|
|
77
|
+
{
|
78
|
+
type: type,
|
79
|
+
x: series.xs.to_a,
|
80
|
+
y: series.ys.to_a,
|
81
|
+
name: series.label
|
82
|
+
}.merge(options[:data] || {})
|
83
|
+
end
|
84
|
+
layout = {
|
85
|
+
title: { text: context.title },
|
86
|
+
xaxis: {
|
87
|
+
title: context.xlabel,
|
88
|
+
range: [context.range[:x].first, context.range[:x].last]
|
89
|
+
},
|
90
|
+
yaxis: {
|
91
|
+
title: context.ylabel,
|
92
|
+
range: [context.range[:y].first, context.range[:y].last]
|
93
|
+
}
|
94
|
+
}
|
95
|
+
render_html(data, layout)
|
96
|
+
end
|
97
|
+
|
98
|
+
private def render_html(data, layout)
|
99
|
+
<<~FRAGMENT
|
100
|
+
#{plotly_load_tag unless self.class.chart_id > 1}
|
101
|
+
<div id="#{div_id}" style="#{div_style}"></div>
|
102
|
+
<script>
|
103
|
+
Plotly.plot('#{div_id}', #{JSON.dump(data)}, #{JSON.dump(layout)} );
|
104
|
+
</script>
|
105
|
+
FRAGMENT
|
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, legend:, color:, color_mapper:,
|
252
|
+
style:, style_mapper:, size:, size_mapper:)
|
253
|
+
if legend == :full
|
254
|
+
warn("Plotly backend does not support full verbosity legend")
|
255
|
+
end
|
256
|
+
|
257
|
+
orig_x, orig_y = x, y
|
258
|
+
|
259
|
+
x = case x
|
260
|
+
when Charty::Vector
|
261
|
+
x.to_a
|
262
|
+
else
|
263
|
+
Array.try_convert(x)
|
264
|
+
end
|
265
|
+
if x.nil?
|
266
|
+
raise ArgumentError, "Invalid value for x: %p" % orig_x
|
267
|
+
end
|
268
|
+
|
269
|
+
y = case y
|
270
|
+
when Charty::Vector
|
271
|
+
y.to_a
|
272
|
+
else
|
273
|
+
Array.try_convert(y)
|
274
|
+
end
|
275
|
+
if y.nil?
|
276
|
+
raise ArgumentError, "Invalid value for y: %p" % orig_y
|
277
|
+
end
|
278
|
+
|
279
|
+
unless color.nil? && style.nil?
|
280
|
+
grouped_scatter(x, y, variables, legend: legend,
|
281
|
+
color: color, color_mapper: color_mapper,
|
282
|
+
style: style, style_mapper: style_mapper,
|
283
|
+
size: size, size_mapper: size_mapper)
|
284
|
+
return
|
285
|
+
end
|
286
|
+
|
287
|
+
trace = {
|
288
|
+
type: :scatter,
|
289
|
+
mode: :markers,
|
290
|
+
x: x,
|
291
|
+
y: y,
|
292
|
+
marker: {
|
293
|
+
line: {
|
294
|
+
width: 1,
|
295
|
+
color: "#fff"
|
296
|
+
},
|
297
|
+
size: 10
|
298
|
+
}
|
299
|
+
}
|
300
|
+
|
301
|
+
unless size.nil?
|
302
|
+
trace[:marker][:size] = size_mapper[size].map {|x| 6.0 + x * 6.0 }
|
303
|
+
end
|
304
|
+
|
305
|
+
@traces << trace
|
306
|
+
end
|
307
|
+
|
308
|
+
private def grouped_scatter(x, y, variables, legend:, color:, color_mapper:,
|
309
|
+
style:, style_mapper:, size:, size_mapper:)
|
310
|
+
@layout[:showlegend] = true
|
311
|
+
|
312
|
+
groups = (0 ... x.length).group_by do |i|
|
313
|
+
key = {}
|
314
|
+
key[:color] = color[i] unless color.nil?
|
315
|
+
key[:style] = style[i] unless style.nil?
|
316
|
+
key
|
317
|
+
end
|
318
|
+
|
319
|
+
groups.each do |group_key, indices|
|
320
|
+
trace = {
|
321
|
+
type: :scatter,
|
322
|
+
mode: :markers,
|
323
|
+
x: x.values_at(*indices),
|
324
|
+
y: y.values_at(*indices),
|
325
|
+
marker: {
|
326
|
+
line: {
|
327
|
+
width: 1,
|
328
|
+
color: "#fff"
|
329
|
+
},
|
330
|
+
size: 10
|
331
|
+
}
|
332
|
+
}
|
333
|
+
|
334
|
+
unless size.nil?
|
335
|
+
vals = size.values_at(*indices)
|
336
|
+
trace[:marker][:size] = size_mapper[vals].map(&method(:scale_scatter_point_size))
|
337
|
+
end
|
338
|
+
|
339
|
+
name = []
|
340
|
+
legend_title = []
|
341
|
+
|
342
|
+
if group_key.key?(:color)
|
343
|
+
trace[:marker][:color] = color_mapper[group_key[:color]].to_hex_string
|
344
|
+
name << group_key[:color]
|
345
|
+
legend_title << variables[:color]
|
346
|
+
end
|
347
|
+
|
348
|
+
if group_key.key?(:style)
|
349
|
+
trace[:marker][:symbol] = style_mapper[group_key[:style], :marker]
|
350
|
+
name << group_key[:style]
|
351
|
+
legend_title << variables[:style]
|
352
|
+
end
|
353
|
+
|
354
|
+
trace[:name] = name.uniq.join(", ") unless name.empty?
|
355
|
+
|
356
|
+
@traces << trace
|
357
|
+
|
358
|
+
unless legend_title.empty?
|
359
|
+
@layout[:legend] ||= {}
|
360
|
+
@layout[:legend][:title] = {text: legend_title.uniq.join(", ")}
|
361
|
+
end
|
362
|
+
end
|
363
|
+
end
|
364
|
+
|
365
|
+
private def scale_scatter_point_size(x)
|
366
|
+
min = 6
|
367
|
+
max = 12
|
368
|
+
|
369
|
+
min + x * (max - min)
|
370
|
+
end
|
371
|
+
|
372
|
+
def set_xlabel(label)
|
373
|
+
@layout[:xaxis] ||= {}
|
374
|
+
@layout[:xaxis][:title] = label
|
375
|
+
end
|
376
|
+
|
377
|
+
def set_ylabel(label)
|
378
|
+
@layout[:yaxis] ||= {}
|
379
|
+
@layout[:yaxis][:title] = label
|
380
|
+
end
|
381
|
+
|
382
|
+
def set_xticks(values)
|
383
|
+
@layout[:xaxis] ||= {}
|
384
|
+
@layout[:xaxis][:tickmode] = "array"
|
385
|
+
@layout[:xaxis][:tickvals] = values
|
386
|
+
end
|
387
|
+
|
388
|
+
def set_yticks(values)
|
389
|
+
@layout[:yaxis] ||= {}
|
390
|
+
@layout[:yaxis][:tickmode] = "array"
|
391
|
+
@layout[:yaxis][:tickvals] = values
|
392
|
+
end
|
393
|
+
|
394
|
+
def set_xtick_labels(labels)
|
395
|
+
@layout[:xaxis] ||= {}
|
396
|
+
@layout[:xaxis][:tickmode] = "array"
|
397
|
+
@layout[:xaxis][:ticktext] = labels
|
398
|
+
end
|
399
|
+
|
400
|
+
def set_ytick_labels(labels)
|
401
|
+
@layout[:yaxis] ||= {}
|
402
|
+
@layout[:yaxis][:tickmode] = "array"
|
403
|
+
@layout[:yaxis][:ticktext] = labels
|
404
|
+
end
|
405
|
+
|
406
|
+
def set_xlim(min, max)
|
407
|
+
@layout[:xaxis] ||= {}
|
408
|
+
@layout[:xaxis][:range] = [min, max]
|
409
|
+
end
|
410
|
+
|
411
|
+
def set_ylim(min, max)
|
412
|
+
@layout[:yaxis] ||= {}
|
413
|
+
@layout[:yaxis][:range] = [min, max]
|
414
|
+
end
|
415
|
+
|
416
|
+
def disable_xaxis_grid
|
417
|
+
# do nothing
|
418
|
+
end
|
419
|
+
|
420
|
+
def disable_yaxis_grid
|
421
|
+
# do nothing
|
422
|
+
end
|
423
|
+
|
424
|
+
def invert_yaxis
|
425
|
+
@traces.each do |trace|
|
426
|
+
case trace[:type]
|
427
|
+
when :bar
|
428
|
+
trace[:y].reverse!
|
429
|
+
end
|
430
|
+
end
|
431
|
+
|
432
|
+
if @layout[:boxmode] == :group
|
433
|
+
@traces.reverse!
|
434
|
+
end
|
435
|
+
|
436
|
+
if @layout[:yaxis] && @layout[:yaxis][:ticktext]
|
437
|
+
@layout[:yaxis][:ticktext].reverse!
|
438
|
+
end
|
439
|
+
end
|
440
|
+
|
441
|
+
def legend(loc:, title:)
|
442
|
+
@layout[:showlegend] = true
|
443
|
+
@layout[:legend] = {
|
444
|
+
title: {
|
445
|
+
text: title
|
446
|
+
}
|
447
|
+
}
|
448
|
+
# TODO: Handle loc
|
449
|
+
end
|
450
|
+
|
451
|
+
def save(filename, format: nil, title: nil, width: 700, height: 500, **kwargs)
|
452
|
+
format = detect_format(filename) if format.nil?
|
453
|
+
|
454
|
+
case format
|
455
|
+
when nil, :html, "text/html"
|
456
|
+
save_html(filename, title: title, **kwargs)
|
457
|
+
when :png, "png", "image/png",
|
458
|
+
:jpeg, "jpeg", "image/jpeg"
|
459
|
+
render_image(format, filename: filename, notebook: false, title: title, width: width, height: height, **kwargs)
|
460
|
+
end
|
461
|
+
nil
|
462
|
+
end
|
463
|
+
|
464
|
+
private def detect_format(filename)
|
465
|
+
case File.extname(filename).downcase
|
466
|
+
when ".htm", ".html"
|
467
|
+
:html
|
468
|
+
when ".png"
|
469
|
+
:png
|
470
|
+
when ".jpg", ".jpeg"
|
471
|
+
:jpeg
|
472
|
+
else
|
473
|
+
raise ArgumentError,
|
474
|
+
"Unable to infer file type from filename: %p" % filename
|
475
|
+
end
|
476
|
+
end
|
477
|
+
|
478
|
+
private def save_html(filename, title:, element_id: nil)
|
479
|
+
html = <<~HTML
|
480
|
+
<!DOCTYPE html>
|
481
|
+
<html>
|
482
|
+
<head>
|
483
|
+
<meta charset="utf-8">
|
484
|
+
<title>%{title}</title>
|
485
|
+
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
|
486
|
+
</head>
|
487
|
+
<body>
|
488
|
+
<div id="%{id}" style="width: 100%%; height:100%%;"></div>
|
489
|
+
<script type="text/javascript">
|
490
|
+
Plotly.newPlot("%{id}", %{data}, %{layout});
|
491
|
+
</script>
|
492
|
+
</body>
|
493
|
+
</html>
|
494
|
+
HTML
|
495
|
+
|
496
|
+
element_id = SecureRandom.uuid if element_id.nil?
|
497
|
+
|
498
|
+
html %= {
|
499
|
+
title: title || default_html_title,
|
500
|
+
id: element_id,
|
501
|
+
data: JSON.dump(@traces),
|
502
|
+
layout: JSON.dump(@layout)
|
503
|
+
}
|
504
|
+
File.write(filename, html)
|
505
|
+
end
|
506
|
+
|
507
|
+
private def default_html_title
|
508
|
+
"Charty plot"
|
509
|
+
end
|
510
|
+
|
511
|
+
def render(element_id: nil, format: nil, notebook: false)
|
512
|
+
case format
|
513
|
+
when :html, "html"
|
514
|
+
format = "text/html"
|
515
|
+
when :png, "png"
|
516
|
+
format = "image/png"
|
517
|
+
when :jpeg, "jpeg"
|
518
|
+
format = "image/jpeg"
|
519
|
+
end
|
520
|
+
|
521
|
+
case format
|
522
|
+
when "text/html", nil
|
523
|
+
# render html after this case cause
|
524
|
+
when "image/png", "image/jpeg"
|
525
|
+
image_data = render_image(format, element_id: element_id, notebook: false)
|
526
|
+
if notebook
|
527
|
+
return [format, image_data]
|
528
|
+
else
|
529
|
+
return image_data
|
530
|
+
end
|
531
|
+
else
|
532
|
+
raise ArgumentError,
|
533
|
+
"Unsupported mime type to render: %p" % format
|
534
|
+
end
|
535
|
+
|
536
|
+
# TODO: size should be customizable
|
537
|
+
html = <<~HTML
|
538
|
+
<div id="%{id}" style="width: 100%%; height:525px;"></div>
|
539
|
+
<script type="text/javascript">
|
540
|
+
requirejs(["plotly"], function (Plotly) {
|
541
|
+
Plotly.newPlot("%{id}", %{data}, %{layout});
|
542
|
+
});
|
543
|
+
</script>
|
544
|
+
HTML
|
545
|
+
|
546
|
+
element_id = SecureRandom.uuid if element_id.nil?
|
547
|
+
|
548
|
+
html %= {
|
549
|
+
id: element_id,
|
550
|
+
data: JSON.dump(@traces),
|
551
|
+
layout: JSON.dump(@layout)
|
552
|
+
}
|
553
|
+
|
554
|
+
if notebook
|
555
|
+
IRubyOutput.prepare
|
556
|
+
["text/html", html]
|
557
|
+
else
|
558
|
+
html
|
559
|
+
end
|
560
|
+
end
|
561
|
+
|
562
|
+
private def render_image(format=nil, filename: nil, element_id: nil, notebook: false,
|
563
|
+
title: nil, width: nil, height: nil)
|
564
|
+
format = "image/png" if format.nil?
|
565
|
+
case format
|
566
|
+
when :png, "png", :jpeg, "jpeg"
|
567
|
+
image_type = format.to_s
|
568
|
+
when "image/png", "image/jpeg"
|
569
|
+
image_type = format.split("/").last
|
570
|
+
else
|
571
|
+
raise ArgumentError,
|
572
|
+
"Unsupported mime type to render image: %p" % format
|
573
|
+
end
|
574
|
+
|
575
|
+
height = 525 if height.nil?
|
576
|
+
width = (height * Math.sqrt(2)).to_i if width.nil?
|
577
|
+
title = "Charty plot" if title.nil?
|
578
|
+
|
579
|
+
element_id = SecureRandom.uuid if element_id.nil?
|
580
|
+
element_id = "charty-plotly-#{element_id}"
|
581
|
+
Dir.mktmpdir do |tmpdir|
|
582
|
+
html_filename = File.join(tmpdir, "%s.html" % element_id)
|
583
|
+
save_html(html_filename, title: title, element_id: element_id)
|
584
|
+
return self.class.render_image(html_filename, filename, image_type, element_id, width, height)
|
585
|
+
end
|
586
|
+
end
|
587
|
+
|
588
|
+
module IRubyOutput
|
589
|
+
@prepared = false
|
590
|
+
|
591
|
+
def self.prepare
|
592
|
+
return if @prepared
|
593
|
+
|
594
|
+
html = <<~HTML
|
595
|
+
<script type="text/javascript">
|
596
|
+
%{win_config}
|
597
|
+
%{mathjax_config}
|
598
|
+
require.config({
|
599
|
+
paths: {
|
600
|
+
plotly: "https://cdn.plot.ly/plotly-latest.min"
|
601
|
+
}
|
602
|
+
});
|
603
|
+
</script>
|
604
|
+
HTML
|
605
|
+
|
606
|
+
html %= {
|
607
|
+
win_config: window_plotly_config,
|
608
|
+
mathjax_config: mathjax_config
|
609
|
+
}
|
610
|
+
|
611
|
+
IRuby.display(html, mime: "text/html")
|
612
|
+
@prepared = true
|
613
|
+
end
|
614
|
+
|
615
|
+
def self.window_plotly_config
|
616
|
+
<<~END
|
617
|
+
window.PlotlyConfig = {MathJaxConfig: 'local'};
|
618
|
+
END
|
619
|
+
end
|
620
|
+
|
621
|
+
|
622
|
+
def self.mathjax_config
|
623
|
+
<<~END
|
624
|
+
if (window.MathJax) {MathJax.Hub.Config({SVG: {font: "STIX-Web"}});}
|
625
|
+
END
|
626
|
+
end
|
627
|
+
end
|
628
|
+
|
629
|
+
@playwright_fiber = nil
|
630
|
+
|
631
|
+
def self.ensure_playwright
|
632
|
+
if @playwright_fiber.nil?
|
633
|
+
begin
|
634
|
+
require "playwright"
|
635
|
+
rescue LoadError
|
636
|
+
$stderr.puts "ERROR: You need to install playwright and playwright-ruby-client before using Plotly renderer"
|
637
|
+
raise
|
638
|
+
end
|
639
|
+
|
640
|
+
@playwright_fiber = Fiber.new do
|
641
|
+
playwright_cli_executable_path = ENV.fetch("PLAYWRIGHT_CLI_EXECUTABLE_PATH", "npx playwright")
|
642
|
+
Playwright.create(playwright_cli_executable_path: playwright_cli_executable_path) do |playwright|
|
643
|
+
playwright.chromium.launch(headless: true) do |browser|
|
644
|
+
request = Fiber.yield
|
645
|
+
loop do
|
646
|
+
result = nil
|
647
|
+
case request.shift
|
648
|
+
when :finish
|
649
|
+
break
|
650
|
+
when :render
|
651
|
+
input, output, format, element_id, width, height = request
|
652
|
+
|
653
|
+
page = browser.new_page
|
654
|
+
page.set_viewport_size(width: width, height: height)
|
655
|
+
page.goto("file://#{input}")
|
656
|
+
element = page.query_selector("\##{element_id}")
|
657
|
+
|
658
|
+
kwargs = {type: format}
|
659
|
+
kwargs[:path] = output unless output.nil?
|
660
|
+
result = element.screenshot(**kwargs)
|
661
|
+
end
|
662
|
+
request = Fiber.yield(result)
|
663
|
+
end
|
664
|
+
end
|
665
|
+
end
|
666
|
+
end
|
667
|
+
@playwright_fiber.resume
|
668
|
+
end
|
669
|
+
end
|
670
|
+
|
671
|
+
def self.terminate_playwright
|
672
|
+
return if @playwright_fiber.nil?
|
673
|
+
|
674
|
+
@playwright_fiber.resume([:finish])
|
675
|
+
end
|
676
|
+
|
677
|
+
at_exit { terminate_playwright }
|
678
|
+
|
679
|
+
def self.render_image(input, output, format, element_id, width, height)
|
680
|
+
ensure_playwright if @playwright_fiber.nil?
|
681
|
+
@playwright_fiber.resume([:render, input, output, format.to_s, element_id, width, height])
|
682
|
+
end
|
683
|
+
end
|
684
|
+
end
|
685
|
+
end
|