charty 0.2.4 → 0.2.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +64 -15
  3. data/charty.gemspec +10 -3
  4. data/lib/charty.rb +5 -2
  5. data/lib/charty/backends/bokeh.rb +2 -2
  6. data/lib/charty/backends/google_charts.rb +1 -1
  7. data/lib/charty/backends/gruff.rb +1 -1
  8. data/lib/charty/backends/plotly.rb +434 -32
  9. data/lib/charty/backends/plotly_helpers/html_renderer.rb +203 -0
  10. data/lib/charty/backends/plotly_helpers/notebook_renderer.rb +87 -0
  11. data/lib/charty/backends/plotly_helpers/plotly_renderer.rb +121 -0
  12. data/lib/charty/backends/pyplot.rb +187 -48
  13. data/lib/charty/backends/rubyplot.rb +1 -1
  14. data/lib/charty/cache_dir.rb +27 -0
  15. data/lib/charty/dash_pattern_generator.rb +57 -0
  16. data/lib/charty/index.rb +1 -1
  17. data/lib/charty/iruby_helper.rb +18 -0
  18. data/lib/charty/plot_methods.rb +115 -3
  19. data/lib/charty/plotter.rb +2 -2
  20. data/lib/charty/plotters.rb +4 -0
  21. data/lib/charty/plotters/abstract_plotter.rb +106 -11
  22. data/lib/charty/plotters/bar_plotter.rb +1 -16
  23. data/lib/charty/plotters/box_plotter.rb +1 -16
  24. data/lib/charty/plotters/distribution_plotter.rb +150 -0
  25. data/lib/charty/plotters/histogram_plotter.rb +242 -0
  26. data/lib/charty/plotters/line_plotter.rb +300 -0
  27. data/lib/charty/plotters/relational_plotter.rb +213 -96
  28. data/lib/charty/plotters/scatter_plotter.rb +8 -43
  29. data/lib/charty/statistics.rb +11 -2
  30. data/lib/charty/table.rb +124 -14
  31. data/lib/charty/table_adapters/base_adapter.rb +97 -0
  32. data/lib/charty/table_adapters/daru_adapter.rb +2 -0
  33. data/lib/charty/table_adapters/datasets_adapter.rb +7 -0
  34. data/lib/charty/table_adapters/hash_adapter.rb +19 -3
  35. data/lib/charty/table_adapters/pandas_adapter.rb +82 -0
  36. data/lib/charty/util.rb +28 -0
  37. data/lib/charty/vector_adapters.rb +5 -1
  38. data/lib/charty/vector_adapters/array_adapter.rb +2 -10
  39. data/lib/charty/vector_adapters/daru_adapter.rb +3 -11
  40. data/lib/charty/vector_adapters/narray_adapter.rb +1 -6
  41. data/lib/charty/vector_adapters/numpy_adapter.rb +1 -1
  42. data/lib/charty/vector_adapters/pandas_adapter.rb +0 -1
  43. data/lib/charty/version.rb +1 -1
  44. metadata +104 -11
  45. data/lib/charty/missing_value_support.rb +0 -14
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6d70645a4ca621ee142714f3079e85dc843585fa6364c4476d4fdd86761c3b6f
4
- data.tar.gz: b4fa589a9b2c5ba31a6869399c5e90e79767d9e8e4a9a0bf0c5e2d4997725dbf
3
+ metadata.gz: 37ead1e86203bd6580125f4ed97039c6b512e9ae3037a0bc386743ee436f184c
4
+ data.tar.gz: a94e66292a866acde67c0cfc51bd8eebd5840af68e2d4112caafce80bdd37ed2
5
5
  SHA512:
6
- metadata.gz: 6588c1c0105d0994cd024d08f328a6bf10241975abf4ee1b332be0a956f32d7073aed9867b5ddf708ef578e570caf733d422153a479ecae6fedef81daab391c1
7
- data.tar.gz: 47999e4ce59f7cfb4263a5002b3dc070c128971257d7fed81fc23bd7314632cd441bba2ac93a0fdbeb3644d739434dad3d1cad7b465a093af6c53a5766555991
6
+ metadata.gz: b518be17e0ae85405285fe22022bc698d0c080e08a0682f055ae70c97655ed313038e6e2899b7c961bd73c0afe1b6b0c25bdb2085bd3a66dddd57d3e6245df55
7
+ data.tar.gz: 0bc460c78d178b098c65ab8737efddac0a8484abd920282d185533b72396f0abad62ea247be9e8af4286a70b7a4dc2069e88ff1675ba446af65b5c67edfb59d8
data/README.md CHANGED
@@ -69,6 +69,64 @@ require "datasets"
69
69
  penguins = Datasets::Penguins.new
