charty 0.2.3 → 0.2.8
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 +56 -23
- data/.github/workflows/nmatrix.yml +67 -0
- data/.github/workflows/pycall.yml +86 -0
- data/Gemfile +18 -0
- data/README.md +172 -4
- data/Rakefile +4 -5
- data/charty.gemspec +10 -6
- data/examples/sample_images/hist_gruff.png +0 -0
- 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 +8 -1
- data/lib/charty/backends/bokeh.rb +2 -2
- data/lib/charty/backends/google_charts.rb +1 -1
- data/lib/charty/backends/gruff.rb +14 -3
- data/lib/charty/backends/plotly.rb +731 -32
- data/lib/charty/backends/plotly_helpers/html_renderer.rb +203 -0
- data/lib/charty/backends/plotly_helpers/notebook_renderer.rb +87 -0
- data/lib/charty/backends/plotly_helpers/plotly_renderer.rb +121 -0
- data/lib/charty/backends/pyplot.rb +514 -66
- data/lib/charty/backends/rubyplot.rb +1 -1
- data/lib/charty/cache_dir.rb +27 -0
- data/lib/charty/dash_pattern_generator.rb +57 -0
- data/lib/charty/index.rb +213 -0
- data/lib/charty/iruby_helper.rb +18 -0
- data/lib/charty/linspace.rb +1 -1
- data/lib/charty/plot_methods.rb +283 -8
- data/lib/charty/plotter.rb +2 -2
- data/lib/charty/plotters.rb +11 -0
- data/lib/charty/plotters/abstract_plotter.rb +186 -16
- data/lib/charty/plotters/bar_plotter.rb +189 -7
- data/lib/charty/plotters/box_plotter.rb +64 -11
- data/lib/charty/plotters/categorical_plotter.rb +272 -40
- data/lib/charty/plotters/count_plotter.rb +7 -0
- data/lib/charty/plotters/distribution_plotter.rb +143 -0
- data/lib/charty/plotters/estimation_support.rb +84 -0
- data/lib/charty/plotters/histogram_plotter.rb +186 -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 +96 -2
- data/lib/charty/table.rb +160 -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 +39 -3
- data/lib/charty/table_adapters/datasets_adapter.rb +13 -2
- data/lib/charty/table_adapters/hash_adapter.rb +141 -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 +92 -25
data/Rakefile
CHANGED
@@ -1,10 +1,9 @@
|
|
1
1
|
require "bundler/gem_tasks"
|
2
2
|
require "rake/testtask"
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
t.test_files = FileList['test/**/*_test.rb']
|
4
|
+
desc "Run tests"
|
5
|
+
task :test do
|
6
|
+
ruby("test/run.rb")
|
8
7
|
end
|
9
8
|
|
10
|
-
task :
|
9
|
+
task default: :test
|
data/charty.gemspec
CHANGED
@@ -26,16 +26,20 @@ Gem::Specification.new do |spec|
|
|
26
26
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
27
27
|
spec.require_paths = ["lib"]
|
28
28
|
|
29
|
-
spec.add_dependency "red-colors"
|
30
|
-
spec.add_dependency "red-
|
29
|
+
spec.add_dependency "red-colors", ">= 0.3.0"
|
30
|
+
spec.add_dependency "red-datasets", ">= 0.1.2"
|
31
|
+
spec.add_dependency "red-palette", ">= 0.5.0"
|
32
|
+
|
33
|
+
spec.add_dependency "matplotlib", ">= 1.2.0"
|
34
|
+
spec.add_dependency "pandas", ">= 0.3.5"
|
35
|
+
spec.add_dependency "playwright-ruby-client"
|
36
|
+
|
31
37
|
spec.add_development_dependency "bundler", ">= 1.16"
|
32
38
|
spec.add_development_dependency "rake"
|
33
39
|
spec.add_development_dependency "test-unit"
|
34
|
-
spec.add_development_dependency "numo-narray"
|
35
|
-
spec.add_development_dependency "nmatrix"
|
36
|
-
spec.add_development_dependency "red-datasets", ">= 0.0.9"
|
37
40
|
spec.add_development_dependency "daru"
|
41
|
+
spec.add_development_dependency "matrix" # need for daru on Ruby > 3.0
|
38
42
|
spec.add_development_dependency "activerecord"
|
39
43
|
spec.add_development_dependency "sqlite3"
|
40
|
-
spec.add_development_dependency "
|
44
|
+
spec.add_development_dependency "iruby", ">= 0.7.0"
|
41
45
|
end
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
data/lib/charty.rb
CHANGED
@@ -3,13 +3,20 @@ require_relative "charty/version"
|
|
3
3
|
require "colors"
|
4
4
|
require "palette"
|
5
5
|
|
6
|
+
require_relative "charty/cache_dir"
|
7
|
+
require_relative "charty/util"
|
8
|
+
require_relative "charty/iruby_helper"
|
9
|
+
require_relative "charty/dash_pattern_generator"
|
6
10
|
require_relative "charty/backends"
|
7
11
|
require_relative "charty/backend_methods"
|
8
12
|
require_relative "charty/plotter"
|
13
|
+
require_relative "charty/index"
|
9
14
|
require_relative "charty/layout"
|
10
15
|
require_relative "charty/linspace"
|
11
16
|
require_relative "charty/plotters"
|
12
17
|
require_relative "charty/plot_methods"
|
13
|
-
require_relative "charty/table_adapters"
|
14
18
|
require_relative "charty/table"
|
19
|
+
require_relative "charty/table_adapters"
|
15
20
|
require_relative "charty/statistics"
|
21
|
+
require_relative "charty/vector_adapters"
|
22
|
+
require_relative "charty/vector"
|
@@ -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
|
@@ -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,10 @@
|
|
1
|
-
require
|
1
|
+
require "json"
|
2
|
+
require "securerandom"
|
3
|
+
require "tmpdir"
|
4
|
+
|
5
|
+
require_relative "plotly_helpers/html_renderer"
|
6
|
+
require_relative "plotly_helpers/notebook_renderer"
|
7
|
+
require_relative "plotly_helpers/plotly_renderer"
|
2
8
|
|
3
9
|
module Charty
|
4
10
|
module Backends
|
@@ -35,7 +41,7 @@ module Charty
|
|
35
41
|
@series = series
|
36
42
|
end
|
37
43
|
|
38
|
-
def
|
44
|
+
def old_style_render(context, filename)
|
39
45
|
plot(nil, context)
|
40
46
|
end
|
41
47
|
|
@@ -117,32 +123,489 @@ module Charty
|
|
117
123
|
|
118
124
|
def begin_figure
|
119
125
|
@traces = []
|
120
|
-
@layout = {}
|
126
|
+
@layout = {showlegend: false}
|
121
127
|
end
|
122
128
|
|
123
|
-
def bar(bar_pos, values,
|
124
|
-
|
125
|
-
|
129
|
+
def bar(bar_pos, group_names, values, colors, orient, label: nil, width: 0.8r,
|
130
|
+
align: :center, conf_int: nil, error_colors: nil, error_width: nil, cap_size: nil)
|
131
|
+
bar_pos = Array(bar_pos)
|
132
|
+
values = Array(values)
|
133
|
+
colors = Array(colors).map(&:to_hex_string)
|
134
|
+
|
135
|
+
if orient == :v
|
136
|
+
x, y = bar_pos, values
|
137
|
+
x = group_names unless group_names.nil?
|
138
|
+
else
|
139
|
+
x, y = values, bar_pos
|
140
|
+
y = group_names unless group_names.nil?
|
141
|
+
end
|
142
|
+
|
143
|
+
trace = {
|
126
144
|
type: :bar,
|
127
|
-
|
128
|
-
|
129
|
-
|
145
|
+
orientation: orient,
|
146
|
+
x: x,
|
147
|
+
y: y,
|
148
|
+
width: width,
|
149
|
+
marker: {color: colors}
|
130
150
|
}
|
131
|
-
|
151
|
+
trace[:name] = label unless label.nil?
|
152
|
+
|
153
|
+
unless conf_int.nil?
|
154
|
+
errors_low = conf_int.map.with_index {|(low, _), i| values[i] - low }
|
155
|
+
errors_high = conf_int.map.with_index {|(_, high), i| high - values[i] }
|
156
|
+
|
157
|
+
error_bar = {
|
158
|
+
type: :data,
|
159
|
+
visible: true,
|
160
|
+
symmetric: false,
|
161
|
+
array: errors_high,
|
162
|
+
arrayminus: errors_low,
|
163
|
+
color: error_colors[0].to_hex_string
|
164
|
+
}
|
165
|
+
error_bar[:thickness] = error_width unless error_width.nil?
|
166
|
+
error_bar[:width] = cap_size unless cap_size.nil?
|
167
|
+
|
168
|
+
error_bar_key = orient == :v ? :error_y : :error_x
|
169
|
+
trace[error_bar_key] = error_bar
|
170
|
+
end
|
171
|
+
|
172
|
+
@traces << trace
|
173
|
+
|
174
|
+
if group_names
|
175
|
+
@layout[:barmode] = :group
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def box_plot(plot_data, group_names,
|
180
|
+
orient:, colors:, gray:, dodge:, width: 0.8r,
|
181
|
+
flier_size: 5, whisker: 1.5, notch: false)
|
182
|
+
colors = Array(colors).map(&:to_hex_string)
|
183
|
+
gray = gray.to_hex_string
|
184
|
+
width = Float(width)
|
185
|
+
flier_size = Float(width)
|
186
|
+
whisker = Float(whisker)
|
187
|
+
|
188
|
+
traces = plot_data.map.with_index do |group_data, i|
|
189
|
+
group_data = Array(group_data)
|
190
|
+
trace = {
|
191
|
+
type: :box,
|
192
|
+
orientation: orient,
|
193
|
+
name: group_names[i],
|
194
|
+
marker: {color: colors[i]}
|
195
|
+
}
|
196
|
+
if orient == :v
|
197
|
+
trace.update(y: group_data)
|
198
|
+
else
|
199
|
+
trace.update(x: group_data)
|
200
|
+
end
|
201
|
+
|
202
|
+
trace
|
203
|
+
end
|
204
|
+
|
205
|
+
traces.reverse! if orient == :h
|
206
|
+
|
207
|
+
@traces.concat(traces)
|
208
|
+
end
|
209
|
+
|
210
|
+
def grouped_box_plot(plot_data, group_names, color_names,
|
211
|
+
orient:, colors:, gray:, dodge:, width: 0.8r,
|
212
|
+
flier_size: 5, whisker: 1.5, notch: false)
|
213
|
+
colors = Array(colors).map(&:to_hex_string)
|
214
|
+
gray = gray.to_hex_string
|
215
|
+
width = Float(width)
|
216
|
+
flier_size = Float(width)
|
217
|
+
whisker = Float(whisker)
|
218
|
+
|
219
|
+
@layout[:boxmode] = :group
|
220
|
+
|
221
|
+
if orient == :h
|
222
|
+
@layout[:xaxis] ||= {}
|
223
|
+
@layout[:xaxis][:zeroline] = false
|
224
|
+
|
225
|
+
plot_data = plot_data.map {|d| d.reverse }
|
226
|
+
group_names = group_names.reverse
|
227
|
+
end
|
228
|
+
|
229
|
+
traces = color_names.map.with_index do |color_name, i|
|
230
|
+
group_keys = group_names.flat_map.with_index { |name, j|
|
231
|
+
Array.new(plot_data[i][j].length, name)
|
232
|
+
}.flatten
|
233
|
+
|
234
|
+
values = plot_data[i].flat_map {|d| Array(d) }
|
235
|
+
|
236
|
+
trace = {
|
237
|
+
type: :box,
|
238
|
+
orientation: orient,
|
239
|
+
name: color_name,
|
240
|
+
marker: {color: colors[i]}
|
241
|
+
}
|
242
|
+
|
243
|
+
if orient == :v
|
244
|
+
trace.update(y: values, x: group_keys)
|
245
|
+
else
|
246
|
+
trace.update(x: values, y: group_keys)
|
247
|
+
end
|
248
|
+
|
249
|
+
trace
|
250
|
+
end
|
251
|
+
|
252
|
+
@traces.concat(traces)
|
253
|
+
end
|
254
|
+
|
255
|
+
def scatter(x, y, variables, color:, color_mapper:,
|
256
|
+
style:, style_mapper:, size:, size_mapper:)
|
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,
|
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, 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 do |x|
|
337
|
+
scale_scatter_point_size(x).to_f
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
name = []
|
342
|
+
legend_title = []
|
343
|
+
|
344
|
+
if group_key.key?(:color)
|
345
|
+
trace[:marker][:color] = color_mapper[group_key[:color]].to_hex_string
|
346
|
+
name << group_key[:color]
|
347
|
+
legend_title << variables[:color]
|
348
|
+
end
|
349
|
+
|
350
|
+
if group_key.key?(:style)
|
351
|
+
trace[:marker][:symbol] = style_mapper[group_key[:style], :marker]
|
352
|
+
name << group_key[:style]
|
353
|
+
legend_title << variables[:style]
|
354
|
+
end
|
355
|
+
|
356
|
+
trace[:name] = name.uniq.join(", ") unless name.empty?
|
357
|
+
|
358
|
+
@traces << trace
|
359
|
+
|
360
|
+
unless legend_title.empty?
|
361
|
+
@layout[:legend] ||= {}
|
362
|
+
@layout[:legend][:title] = {text: legend_title.uniq.join(", ")}
|
363
|
+
end
|
364
|
+
end
|
132
365
|
end
|
133
366
|
|
134
|
-
def
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
367
|
+
def add_scatter_plot_legend(variables, color_mapper, size_mapper, style_mapper, legend)
|
368
|
+
if legend == :full
|
369
|
+
warn("Plotly backend does not support full verbosity legend")
|
370
|
+
end
|
371
|
+
end
|
372
|
+
|
373
|
+
private def scale_scatter_point_size(x)
|
374
|
+
min = 6
|
375
|
+
max = 12
|
376
|
+
|
377
|
+
min + x * (max - min)
|
378
|
+
end
|
379
|
+
|
380
|
+
def line(x, y, variables, color:, color_mapper:, size:, size_mapper:, style:, style_mapper:, ci_params:)
|
381
|
+
x = case x
|
382
|
+
when Charty::Vector
|
383
|
+
x.to_a
|
384
|
+
else
|
385
|
+
orig_x, x = x, Array.try_convert(x)
|
386
|
+
if x.nil?
|
387
|
+
raise ArgumentError, "Invalid value for x: %p" % orig_x
|
388
|
+
end
|
389
|
+
end
|
390
|
+
|
391
|
+
y = case y
|
392
|
+
when Charty::Vector
|
393
|
+
y.to_a
|
394
|
+
else
|
395
|
+
orig_y, y = y, Array.try_convert(y)
|
396
|
+
if y.nil?
|
397
|
+
raise ArgumentError, "Invalid value for y: %p" % orig_y
|
398
|
+
end
|
399
|
+
end
|
400
|
+
|
401
|
+
name = []
|
402
|
+
legend_title = []
|
403
|
+
|
404
|
+
if color.nil?
|
405
|
+
# TODO: do not hard code this
|
406
|
+
line_color = Colors["#1f77b4"] # the first color of D3's category10 palette
|
407
|
+
else
|
408
|
+
line_color = color_mapper[color].to_rgb
|
409
|
+
name << color
|
410
|
+
legend_title << variables[:color]
|
411
|
+
end
|
412
|
+
|
413
|
+
unless style.nil?
|
414
|
+
marker, dashes = style_mapper[style].values_at(:marker, :dashes)
|
415
|
+
name << style
|
416
|
+
legend_title << variables[:style]
|
417
|
+
end
|
418
|
+
|
419
|
+
trace = {
|
420
|
+
type: :scatter,
|
421
|
+
mode: marker.nil? ? "lines" : "lines+markers",
|
422
|
+
x: x,
|
423
|
+
y: y,
|
424
|
+
line: {
|
425
|
+
shape: :linear,
|
426
|
+
color: line_color.to_hex_string
|
427
|
+
}
|
428
|
+
}
|
429
|
+
|
430
|
+
default_line_width = 2.0
|
431
|
+
unless size.nil?
|
432
|
+
line_width = default_line_width + 2.0 * size_mapper[size]
|
433
|
+
trace[:line][:width] = line_width
|
434
|
+
end
|
435
|
+
|
436
|
+
unless dashes.nil?
|
437
|
+
trace[:line][:dash] = convert_dash_pattern(dashes, line_width || default_line_width)
|
438
|
+
end
|
439
|
+
|
440
|
+
unless marker.nil?
|
441
|
+
trace[:marker] = {
|
442
|
+
line: {
|
443
|
+
width: 1,
|
444
|
+
color: "#fff"
|
445
|
+
},
|
446
|
+
symbol: marker,
|
447
|
+
size: 10
|
448
|
+
}
|
449
|
+
end
|
450
|
+
|
451
|
+
unless ci_params.nil?
|
452
|
+
case ci_params[:style]
|
453
|
+
when :band
|
454
|
+
y_min = ci_params[:y_min].to_a
|
455
|
+
y_max = ci_params[:y_max].to_a
|
456
|
+
@traces << {
|
457
|
+
type: :scatter,
|
458
|
+
x: x,
|
459
|
+
y: y_max,
|
460
|
+
mode: :lines,
|
461
|
+
line: { shape: :linear, width: 0 },
|
462
|
+
showlegend: false
|
463
|
+
}
|
464
|
+
@traces << {
|
465
|
+
type: :scatter,
|
466
|
+
x: x,
|
467
|
+
y: y_min,
|
468
|
+
mode: :lines,
|
469
|
+
line: { shape: :linear, width: 0 },
|
470
|
+
fill: :tonexty,
|
471
|
+
fillcolor: line_color.to_rgba(alpha: 0.2).to_hex_string,
|
472
|
+
showlegend: false
|
473
|
+
}
|
474
|
+
when :bars
|
475
|
+
y_min = ci_params[:y_min].map.with_index {|v, i| y[i] - v }
|
476
|
+
y_max = ci_params[:y_max].map.with_index {|v, i| v - y[i] }
|
477
|
+
trace[:error_y] = {
|
478
|
+
visible: true,
|
479
|
+
type: :data,
|
480
|
+
array: y_max,
|
481
|
+
arrayminus: y_min
|
482
|
+
}
|
483
|
+
unless line_color.nil?
|
484
|
+
trace[:error_y][:color] = line_color
|
485
|
+
end
|
486
|
+
unless line_width.nil?
|
487
|
+
trace[:error_y][:thickness] = line_width
|
488
|
+
end
|
489
|
+
end
|
490
|
+
end
|
491
|
+
|
492
|
+
trace[:name] = name.uniq.join(", ") unless name.empty?
|
493
|
+
|
494
|
+
@traces << trace
|
495
|
+
|
496
|
+
unless legend_title.empty?
|
497
|
+
@layout[:showlegend] = true
|
498
|
+
@layout[:legend] ||= {}
|
499
|
+
@layout[:legend][:title] = {text: legend_title.uniq.join(", ")}
|
500
|
+
end
|
501
|
+
end
|
502
|
+
|
503
|
+
def add_line_plot_legend(variables, color_mapper, size_mapper, style_mapper, legend)
|
504
|
+
if legend == :full
|
505
|
+
warn("Plotly backend does not support full verbosity legend")
|
506
|
+
end
|
507
|
+
|
508
|
+
legend_order = if variables.key?(:color)
|
509
|
+
if variables.key?(:style)
|
510
|
+
# both color and style
|
511
|
+
color_mapper.levels.product(style_mapper.levels)
|
512
|
+
else
|
513
|
+
# only color
|
514
|
+
color_mapper.levels
|
515
|
+
end
|
516
|
+
elsif variables.key?(:style)
|
517
|
+
# only style
|
518
|
+
style_mapper.levels
|
519
|
+
else
|
520
|
+
# no legend entries
|
521
|
+
nil
|
522
|
+
end
|
523
|
+
|
524
|
+
if legend_order
|
525
|
+
# sort traces
|
526
|
+
legend_index = legend_order.map.with_index { |name, i|
|
527
|
+
[Array(name).uniq.join(", "), i]
|
528
|
+
}.to_h
|
529
|
+
@traces = @traces.each_with_index.sort_by { |trace, trace_index|
|
530
|
+
index = legend_index.fetch(trace[:name], legend_order.length)
|
531
|
+
[index, trace_index]
|
532
|
+
}.map(&:first)
|
533
|
+
|
534
|
+
# remove duplicated legend entries
|
535
|
+
names = {}
|
536
|
+
@traces.each do |trace|
|
537
|
+
if trace[:showlegend] != false
|
538
|
+
name = trace[:name]
|
539
|
+
if name
|
540
|
+
if names.key?(name)
|
541
|
+
# Hide duplications
|
542
|
+
trace[:showlegend] = false
|
543
|
+
else
|
544
|
+
trace[:showlegend] = true
|
545
|
+
names[name] = true
|
546
|
+
end
|
547
|
+
else
|
548
|
+
# Hide no name trace in legend
|
549
|
+
trace[:showlegend] = false
|
550
|
+
end
|
551
|
+
end
|
552
|
+
end
|
553
|
+
end
|
554
|
+
end
|
555
|
+
|
556
|
+
private def convert_dash_pattern(pattern, line_width)
|
557
|
+
case pattern
|
558
|
+
when ""
|
559
|
+
:solid
|
560
|
+
else
|
561
|
+
pattern.map {|d| "#{line_width * d}px" }.join(",")
|
562
|
+
end
|
563
|
+
end
|
564
|
+
|
565
|
+
PLOTLY_HISTNORM = {
|
566
|
+
count: "".freeze,
|
567
|
+
frequency: "density".freeze,
|
568
|
+
density: "probability density".freeze,
|
569
|
+
probability: "probability".freeze
|
570
|
+
}.freeze
|
571
|
+
|
572
|
+
def univariate_histogram(data, name, variable_name, stat,
|
573
|
+
bin_start, bin_end, bin_size, alpha,
|
574
|
+
color, color_mapper)
|
575
|
+
orientation = case variable_name
|
576
|
+
when :x
|
577
|
+
:v
|
578
|
+
else
|
579
|
+
:h
|
580
|
+
end
|
581
|
+
trace = {
|
582
|
+
type: "histogram",
|
583
|
+
name: name.to_s,
|
584
|
+
variable_name => data.to_a,
|
585
|
+
orientation: orientation,
|
586
|
+
histnorm: PLOTLY_HISTNORM[stat],
|
587
|
+
"#{variable_name}bins": {
|
588
|
+
start: bin_start,
|
589
|
+
end: bin_end,
|
590
|
+
size: bin_size
|
591
|
+
},
|
592
|
+
opacity: alpha
|
593
|
+
}
|
594
|
+
|
595
|
+
if color
|
596
|
+
trace[:marker] = {
|
597
|
+
color: color_mapper[color].to_rgb.to_hex_string
|
598
|
+
}
|
599
|
+
end
|
600
|
+
|
601
|
+
@traces << trace
|
602
|
+
|
603
|
+
@layout[:bargap] = 0.05
|
604
|
+
|
605
|
+
if @traces.length > 1
|
606
|
+
@layout[:barmode] = "overlay"
|
607
|
+
@layout[:showlegend] = true
|
144
608
|
end
|
145
|
-
@layout[:showlegend] = false
|
146
609
|
end
|
147
610
|
|
148
611
|
def set_xlabel(label)
|
@@ -161,45 +624,226 @@ module Charty
|
|
161
624
|
@layout[:xaxis][:tickvals] = values
|
162
625
|
end
|
163
626
|
|
627
|
+
def set_yticks(values)
|
628
|
+
@layout[:yaxis] ||= {}
|
629
|
+
@layout[:yaxis][:tickmode] = "array"
|
630
|
+
@layout[:yaxis][:tickvals] = values
|
631
|
+
end
|
632
|
+
|
164
633
|
def set_xtick_labels(labels)
|
165
634
|
@layout[:xaxis] ||= {}
|
166
635
|
@layout[:xaxis][:tickmode] = "array"
|
167
636
|
@layout[:xaxis][:ticktext] = labels
|
168
637
|
end
|
169
638
|
|
639
|
+
def set_ytick_labels(labels)
|
640
|
+
@layout[:yaxis] ||= {}
|
641
|
+
@layout[:yaxis][:tickmode] = "array"
|
642
|
+
@layout[:yaxis][:ticktext] = labels
|
643
|
+
end
|
644
|
+
|
170
645
|
def set_xlim(min, max)
|
171
646
|
@layout[:xaxis] ||= {}
|
172
647
|
@layout[:xaxis][:range] = [min, max]
|
173
648
|
end
|
174
649
|
|
650
|
+
def set_ylim(min, max)
|
651
|
+
@layout[:yaxis] ||= {}
|
652
|
+
@layout[:yaxis][:range] = [min, max]
|
653
|
+
end
|
654
|
+
|
175
655
|
def disable_xaxis_grid
|
176
656
|
# do nothing
|
177
657
|
end
|
178
658
|
|
179
|
-
def
|
180
|
-
|
181
|
-
|
182
|
-
|
659
|
+
def disable_yaxis_grid
|
660
|
+
# do nothing
|
661
|
+
end
|
662
|
+
|
663
|
+
def invert_yaxis
|
664
|
+
@traces.each do |trace|
|
665
|
+
case trace[:type]
|
666
|
+
when :bar
|
667
|
+
trace[:y].reverse!
|
668
|
+
end
|
183
669
|
end
|
184
670
|
|
185
|
-
|
671
|
+
if @layout[:boxmode] == :group
|
672
|
+
@traces.reverse!
|
673
|
+
end
|
674
|
+
|
675
|
+
if @layout[:yaxis] && @layout[:yaxis][:ticktext]
|
676
|
+
@layout[:yaxis][:ticktext].reverse!
|
677
|
+
end
|
678
|
+
end
|
679
|
+
|
680
|
+
def legend(loc:, title:)
|
681
|
+
@layout[:showlegend] = true
|
682
|
+
@layout[:legend] = {
|
683
|
+
title: {
|
684
|
+
text: title
|
685
|
+
}
|
686
|
+
}
|
687
|
+
# TODO: Handle loc
|
688
|
+
end
|
689
|
+
|
690
|
+
def save(filename, format: nil, title: nil, width: 700, height: 500, **kwargs)
|
691
|
+
format = detect_format(filename) if format.nil?
|
692
|
+
|
693
|
+
case format
|
694
|
+
when nil, :html, "text/html"
|
695
|
+
save_html(filename, title: title, **kwargs)
|
696
|
+
when :png, "png", "image/png",
|
697
|
+
:jpeg, "jpeg", "image/jpeg"
|
698
|
+
render_image(format, filename: filename, notebook: false, title: title, width: width, height: height, **kwargs)
|
699
|
+
end
|
700
|
+
nil
|
701
|
+
end
|
186
702
|
|
703
|
+
private def detect_format(filename)
|
704
|
+
case File.extname(filename).downcase
|
705
|
+
when ".htm", ".html"
|
706
|
+
:html
|
707
|
+
when ".png"
|
708
|
+
:png
|
709
|
+
when ".jpg", ".jpeg"
|
710
|
+
:jpeg
|
711
|
+
else
|
712
|
+
raise ArgumentError,
|
713
|
+
"Unable to infer file type from filename: %p" % filename
|
714
|
+
end
|
715
|
+
end
|
716
|
+
|
717
|
+
private def save_html(filename, title:, element_id: nil)
|
187
718
|
html = <<~HTML
|
719
|
+
<!DOCTYPE html>
|
720
|
+
<html>
|
721
|
+
<head>
|
722
|
+
<meta charset="utf-8">
|
723
|
+
<title>%{title}</title>
|
724
|
+
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
|
725
|
+
</head>
|
726
|
+
<body>
|
188
727
|
<div id="%{id}" style="width: 100%%; height:100%%;"></div>
|
189
728
|
<script type="text/javascript">
|
190
|
-
|
191
|
-
Plotly.newPlot("%{id}", %{data}, %{layout});
|
192
|
-
});
|
729
|
+
Plotly.newPlot("%{id}", %{data}, %{layout});
|
193
730
|
</script>
|
731
|
+
</body>
|
732
|
+
</html>
|
194
733
|
HTML
|
195
734
|
|
735
|
+
element_id = SecureRandom.uuid if element_id.nil?
|
736
|
+
|
196
737
|
html %= {
|
197
|
-
|
738
|
+
title: title || default_html_title,
|
739
|
+
id: element_id,
|
198
740
|
data: JSON.dump(@traces),
|
199
741
|
layout: JSON.dump(@layout)
|
200
742
|
}
|
201
|
-
|
202
|
-
|
743
|
+
File.write(filename, html)
|
744
|
+
end
|
745
|
+
|
746
|
+
private def default_html_title
|
747
|
+
"Charty plot"
|
748
|
+
end
|
749
|
+
|
750
|
+
def render(element_id: nil, format: nil, notebook: false)
|
751
|
+
case format
|
752
|
+
when :html, "html", nil
|
753
|
+
format = "text/html"
|
754
|
+
when :png, "png"
|
755
|
+
format = "image/png"
|
756
|
+
when :jpeg, "jpeg"
|
757
|
+
format = "image/jpeg"
|
758
|
+
end
|
759
|
+
|
760
|
+
case format
|
761
|
+
when "text/html"
|
762
|
+
# render html after this case cause
|
763
|
+
when "image/png", "image/jpeg"
|
764
|
+
image_data = render_image(format, element_id: element_id, notebook: false)
|
765
|
+
if notebook
|
766
|
+
return [format, image_data]
|
767
|
+
else
|
768
|
+
return image_data
|
769
|
+
end
|
770
|
+
else
|
771
|
+
raise ArgumentError,
|
772
|
+
"Unsupported mime type to render: %p" % format
|
773
|
+
end
|
774
|
+
|
775
|
+
element_id = SecureRandom.uuid if element_id.nil?
|
776
|
+
|
777
|
+
renderer = PlotlyHelpers::HtmlRenderer.new(full_html: !notebook)
|
778
|
+
html = renderer.render({data: @traces, layout: @layout}, element_id: element_id)
|
779
|
+
if notebook
|
780
|
+
[format, html]
|
781
|
+
else
|
782
|
+
html
|
783
|
+
end
|
784
|
+
end
|
785
|
+
|
786
|
+
def render_mimebundle(include: [], exclude: [])
|
787
|
+
types = case
|
788
|
+
when IRubyHelper.vscode?,
|
789
|
+
IRubyHelper.nteract?
|
790
|
+
[:plotly_mimetype]
|
791
|
+
else
|
792
|
+
[:plotly_mimetype, :notebook]
|
793
|
+
end
|
794
|
+
bundle = Util.filter_map(types) { |type|
|
795
|
+
case type
|
796
|
+
when :plotly_mimetype
|
797
|
+
render_plotly_mimetype_bundle
|
798
|
+
when :notebook
|
799
|
+
render_notebook_bundle
|
800
|
+
end
|
801
|
+
}.to_h
|
802
|
+
bundle
|
803
|
+
end
|
804
|
+
|
805
|
+
private def render_plotly_mimetype_bundle
|
806
|
+
renderer = PlotlyHelpers::PlotlyRenderer.new
|
807
|
+
obj = renderer.render({data: @traces, layout: @layout})
|
808
|
+
[ "application/vnd.plotly.v1+json", obj ]
|
809
|
+
end
|
810
|
+
|
811
|
+
private def render_notebook_bundle
|
812
|
+
renderer = self.class.notebook_renderer
|
813
|
+
renderer.activate
|
814
|
+
html = renderer.render({data: @traces, layout: @layout})
|
815
|
+
[ "text/html", html ]
|
816
|
+
end
|
817
|
+
|
818
|
+
# for new APIs
|
819
|
+
def self.notebook_renderer
|
820
|
+
@notebook_renderer ||= PlotlyHelpers::NotebookRenderer.new
|
821
|
+
end
|
822
|
+
|
823
|
+
private def render_image(format=nil, filename: nil, element_id: nil, notebook: false,
|
824
|
+
title: nil, width: nil, height: nil)
|
825
|
+
format = "image/png" if format.nil?
|
826
|
+
case format
|
827
|
+
when :png, "png", :jpeg, "jpeg"
|
828
|
+
image_type = format.to_s
|
829
|
+
when "image/png", "image/jpeg"
|
830
|
+
image_type = format.split("/").last
|
831
|
+
else
|
832
|
+
raise ArgumentError,
|
833
|
+
"Unsupported mime type to render image: %p" % format
|
834
|
+
end
|
835
|
+
|
836
|
+
height = 525 if height.nil?
|
837
|
+
width = (height * Math.sqrt(2)).to_i if width.nil?
|
838
|
+
title = "Charty plot" if title.nil?
|
839
|
+
|
840
|
+
element_id = SecureRandom.uuid if element_id.nil?
|
841
|
+
element_id = "charty-plotly-#{element_id}"
|
842
|
+
Dir.mktmpdir do |tmpdir|
|
843
|
+
html_filename = File.join(tmpdir, "%s.html" % element_id)
|
844
|
+
save_html(html_filename, title: title, element_id: element_id)
|
845
|
+
return self.class.render_image(html_filename, filename, image_type, element_id, width, height)
|
846
|
+
end
|
203
847
|
end
|
204
848
|
|
205
849
|
module IRubyOutput
|
@@ -242,6 +886,61 @@ module Charty
|
|
242
886
|
END
|
243
887
|
end
|
244
888
|
end
|
889
|
+
|
890
|
+
@playwright_fiber = nil
|
891
|
+
|
892
|
+
def self.ensure_playwright
|
893
|
+
if @playwright_fiber.nil?
|
894
|
+
begin
|
895
|
+
require "playwright"
|
896
|
+
rescue LoadError
|
897
|
+
$stderr.puts "ERROR: You need to install playwright and playwright-ruby-client before using Plotly renderer"
|
898
|
+
raise
|
899
|
+
end
|
900
|
+
|
901
|
+
@playwright_fiber = Fiber.new do
|
902
|
+
playwright_cli_executable_path = ENV.fetch("PLAYWRIGHT_CLI_EXECUTABLE_PATH", "npx playwright")
|
903
|
+
Playwright.create(playwright_cli_executable_path: playwright_cli_executable_path) do |playwright|
|
904
|
+
playwright.chromium.launch(headless: true) do |browser|
|
905
|
+
request = Fiber.yield
|
906
|
+
loop do
|
907
|
+
result = nil
|
908
|
+
case request.shift
|
909
|
+
when :finish
|
910
|
+
break
|
911
|
+
when :render
|
912
|
+
input, output, format, element_id, width, height = request
|
913
|
+
|
914
|
+
page = browser.new_page
|
915
|
+
page.set_viewport_size(width: width, height: height)
|
916
|
+
page.goto("file://#{input}")
|
917
|
+
element = page.query_selector("\##{element_id}")
|
918
|
+
|
919
|
+
kwargs = {type: format}
|
920
|
+
kwargs[:path] = output unless output.nil?
|
921
|
+
result = element.screenshot(**kwargs)
|
922
|
+
end
|
923
|
+
request = Fiber.yield(result)
|
924
|
+
end
|
925
|
+
end
|
926
|
+
end
|
927
|
+
end
|
928
|
+
@playwright_fiber.resume
|
929
|
+
end
|
930
|
+
end
|
931
|
+
|
932
|
+
def self.terminate_playwright
|
933
|
+
return if @playwright_fiber.nil?
|
934
|
+
|
935
|
+
@playwright_fiber.resume([:finish])
|
936
|
+
end
|
937
|
+
|
938
|
+
at_exit { terminate_playwright }
|
939
|
+
|
940
|
+
def self.render_image(input, output, format, element_id, width, height)
|
941
|
+
ensure_playwright if @playwright_fiber.nil?
|
942
|
+
@playwright_fiber.resume([:render, input, output, format.to_s, element_id, width, height])
|
943
|
+
end
|
245
944
|
end
|
246
945
|
end
|
247
946
|
end
|