charty 0.1.4.dev → 0.2.4

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