70
70
  ```
71
71
 
72
+ #### A basic workflow
73
+
74
+ The following code shows a basic workflow of the visualization with Charty.
75
+
76
+ First you need to load the Charty library.
77
+
78
+ ```ruby
79
+ require "charty"
80
+ ```
81
+
82
+ Next you msut have a dataset you want to visualize. Here, we use the penguins dataset provided in red-datasets library.
83
+
84
+ ```ruby
85
+ require "datasets"
86
+ penguins = Datasets::Penguins.new
87
+ ```
88
+
89
+ Next you need to create a plotter object by a plotting method. Here, we use `scatter_plot` method to show the relationship
90
+ among `body_mass_g`, `flipper_length_mm`, and `species` columns in the penguins dataset.
91
+
92
+ ```ruby
93
+ plot = Charty.scatter_plot(data: penguins, x: :body_mass_g, y: :flipper_length_mm, color: :species)
94
+ ```
95
+
96
+ If you want to render and save this plotter object into an HTML file by plotly backend, you can do it like below.
97
+
98
+ ```ruby
99
+ Charty::Backends.use(:plotly) # select plotly backend
100
+ plot.save("scatter.html") # save the plot as an HTML file
101
+ ```
102
+
103
+ When you already have prepared [playwright-ruby-client](https://github.com/YusukeIwaki/playwright-ruby-client),
104
+ you can render a plot into a PNG file by plotly backend by specifying a filename with `.png` extension.
105
+
106
+ ```ruby
107
+ plot.save("scatter.png")
108
+ ```
109
+
110
+ #### Jupyter Notebook
111
+
112
+ If you use Charty on Jupyter Notebook with IRuby kerenl (a.k.a. IRuby notebook),
113
+ you can render the plot just evaluate a plotter object. For example, the code below shows a scatter plot figure in
114
+ the output area.
115
+
116
+ ```ruby
117
+ Charty::Backends.use(:plotly)
118
+
119
+ Charty.scatter_plot(data: penguins, x: :body_mass_g, y: :flipper_length_mm, color: :species)
120
+ ```
121
+
122
+ Note that if you want to use the pyplot backend, you need to activate the integration between the pyplot backend and IRuby.
123
+ You can activate the integration by the following two lines.
124
+
125
+ ```ruby
126
+ Charty::Backends.use(:pyplot)
127
+ Charty::Backends::Pyplot.activate_iruby_integration
128
+ ```
129
+
72
130
  #### Bar plot
73
131
 
74
132
  Charty's statistical bar plot shows the relationship between a categorical variable and estimated means of a numeric variable.
@@ -80,8 +138,7 @@ Instead, when we specify the categorical variable as y-axis, the plot draws a ho
80
138
  The following code shows the relationship between species and the mean body masses of penguins in a vertical bar chart.
81
139
 
82
140
  ```ruby
83
- Charty::Backends.use(:pyplot)
84
- Charty.bar_plot(data: penguins, x: :species, y: :body_mass_g).render
141
+ Charty.bar_plot(data: penguins, x: :species, y: :body_mass_g)
85
142
  ```
86
143
 
87
144
  ![](images/penguins_species_body_mass_g_bar_plot_v.png)
@@ -89,8 +146,7 @@ Charty.bar_plot(data: penguins, x: :species, y: :body_mass_g).render
89
146
  Exchanging x and y axes alternates the orientation of the resulting chart.
90
147
 
91
148
  ```ruby
92
- Charty::Backends.use(:pyplot)
93
- Charty.bar_plot(data: penguins, x: :body_mass_g, y: :species).render
149
+ Charty.bar_plot(data: penguins, x: :body_mass_g, y: :species)
94
150
  ```
95
151
 
96
152
  ![](images/penguins_species_body_mass_g_bar_plot_h.png)
@@ -98,8 +154,7 @@ Charty.bar_plot(data: penguins, x: :body_mass_g, y: :species).render
98
154
  Adding color axis introduces color grouping in the bar plot.
99
155
 
100
156
  ```ruby
