charty 0.1.4.dev → 0.2.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +71 -0
  3. data/.github/workflows/nmatrix.yml +67 -0
  4. data/.github/workflows/pycall.yml +86 -0
  5. data/Dockerfile.dev +9 -1
  6. data/Gemfile +18 -0
  7. data/README.md +128 -9
  8. data/Rakefile +4 -5
  9. data/charty.gemspec +7 -2
  10. data/examples/Gemfile +1 -0
  11. data/examples/active_record.ipynb +34 -34
  12. data/examples/daru.ipynb +71 -29
  13. data/examples/iris_dataset.ipynb +12 -5
  14. data/examples/nmatrix.ipynb +30 -30
  15. data/examples/numo_narray.ipynb +245 -0
  16. data/examples/palette.rb +71 -0
  17. data/examples/sample.png +0 -0
  18. data/examples/sample_bokeh.ipynb +156 -0
  19. data/examples/sample_google_chart.ipynb +229 -68
  20. data/examples/sample_gruff.ipynb +148 -133
  21. data/examples/sample_images/bar_bokeh.html +85 -0
  22. data/examples/sample_images/barh_bokeh.html +85 -0
  23. data/examples/sample_images/barh_gruff.png +0 -0
  24. data/examples/sample_images/box_plot_bokeh.html +85 -0
  25. data/examples/sample_images/{boxplot_pyplot.png → box_plot_pyplot.png} +0 -0
  26. data/examples/sample_images/curve_bokeh.html +85 -0
  27. data/examples/sample_images/curve_with_function_bokeh.html +85 -0
  28. data/examples/sample_images/{errorbar_pyplot.png → error_bar_pyplot.png} +0 -0
  29. data/examples/sample_images/hist_gruff.png +0 -0
  30. data/examples/sample_images/scatter_bokeh.html +85 -0
  31. data/examples/sample_pyplot.ipynb +37 -35
  32. data/images/penguins_body_mass_g_flipper_length_mm_scatter_plot.png +0 -0
  33. data/images/penguins_body_mass_g_flipper_length_mm_species_scatter_plot.png +0 -0
  34. data/images/penguins_body_mass_g_flipper_length_mm_species_sex_scatter_plot.png +0 -0
  35. data/images/penguins_species_body_mass_g_bar_plot_h.png +0 -0
  36. data/images/penguins_species_body_mass_g_bar_plot_v.png +0 -0
  37. data/images/penguins_species_body_mass_g_box_plot_h.png +0 -0
  38. data/images/penguins_species_body_mass_g_box_plot_v.png +0 -0
  39. data/images/penguins_species_body_mass_g_sex_bar_plot_v.png +0 -0
  40. data/images/penguins_species_body_mass_g_sex_box_plot_v.png +0 -0
  41. data/lib/charty.rb +13 -7
  42. data/lib/charty/backend_methods.rb +8 -0
  43. data/lib/charty/backends.rb +80 -0
  44. data/lib/charty/backends/bokeh.rb +80 -0
  45. data/lib/charty/backends/google_charts.rb +267 -0
  46. data/lib/charty/backends/gruff.rb +104 -67
  47. data/lib/charty/backends/plotly.rb +549 -0
  48. data/lib/charty/backends/pyplot.rb +584 -86
  49. data/lib/charty/backends/rubyplot.rb +82 -74
  50. data/lib/charty/backends/unicode_plot.rb +79 -0
  51. data/lib/charty/index.rb +213 -0
  52. data/lib/charty/linspace.rb +1 -1
  53. data/lib/charty/missing_value_support.rb +14 -0
  54. data/lib/charty/plot_methods.rb +184 -0
  55. data/lib/charty/plotter.rb +57 -41
  56. data/lib/charty/plotters.rb +11 -0
  57. data/lib/charty/plotters/abstract_plotter.rb +156 -0
  58. data/lib/charty/plotters/bar_plotter.rb +216 -0
  59. data/lib/charty/plotters/box_plotter.rb +94 -0
  60. data/lib/charty/plotters/categorical_plotter.rb +380 -0
  61. data/lib/charty/plotters/count_plotter.rb +7 -0
  62. data/lib/charty/plotters/estimation_support.rb +84 -0
  63. data/lib/charty/plotters/random_support.rb +25 -0
  64. data/lib/charty/plotters/relational_plotter.rb +518 -0
  65. data/lib/charty/plotters/scatter_plotter.rb +115 -0
  66. data/lib/charty/plotters/vector_plotter.rb +6 -0
  67. data/lib/charty/statistics.rb +114 -0
  68. data/lib/charty/table.rb +82 -3
  69. data/lib/charty/table_adapters.rb +25 -0
  70. data/lib/charty/table_adapters/active_record_adapter.rb +63 -0
  71. data/lib/charty/table_adapters/base_adapter.rb +69 -0
  72. data/lib/charty/table_adapters/daru_adapter.rb +70 -0
  73. data/lib/charty/table_adapters/datasets_adapter.rb +49 -0
  74. data/lib/charty/table_adapters/hash_adapter.rb +224 -0
  75. data/lib/charty/table_adapters/narray_adapter.rb +76 -0
  76. data/lib/charty/table_adapters/nmatrix_adapter.rb +67 -0
  77. data/lib/charty/table_adapters/pandas_adapter.rb +81 -0
  78. data/lib/charty/vector.rb +69 -0
  79. data/lib/charty/vector_adapters.rb +183 -0
  80. data/lib/charty/vector_adapters/array_adapter.rb +109 -0
  81. data/lib/charty/vector_adapters/daru_adapter.rb +171 -0
  82. data/lib/charty/vector_adapters/narray_adapter.rb +187 -0
  83. data/lib/charty/vector_adapters/nmatrix_adapter.rb +37 -0
  84. data/lib/charty/vector_adapters/numpy_adapter.rb +168 -0
  85. data/lib/charty/vector_adapters/pandas_adapter.rb +200 -0
  86. data/lib/charty/version.rb +1 -1
  87. metadata +127 -13
  88. data/.travis.yml +0 -11
  89. data/examples/numo-narray.ipynb +0 -234
  90. data/lib/charty/backends/google_chart.rb +0 -167
  91. data/lib/charty/plotter_adapter.rb +0 -17
