charty 0.2.4 → 0.2.9

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.
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