101
- Charty::Backends.use(:pyplot)
102
- Charty.bar_plot(data: penguins, x: :species, y: :body_mass_g, color: :sex).render
157
+ Charty.bar_plot(data: penguins, x: :species, y: :body_mass_g, color: :sex)
103
158
  ```
104
159
 
105
160
  ![](images/penguins_species_body_mass_g_sex_bar_plot_v.png)
@@ -116,8 +171,7 @@ Instead, when we specify the categorical variable as y-axis, the plot draws a ho
116
171
  The following code draws a vertical box plot to show distributions of penguins' body mass per species.
117
172
 
118
173
  ```ruby
119
- Charty::Backends.use(:pyplot)
120
- Charty.box_plot(data: penguins, x: :species, y: :body_mass_g).render
174
+ Charty.box_plot(data: penguins, x: :species, y: :body_mass_g)
121
175
  ```
122
176
 
123
177
  ![](images/penguins_species_body_mass_g_box_plot_v.png)
@@ -125,8 +179,7 @@ Charty.box_plot(data: penguins, x: :species, y: :body_mass_g).render
125
179
  As `bar_plot` above, exchanging x and y axes alternates the orientation of the resulting chart.
126
180
 
127
181
  ```ruby
128
- Charty::Backends.use(:pyplot)
129
- Charty.box_plot(data: penguins, x: :body_mass_g, y: :species).render
182
+ Charty.box_plot(data: penguins, x: :body_mass_g, y: :species)
130
183
  ```
131
184
 
132
185
  ![](images/penguins_species_body_mass_g_box_plot_h.png)
@@ -134,8 +187,7 @@ Charty.box_plot(data: penguins, x: :body_mass_g, y: :species).render
134
187
  Adding color axis introduces color grouping in the box plot.
135
188
 
136
189
  ```ruby
137
- Charty::Backends.use(:pyplot)
138
- Charty.box_plot(data: penguins, x: :species, y: :body_mass_g, color: :sex).render
190
+ Charty.box_plot(data: penguins, x: :species, y: :body_mass_g, color: :sex)
139
191
  ```
140
192
 
141
193
  ![](images/penguins_species_body_mass_g_sex_box_plot_v.png)
@@ -145,7 +197,6 @@ Charty.box_plot(data: penguins, x: :species, y: :body_mass_g, color: :sex).rende
145
197
  Charty's scatter plot shows the relationship between two numeric variables.
146
198
 
147
199
  ```ruby
148
- Charty::Backends.use(:pyplot)
149
200
  Charty.scatter_plot(data: penguins, x: :body_mass_g, y: flipper_length_mm)
150
201
  ```
151
202
 
@@ -156,7 +207,6 @@ The following example specifies `:species` variable in the color axis.
156
207
  It shows the different species by the different colors.
157
208
 
158
209
  ```ruby
159
- Charty::Backends.use(:pyplot)
160
210
  Charty.scatter_plot(data: penguins, x: :body_mass_g, y: flipper_length_mm, color: :species)
161
211
  ```
162
212
 
@@ -166,7 +216,6 @@ Moreover, size and style axes can be specified.
166
216
  The following example specifies `:sex` variable in the style axis.
167
217
 
168
218
  ```ruby
169
- Charty::Backends.use(:pyplot)
170
219
  Charty.scatter_plot(data: penguins, x: :body_mass_g, y: flipper_length_mm, color: :species, style: :sex)