@@ -0,0 +1,549 @@
1
+ require "json"
2
+ require "securerandom"
3
+
4
+ module Charty
5
+ module Backends
6
+ class Plotly
7
+ Backends.register(:plotly, self)
8
+
9
+ attr_reader :context
10
+
11
+ class << self
12
+ attr_writer :chart_id, :with_api_load_tag, :plotly_src
13
+
14
+ def chart_id
15
+ @chart_id ||= 0
16
+ end
17
+
18
+ def with_api_load_tag
19
+ return @with_api_load_tag unless @with_api_load_tag.nil?
20
+
21
+ @with_api_load_tag = true
22
+ end
23
+
24
+ def plotly_src
25
+ @plotly_src ||= 'https://cdn.plot.ly/plotly-latest.min.js'
26
+ end
27
+ end
28
+
29
+ def initilize
30
+ end
31
+
32
+ def label(x, y)
33
+ end
34
+
35
+ def series=(series)
36
+ @series = series
37
+ end
38
+
39
+ def render(context, filename)
40
+ plot(nil, context)
41
+ end
42
+
43
+ def plot(plot, context)
44
+ context = context
45
+ self.class.chart_id += 1
46
+
47
+ case context.method
48
+ when :bar
49
+ render_graph(context, :bar)
50
+ when :curve
51
+ render_graph(context, :scatter)
52
+ when :scatter
53
+ render_graph(context, nil, options: {data: {mode: "markers"}})
54
+ else
55
+ raise NotImplementedError
56
+ end
57
+ end
58
+
59
+ private def plotly_load_tag
60
+ if self.class.with_api_load_tag
61
+ "<script type='text/javascript' src='#{self.class.plotly_src}'></script>"
62
+ else
63
+ end
64
+ end
65
+
66
+ private def div_id
67
+ "charty-plotly-#{self.class.chart_id}"
68
+ end
69
+
70
+ private def div_style
71
+ "width: 100%;height: 100%;"
72
+ end
73
+
74
+ private def render_graph(context, type, options: {})
75
+ data = context.series.map do |series|
76
+ {
77
+ type: type,
78
+ x: series.xs.to_a,
79
+ y: series.ys.to_a,
80
+ name: series.label
81
+ }.merge(options[:data] || {})
82
+ end
83
+ layout = {
84
+ title: { text: context.title },
85
+ xaxis: {
86
+ title: context.xlabel,
87
+ range: [context.range[:x].first, context.range[:x].last]
88
+ },
89
+ yaxis: {
90
+ title: context.ylabel,
91
+ range: [context.range[:y].first, context.range[:y].last]
92
+ }
93
+ }
94
+ render_html(data, layout)
95
+ end
96
+
97
+ private def render_html(data, layout)
98
+ <<~FRAGMENT
99
+ #{plotly_load_tag unless self.class.chart_id > 1}
100
+ <div id="#{div_id}" style="#{div_style}"></div>
101
+ <script>
102
+ Plotly.plot('#{div_id}', #{JSON.dump(data)}, #{JSON.dump(layout)} );
103
+ </script>
104
+ FRAGMENT
105
+ end
106
+
107
+ # ==== NEW PLOTTING API ====
108
+
109
+ class HTML
110
+ def initialize(html)
111
+ @html = html
112
+ end
113
+
114
+ def to_iruby
115
+ ["text/html", @html]
116
+ end
117
+ end
118
+
119
+ def begin_figure
120
+ @traces = []
121
+ @layout = {showlegend: false}
122
+ end
123
+
124
+ def bar(bar_pos, group_names, values, colors, orient, label: nil, width: 0.8r,
125
+ align: :center, conf_int: nil, error_colors: nil, error_width: nil, cap_size: nil)
126
+ bar_pos = Array(bar_pos)
127
+ values = Array(values)
128
+ colors = Array(colors).map(&:to_hex_string)
129
+
130
+ if orient == :v
131
+ x, y = bar_pos, values
132
+ x = group_names unless group_names.nil?
133
+ else
134
+ x, y = values, bar_pos
135
+ y = group_names unless group_names.nil?
136
+ end
137
+
138
+ trace = {
139
+ type: :bar,
140
+ orientation: orient,
141
+ x: x,
142
+ y: y,
143
+ width: width,
144
+ marker: {color: colors}
145
+ }
146
+ trace[:name] = label unless label.nil?
147
+
148
+ unless conf_int.nil?
149
+ errors_low = conf_int.map.with_index {|(low, _), i| values[i] - low }
150
+ errors_high = conf_int.map.with_index {|(_, high), i| high - values[i] }
151
+
152
+ error_bar = {
153
+ type: :data,
154
+ visible: true,
155
+ symmetric: false,
156
+ array: errors_high,
157
+ arrayminus: errors_low,
158
+ color: error_colors[0].to_hex_string
159
+ }
160
+ error_bar[:thickness] = error_width unless error_width.nil?
161
+ error_bar[:width] = cap_size unless cap_size.nil?
162
+
163
+ error_bar_key = orient == :v ? :error_y : :error_x
164
+ trace[error_bar_key] = error_bar
165
+ end
166
+
167
+ @traces << trace
168
+
169
+ if group_names
170
+ @layout[:barmode] = :group
171
+ end
172
+ end
173
+
174
+ def box_plot(plot_data, group_names,
175
+ orient:, colors:, gray:, dodge:, width: 0.8r,
176
+ flier_size: 5, whisker: 1.5, notch: false)
177
+ colors = Array(colors).map(&:to_hex_string)
178
+ gray = gray.to_hex_string
179
+ width = Float(width)
180
+ flier_size = Float(width)
181
+ whisker = Float(whisker)
182
+
183
+ traces = plot_data.map.with_index do |group_data, i|
184
+ group_data = Array(group_data)
185
+ trace = {
186
+ type: :box,
187
+ orientation: orient,
188
+ name: group_names[i],
189
+ marker: {color: colors[i]}
190
+ }
191
+ if orient == :v
192
+ trace.update(y: group_data)
193
+ else
194
+ trace.update(x: group_data)
195
+ end
196
+
197
+ trace
198
+ end
199
+
200
+ traces.reverse! if orient == :h
201
+
202
+ @traces.concat(traces)
203
+ end
204
+
205
+ def grouped_box_plot(plot_data, group_names, color_names,
206
+ orient:, colors:, gray:, dodge:, width: 0.8r,
207
+ flier_size: 5, whisker: 1.5, notch: false)
208
+ colors = Array(colors).map(&:to_hex_string)
209
+ gray = gray.to_hex_string
210
+ width = Float(width)
211
+ flier_size = Float(width)
212
+ whisker = Float(whisker)
213
+
214
+ @layout[:boxmode] = :group
215
+
216
+ if orient == :h
217
+ @layout[:xaxis] ||= {}
218
+ @layout[:xaxis][:zeroline] = false
219
+
220
+ plot_data = plot_data.map {|d| d.reverse }
221
+ group_names = group_names.reverse
222
+ end
223
+
224
+ traces = color_names.map.with_index do |color_name, i|
225
+ group_keys = group_names.flat_map.with_index { |name, j|
226
+ Array.new(plot_data[i][j].length, name)
227
+ }.flatten
228
+
229
+ values = plot_data[i].flat_map {|d| Array(d) }
230
+
231
+ trace = {
232
+ type: :box,
233
+ orientation: orient,
234
+ name: color_name,
235
+ marker: {color: colors[i]}
236
+ }
237
+
238
+ if orient == :v
239
+ trace.update(y: values, x: group_keys)
240
+ else
241
+ trace.update(x: values, y: group_keys)
242
+ end
243
+
244
+ trace
245
+ end
246
+
247
+ @traces.concat(traces)
248
+ end
249
+
250
+ def scatter(x, y, variables, legend:, color:, color_mapper:,
251
+ style:, style_mapper:, size:, size_mapper:)
252
+ if legend == :full
253
+ warn("Plotly backend does not support full verbosity legend")
254
+ end
255
+
256
+ orig_x, orig_y = x, y
257
+
258
+ x = case x
259
+ when Charty::Vector
260
+ x.to_a
261
+ else
262
+ Array.try_convert(x)
263
+ end
264
+ if x.nil?
265
+ raise ArgumentError, "Invalid value for x: %p" % orig_x
266
+ end
267
+
268
+ y = case y
269
+ when Charty::Vector
270
+ y.to_a
271
+ else
272
+ Array.try_convert(y)
273
+ end
274
+ if y.nil?
275
+ raise ArgumentError, "Invalid value for y: %p" % orig_y
276
+ end
277
+
278
+ unless color.nil? && style.nil?
279
+ grouped_scatter(x, y, variables, legend: legend,
280
+ color: color, color_mapper: color_mapper,
281
+ style: style, style_mapper: style_mapper,
282
+ size: size, size_mapper: size_mapper)
283
+ return
284
+ end
285
+
286
+ trace = {
287
+ type: :scatter,
288
+ mode: :markers,
289
+ x: x,
290
+ y: y,
291
+ marker: {
292
+ line: {
293
+ width: 1,
294
+ color: "#fff"
295
+ },
296
+ size: 10
297
+ }
298
+ }
299
+
300
+ unless size.nil?
301
+ trace[:marker][:size] = size_mapper[size].map {|x| 6.0 + x * 6.0 }
302
+ end
303
+
304
+ @traces << trace
305
+ end
306
+
307
+ private def grouped_scatter(x, y, variables, legend:, color:, color_mapper:,
308
+ style:, style_mapper:, size:, size_mapper:)
309
+ @layout[:showlegend] = true
310
+
311
+ groups = (0 ... x.length).group_by do |i|
312
+ key = {}
313
+ key[:color] = color[i] unless color.nil?
314
+ key[:style] = style[i] unless style.nil?
315
+ key
316
+ end
317
+
318
+ groups.each do |group_key, indices|
319
+ trace = {
320
+ type: :scatter,
321
+ mode: :markers,
322
+ x: x.values_at(*indices),
323
+ y: y.values_at(*indices),
324
+ marker: {
325
+ line: {
326
+ width: 1,
327
+ color: "#fff"
328
+ },
329
+ size: 10
330
+ }
331
+ }
332
+
333
+ unless size.nil?
334
+ vals = size.values_at(*indices)
335
+ trace[:marker][:size] = size_mapper[vals].map(&method(:scale_scatter_point_size))
336
+ end
337
+
338
+ name = []
339
+ legend_title = []
340
+
341
+ if group_key.key?(:color)
342
+ trace[:marker][:color] = color_mapper[group_key[:color]].to_hex_string
343
+ name << group_key[:color]
344
+ legend_title << variables[:color]
345
+ end
346
+
347
+ if group_key.key?(:style)
348
+ trace[:marker][:symbol] = style_mapper[group_key[:style], :marker]
349
+ name << group_key[:style]
350
+ legend_title << variables[:style]
351
+ end
352
+
353
+ trace[:name] = name.uniq.join(", ") unless name.empty?
354
+
355
+ @traces << trace
356
+
357
+ unless legend_title.empty?
358
+ @layout[:legend] ||= {}
359
+ @layout[:legend][:title] = {text: legend_title.uniq.join(", ")}
360
+ end
361
+ end
362
+ end
363
+
364
+ private def scale_scatter_point_size(x)
365
+ min = 6
366
+ max = 12
367
+
368
+ min + x * (max - min)
369
+ end
370
+
371
+ def set_xlabel(label)
372
+ @layout[:xaxis] ||= {}
373
+ @layout[:xaxis][:title] = label
374
+ end
375
+
376
+ def set_ylabel(label)
377
+ @layout[:yaxis] ||= {}
378
+ @layout[:yaxis][:title] = label
379
+ end
380
+
381
+ def set_xticks(values)
382
+ @layout[:xaxis] ||= {}
383
+ @layout[:xaxis][:tickmode] = "array"
384
+ @layout[:xaxis][:tickvals] = values
385
+ end
386
+
387
+ def set_yticks(values)
388
+ @layout[:yaxis] ||= {}
389
+ @layout[:yaxis][:tickmode] = "array"
390
+ @layout[:yaxis][:tickvals] = values
391
+ end
392
+
393
+ def set_xtick_labels(labels)
394
+ @layout[:xaxis] ||= {}
395
+ @layout[:xaxis][:tickmode] = "array"
396
+ @layout[:xaxis][:ticktext] = labels
397
+ end
398
+
399
+ def set_ytick_labels(labels)
400
+ @layout[:yaxis] ||= {}
401
+ @layout[:yaxis][:tickmode] = "array"
402
+ @layout[:yaxis][:ticktext] = labels
403
+ end
404
+
405
+ def set_xlim(min, max)
406
+ @layout[:xaxis] ||= {}
407
+ @layout[:xaxis][:range] = [min, max]
408
+ end
409
+
410
+ def set_ylim(min, max)
411
+ @layout[:yaxis] ||= {}
412
+ @layout[:yaxis][:range] = [min, max]
413
+ end
414
+
415
+ def disable_xaxis_grid
416
+ # do nothing
417
+ end
418
+
419
+ def disable_yaxis_grid
420
+ # do nothing
421
+ end
422
+
423
+ def invert_yaxis
424
+ @traces.each do |trace|
425
+ case trace[:type]
426
+ when :bar
427
+ trace[:y].reverse!
428
+ end
429
+ end
430
+
431
+ if @layout[:boxmode] == :group
432
+ @traces.reverse!
433
+ end
434
+
435
+ if @layout[:yaxis] && @layout[:yaxis][:ticktext]
436
+ @layout[:yaxis][:ticktext].reverse!
437
+ end
438
+ end
439
+
440
+ def legend(loc:, title:)
441
+ @layout[:showlegend] = true
442
+ @layout[:legend] = {
443
+ title: {
444
+ text: title
445
+ }
446
+ }
447
+ # TODO: Handle loc
448
+ end
449
+
450
+ def save(filename, title: nil)
451
+ html = <<~HTML
452
+ <!DOCTYPE html>
453
+ <html>
454
+ <head>
455
+ <meta charset="utf-8">
456
+ <title>%{title}</title>
457
+ <script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
458
+ </head>
459
+ <body>
460
+ <div id="%{id}" style="width: 100%%; height:100%%;"></div>
461
+ <script type="text/javascript">
462
+ Plotly.newPlot("%{id}", %{data}, %{layout});
463
+ </script>
464
+ </body>
465
+ </html>
466
+ HTML
467
+ html %= {
468
+ title: title || default_html_title,
469
+ id: SecureRandom.uuid,
470
+ data: JSON.dump(@traces),
471
+ layout: JSON.dump(@layout)
472
+ }
473
+ File.write(filename, html)
474
+ nil
475
+ end
476
+
477
+ private def default_html_title
478
+ "Charty plot"
479
+ end
480
+
481
+ def show
482
+ unless defined?(IRuby)
483
+ raise NotImplementedError,
484
+ "Plotly backend outside of IRuby is not supported"
485
+ end
486
+
487
+ IRubyOutput.prepare
488
+
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
497
+
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
505
+ end
506
+
507
+ module IRubyOutput
508
+ @prepared = false
509
+
510
+ def self.prepare
511
+ return if @prepared
512
+
513
+ html = <<~HTML
514
+ <script type="text/javascript">
515
+ %{win_config}
516
+ %{mathjax_config}
517
+ require.config({
518
+ paths: {
519
+ plotly: "https://cdn.plot.ly/plotly-latest.min"
520
+ }
521
+ });
522
+ </script>
523
+ HTML
524
+
525
+ html %= {
526
+ win_config: window_plotly_config,
527
+ mathjax_config: mathjax_config
528
+ }
529
+
530
+ IRuby.display(html, mime: "text/html")
531
+ @prepared = true
532
+ end
533
+
534
+ def self.window_plotly_config
535
+ <<~END
536
+ window.PlotlyConfig = {MathJaxConfig: 'local'};
537
+ END
538
+ end
539
+
540
+
541
+ def self.mathjax_config
542
+ <<~END
543
+ if (window.MathJax) {MathJax.Hub.Config({SVG: {font: "STIX-Web"}});}
544
+ END
545
+ end
546
+ end
547
+ end
548
+ end
549
+ end