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.
- checksums.yaml +4 -4
- data/README.md +64 -15
- data/charty.gemspec +10 -3
- data/lib/charty.rb +5 -2
- data/lib/charty/backends/bokeh.rb +2 -2
- data/lib/charty/backends/google_charts.rb +1 -1
- data/lib/charty/backends/gruff.rb +1 -1
- data/lib/charty/backends/plotly.rb +434 -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 +187 -48
- 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 +1 -1
- data/lib/charty/iruby_helper.rb +18 -0
- data/lib/charty/plot_methods.rb +115 -3
- data/lib/charty/plotter.rb +2 -2
- data/lib/charty/plotters.rb +4 -0
- data/lib/charty/plotters/abstract_plotter.rb +106 -11
- data/lib/charty/plotters/bar_plotter.rb +1 -16
- data/lib/charty/plotters/box_plotter.rb +1 -16
- data/lib/charty/plotters/distribution_plotter.rb +150 -0
- data/lib/charty/plotters/histogram_plotter.rb +242 -0
- data/lib/charty/plotters/line_plotter.rb +300 -0
- data/lib/charty/plotters/relational_plotter.rb +213 -96
- data/lib/charty/plotters/scatter_plotter.rb +8 -43
- data/lib/charty/statistics.rb +11 -2
- data/lib/charty/table.rb +124 -14
- data/lib/charty/table_adapters/base_adapter.rb +97 -0
- data/lib/charty/table_adapters/daru_adapter.rb +2 -0
- data/lib/charty/table_adapters/datasets_adapter.rb +7 -0
- data/lib/charty/table_adapters/hash_adapter.rb +19 -3
- data/lib/charty/table_adapters/pandas_adapter.rb +82 -0
- data/lib/charty/util.rb +28 -0
- data/lib/charty/vector_adapters.rb +5 -1
- data/lib/charty/vector_adapters/array_adapter.rb +2 -10
- data/lib/charty/vector_adapters/daru_adapter.rb +3 -11
- data/lib/charty/vector_adapters/narray_adapter.rb +1 -6
- data/lib/charty/vector_adapters/numpy_adapter.rb +1 -1
- data/lib/charty/vector_adapters/pandas_adapter.rb +0 -1
- data/lib/charty/version.rb +1 -1
- metadata +104 -11
- data/lib/charty/missing_value_support.rb +0 -14
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 37ead1e86203bd6580125f4ed97039c6b512e9ae3037a0bc386743ee436f184c
|
4
|
+
data.tar.gz: a94e66292a866acde67c0cfc51bd8eebd5840af68e2d4112caafce80bdd37ed2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|

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

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

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

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

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

|
@@ -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-
|
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
|
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
|
@@ -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
|
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,
|
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,
|
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,
|
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
|
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:
|
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
|
482
|
-
|
483
|
-
|
484
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
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
|