171
220
  ```
172
221
 
data/charty.gemspec CHANGED
@@ -26,14 +26,21 @@ 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-palette", ">= 0.2.0"
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"
31
36
 
32
37
  spec.add_development_dependency "bundler", ">= 1.16"
33
38
  spec.add_development_dependency "rake"
34
39
  spec.add_development_dependency "test-unit"
35
- spec.add_development_dependency "red-datasets", ">= 0.0.9"
36
40
  spec.add_development_dependency "daru"
41
+ spec.add_development_dependency "matrix" # need for daru on Ruby > 3.0
37
42
  spec.add_development_dependency "activerecord"
38
43
  spec.add_development_dependency "sqlite3"
44
+ spec.add_development_dependency "iruby", ">= 0.7.0"
45
+ spec.add_development_dependency "csv"
39
46
  end
data/lib/charty.rb CHANGED
@@ -3,17 +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"
9
13
  require_relative "charty/index"
10
14
  require_relative "charty/layout"
11
15
  require_relative "charty/linspace"
12
- require_relative "charty/missing_value_support"
13
16
  require_relative "charty/plotters"
14
17
  require_relative "charty/plot_methods"
15
- require_relative "charty/table_adapters"
16
18
  require_relative "charty/table"
19
+ require_relative "charty/table_adapters"
17
20
  require_relative "charty/statistics"
18
21
  require_relative "charty/vector_adapters"
19
22
  require_relative "charty/vector"
@@ -17,13 +17,13 @@ module Charty
17
17
  @series = series
18
18
  end
19
19
 
20
- def render(context, filename)
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 save(plot, context, filename)
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
@@ -33,7 +33,7 @@ module Charty
33
33
  @series = series
34
34
  end
35
35
 
36
- def render(context, filename)
36
+ def old_style_render(context, filename)
37
37
  plot(nil, context)
38
38
  end
39
39
 
@@ -26,7 +26,7 @@ module Charty
26
26
  raise NotImplementedError
27
27
  end
28
28
 
29
- def render(context, filename="")
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
@@ -1,5 +1,10 @@
1
1
  require "json"
2
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"
3
8
 
4
9
  module Charty
5
10
  module Backends
@@ -36,7 +41,7 @@ module Charty
36
41
  @series = series
37
42
  end
38
43
 
39
- def render(context, filename)
44
+ def old_style_render(context, filename)
40
45
  plot(nil, context)
41
46
  end
42
47
 
@@ -247,12 +252,8 @@ module Charty
247
252
  @traces.concat(traces)
248
253
  end
249
254
 
250
- def scatter(x, y, variables, legend:, color:, color_mapper:,
255
+ def scatter(x, y, variables, color:, color_mapper:,
251
256
  style:, style_mapper:, size:, size_mapper:)
252
- if legend == :full
253
- warn("Plotly backend does not support full verbosity legend")
254
- end
255
-
256
257
  orig_x, orig_y = x, y
257
258
 
258
259
  x = case x
@@ -276,7 +277,7 @@ module Charty
276
277
  end
277
278
 
278
279
  unless color.nil? && style.nil?
279
- grouped_scatter(x, y, variables, legend: legend,
280
+ grouped_scatter(x, y, variables,
280
281
  color: color, color_mapper: color_mapper,
281
282
  style: style, style_mapper: style_mapper,
282
283
  size: size, size_mapper: size_mapper)
@@ -304,7 +305,7 @@ module Charty
304
305
  @traces << trace
305
306
  end
306
307
 
307
- private def grouped_scatter(x, y, variables, legend:, color:, color_mapper:,
308
+ private def grouped_scatter(x, y, variables, color:, color_mapper:,
308
309
  style:, style_mapper:, size:, size_mapper:)
309
310
  @layout[:showlegend] = true
310
311
 
@@ -332,7 +333,9 @@ module Charty
332
333
 
333
334
  unless size.nil?
334
335
  vals = size.values_at(*indices)
335
- trace[:marker][:size] = size_mapper[vals].map(&method(:scale_scatter_point_size))
336
+ trace[:marker][:size] = size_mapper[vals].map do |x|
337
+ scale_scatter_point_size(x).to_f
338
+ end
336
339
  end
337
340
 
338
341
  name = []
@@ -361,6 +364,12 @@ module Charty
361
364
  end
362
365
  end
363
366
 
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
+
364
373
  private def scale_scatter_point_size(x)
365
374
  min = 6
366
375
  max = 12
@@ -368,6 +377,242 @@ module Charty
368
377
  min + x * (max - min)
369
378
  end
370
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(hist, name, variable_name, stat,
573
+ alpha, color, key_color, color_mapper,
574
+ _multiple, _element, _fill, _shrink)
575
+ value_axis = variable_name
576
+ case value_axis
577
+ when :x
578
+ weights_axis = :y
579
+ orientation = :v
580
+ else
581
+ weights_axis = :x
582
+ orientation = :h
583
+ end
584
+
585
+ mid_points = hist.edges.each_cons(2).map {|a, b| a + (b - a) / 2 }
586
+
587
+ trace = {
588
+ type: :bar,
589
+ name: name.to_s,
590
+ value_axis => mid_points,
591
+ weights_axis => hist.weights,
592
+ orientation: orientation,
593
+ opacity: alpha
594
+ }
595
+
596
+ if color.nil?
597
+ trace[:marker] = {
598
+ color: key_color.to_rgb.to_hex_string
599
+ }
600
+ else
601
+ trace[:marker] = {
602
+ color: color_mapper[color].to_rgb.to_hex_string
603
+ }
604
+ end
605
+
606
+ @traces << trace
607
+
608
+ @layout[:bargap] = 0.05
609
+
610
+ if @traces.length > 1
611
+ @layout[:barmode] = "overlay"
612
+ @layout[:showlegend] = true
613
+ end
614
+ end
615
+
371
616
  def set_xlabel(label)
372
617
  @layout[:xaxis] ||= {}
373
618
  @layout[:xaxis][:title] = label
@@ -447,7 +692,34 @@ module Charty
447
692
  # TODO: Handle loc
448
693
  end
449
694
 
450
- def save(filename, title: nil)
695
+ def save(filename, format: nil, title: nil, width: 700, height: 500, **kwargs)
696
+ format = detect_format(filename) if format.nil?
697
+
698
+ case format
699
+ when nil, :html, "text/html"
700
+ save_html(filename, title: title, **kwargs)
701
+ when :png, "png", "image/png",
702
+ :jpeg, "jpeg", "image/jpeg"
703
+ render_image(format, filename: filename, notebook: false, title: title, width: width, height: height, **kwargs)
704
+ end
705
+ nil
706
+ end
707
+
708
+ private def detect_format(filename)
709
+ case File.extname(filename).downcase
710
+ when ".htm", ".html"
711
+ :html
712
+ when ".png"
713
+ :png
714
+ when ".jpg", ".jpeg"
715
+ :jpeg
716
+ else
717
+ raise ArgumentError,
718
+ "Unable to infer file type from filename: %p" % filename
719
+ end
720
+ end
721
+
722
+ private def save_html(filename, title:, element_id: nil)
451
723
  html = <<~HTML
452
724
  <!DOCTYPE html>
453
725
  <html>
@@ -464,44 +736,119 @@ module Charty
464
736
  </body>
465
737
  </html>
466
738
  HTML
739
+
740
+ element_id = SecureRandom.uuid if element_id.nil?
741
+
467
742
  html %= {
468
743
  title: title || default_html_title,
469
- id: SecureRandom.uuid,
744
+ id: element_id,
470
745
  data: JSON.dump(@traces),
471
746
  layout: JSON.dump(@layout)
472
747
  }
473
748
  File.write(filename, html)
474
- nil
475
749
  end
476
750
 
477
751
  private def default_html_title
478
752
  "Charty plot"
479
753
  end
480
754
 
481
- def show
482
- unless defined?(IRuby)
483
- raise NotImplementedError,
484
- "Plotly backend outside of IRuby is not supported"
755
+ def render(element_id: nil, format: nil, notebook: false)
756
+ case format
757
+ when :html, "html", nil
758
+ format = "text/html"
759
+ when :png, "png"
760
+ format = "image/png"
761
+ when :jpeg, "jpeg"
762
+ format = "image/jpeg"
485
763
  end
486
764
 
487
- IRubyOutput.prepare
765
+ case format
766
+ when "text/html"
767
+ # render html after this case cause
768
+ when "image/png", "image/jpeg"
769
+ image_data = render_image(format, element_id: element_id, notebook: false)
770
+ if notebook
771
+ return [format, image_data]
772
+ else
773
+ return image_data
774
+ end
775
+ else
776
+ raise ArgumentError,
777
+ "Unsupported mime type to render: %p" % format
778
+ end
488
779
 
489
- html = <<~HTML
490
- <div id="%{id}" style="width: 100%%; height:100%%;"></div>
491
- <script type="text/javascript">
492
- requirejs(["plotly"], function (Plotly) {
493
- Plotly.newPlot("%{id}", %{data}, %{layout});
494
- });
495
- </script>
496
- HTML
780
+ element_id = SecureRandom.uuid if element_id.nil?
497
781
 
498
- html %= {
499
- id: SecureRandom.uuid,
500
- data: JSON.dump(@traces),
501
- layout: JSON.dump(@layout)
502
- }
503
- IRuby.display(html, mime: "text/html")
504
- nil
782
+ renderer = PlotlyHelpers::HtmlRenderer.new(full_html: !notebook)
783
+ html = renderer.render({data: @traces, layout: @layout}, element_id: element_id)
784
+ if notebook
785
+ [format, html]
786
+ else
787
+ html
788
+ end
789
+ end
790
+
791
+ def render_mimebundle(include: [], exclude: [])
792
+ types = case
793
+ when IRubyHelper.vscode?,
794
+ IRubyHelper.nteract?
795
+ [:plotly_mimetype]
796
+ else
797
+ [:plotly_mimetype, :notebook]
798
+ end
799
+ bundle = Util.filter_map(types) { |type|
800
+ case type
801
+ when :plotly_mimetype
802
+ render_plotly_mimetype_bundle
803
+ when :notebook
804
+ render_notebook_bundle
805
+ end
806
+ }.to_h
807
+ bundle
808
+ end
809
+
810
+ private def render_plotly_mimetype_bundle
811
+ renderer = PlotlyHelpers::PlotlyRenderer.new
812
+ obj = renderer.render({data: @traces, layout: @layout})
813
+ [ "application/vnd.plotly.v1+json", obj ]
814
+ end
815
+
816
+ private def render_notebook_bundle
817
+ renderer = self.class.notebook_renderer
818
+ renderer.activate
819
+ html = renderer.render({data: @traces, layout: @layout})
820
+ [ "text/html", html ]
821
+ end
822
+
823
+ # for new APIs
824
+ def self.notebook_renderer
825
+ @notebook_renderer ||= PlotlyHelpers::NotebookRenderer.new
826
+ end
827
+
828
+ private def render_image(format=nil, filename: nil, element_id: nil, notebook: false,
829
+ title: nil, width: nil, height: nil)
830
+ format = "image/png" if format.nil?
831
+ case format
832
+ when :png, "png", :jpeg, "jpeg"
833
+ image_type = format.to_s
834
+ when "image/png", "image/jpeg"
835
+ image_type = format.split("/").last
836
+ else
837
+ raise ArgumentError,
838
+ "Unsupported mime type to render image: %p" % format
839
+ end
840
+
841
+ height = 525 if height.nil?
842
+ width = (height * Math.sqrt(2)).to_i if width.nil?
843
+ title = "Charty plot" if title.nil?
844
+
845
+ element_id = SecureRandom.uuid if element_id.nil?
846
+ element_id = "charty-plotly-#{element_id}"
847
+ Dir.mktmpdir do |tmpdir|
848
+ html_filename = File.join(tmpdir, "%s.html" % element_id)
849
+ save_html(html_filename, title: title, element_id: element_id)
850
+ return self.class.render_image(html_filename, filename, image_type, element_id, width, height)
851
+ end
505
852
  end
506
853
 
507
854
  module IRubyOutput
@@ -544,6 +891,61 @@ module Charty
544
891
  END
545
892
  end
546
893
  end
894
+
895
+ @playwright_fiber = nil
896
+
897
+ def self.ensure_playwright
898
+ if @playwright_fiber.nil?
899
+ begin
900
+ require "playwright"
901
+ rescue LoadError
902
+ $stderr.puts "ERROR: You need to install playwright and playwright-ruby-client before using Plotly renderer"
903
+ raise
904
+ end
905
+
906
+ @playwright_fiber = Fiber.new do
907
+ playwright_cli_executable_path = ENV.fetch("PLAYWRIGHT_CLI_EXECUTABLE_PATH", "npx playwright")
908
+ Playwright.create(playwright_cli_executable_path: playwright_cli_executable_path) do |playwright|
909
+ playwright.chromium.launch(headless: true) do |browser|
910
+ request = Fiber.yield
911
+ loop do
912
+ result = nil
913
+ case request.shift
914
+ when :finish
915
+ break
916
+ when :render
917
+ input, output, format, element_id, width, height = request
918
+
919
+ page = browser.new_page
920
+ page.set_viewport_size(width: width, height: height)
921
+ page.goto("file://#{input}")
922
+ element = page.query_selector("\##{element_id}")
923
+
924
+ kwargs = {type: format}
925
+ kwargs[:path] = output unless output.nil?
926
+ result = element.screenshot(**kwargs)
927
+ end
928
+ request = Fiber.yield(result)
929
+ end
930
+ end
931
+ end
932
+ end
933
+ @playwright_fiber.resume
934
+ end
935
+ end
936
+
937
+ def self.terminate_playwright
938
+ return if @playwright_fiber.nil?
939
+
940
+ @playwright_fiber.resume([:finish])
941
+ end
942
+
943
+ at_exit { terminate_playwright }
944
+
945
+ def self.render_image(input, output, format, element_id, width, height)
946
+ ensure_playwright if @playwright_fiber.nil?
947
+ @playwright_fiber.resume([:render, input, output, format.to_s, element_id, width, height])
948
+ end
547
949
  end
548
950
  end
549
951
  end