charty 0.2.3 → 0.2.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +56 -23
  3. data/.github/workflows/nmatrix.yml +67 -0
  4. data/.github/workflows/pycall.yml +86 -0
  5. data/Gemfile +18 -0
  6. data/README.md +123 -4
  7. data/Rakefile +4 -5
  8. data/charty.gemspec +1 -3
  9. data/examples/sample_images/hist_gruff.png +0 -0
  10. data/images/penguins_body_mass_g_flipper_length_mm_scatter_plot.png +0 -0
  11. data/images/penguins_body_mass_g_flipper_length_mm_species_scatter_plot.png +0 -0
  12. data/images/penguins_body_mass_g_flipper_length_mm_species_sex_scatter_plot.png +0 -0
  13. data/images/penguins_species_body_mass_g_bar_plot_h.png +0 -0
  14. data/images/penguins_species_body_mass_g_bar_plot_v.png +0 -0
  15. data/images/penguins_species_body_mass_g_box_plot_h.png +0 -0
  16. data/images/penguins_species_body_mass_g_box_plot_v.png +0 -0
  17. data/images/penguins_species_body_mass_g_sex_bar_plot_v.png +0 -0
  18. data/images/penguins_species_body_mass_g_sex_box_plot_v.png +0 -0
  19. data/lib/charty.rb +4 -0
  20. data/lib/charty/backends/gruff.rb +13 -2
  21. data/lib/charty/backends/plotly.rb +322 -20
  22. data/lib/charty/backends/pyplot.rb +416 -64
  23. data/lib/charty/index.rb +213 -0
  24. data/lib/charty/linspace.rb +1 -1
  25. data/lib/charty/missing_value_support.rb +14 -0
  26. data/lib/charty/plot_methods.rb +173 -8
  27. data/lib/charty/plotters.rb +7 -0
  28. data/lib/charty/plotters/abstract_plotter.rb +87 -12
  29. data/lib/charty/plotters/bar_plotter.rb +200 -3
  30. data/lib/charty/plotters/box_plotter.rb +75 -7
  31. data/lib/charty/plotters/categorical_plotter.rb +272 -40
  32. data/lib/charty/plotters/count_plotter.rb +7 -0
  33. data/lib/charty/plotters/estimation_support.rb +84 -0
  34. data/lib/charty/plotters/random_support.rb +25 -0
  35. data/lib/charty/plotters/relational_plotter.rb +518 -0
  36. data/lib/charty/plotters/scatter_plotter.rb +115 -0
  37. data/lib/charty/plotters/vector_plotter.rb +6 -0
  38. data/lib/charty/statistics.rb +87 -2
  39. data/lib/charty/table.rb +50 -15
  40. data/lib/charty/table_adapters.rb +2 -0
  41. data/lib/charty/table_adapters/active_record_adapter.rb +17 -9
  42. data/lib/charty/table_adapters/base_adapter.rb +69 -0
  43. data/lib/charty/table_adapters/daru_adapter.rb +37 -3
  44. data/lib/charty/table_adapters/datasets_adapter.rb +6 -2
  45. data/lib/charty/table_adapters/hash_adapter.rb +130 -16
  46. data/lib/charty/table_adapters/narray_adapter.rb +25 -6
  47. data/lib/charty/table_adapters/nmatrix_adapter.rb +15 -5
  48. data/lib/charty/table_adapters/pandas_adapter.rb +81 -0
  49. data/lib/charty/vector.rb +69 -0
  50. data/lib/charty/vector_adapters.rb +183 -0
  51. data/lib/charty/vector_adapters/array_adapter.rb +109 -0
  52. data/lib/charty/vector_adapters/daru_adapter.rb +171 -0
  53. data/lib/charty/vector_adapters/narray_adapter.rb +187 -0
  54. data/lib/charty/vector_adapters/nmatrix_adapter.rb +37 -0
  55. data/lib/charty/vector_adapters/numpy_adapter.rb +168 -0
  56. data/lib/charty/vector_adapters/pandas_adapter.rb +200 -0
  57. data/lib/charty/version.rb +1 -1
  58. metadata +33 -45
data/charty.gemspec CHANGED
@@ -28,14 +28,12 @@ Gem::Specification.new do |spec|
28
28
 
29
29
  spec.add_dependency "red-colors"
30
30
  spec.add_dependency "red-palette", ">= 0.2.0"
31
+
31
32
  spec.add_development_dependency "bundler", ">= 1.16"
32
33
  spec.add_development_dependency "rake"
33
34
  spec.add_development_dependency "test-unit"
34
- spec.add_development_dependency "numo-narray"
35
- spec.add_development_dependency "nmatrix"
36
35
  spec.add_development_dependency "red-datasets", ">= 0.0.9"
37
36
  spec.add_development_dependency "daru"
38
37
  spec.add_development_dependency "activerecord"
39
38
  spec.add_development_dependency "sqlite3"
40
- spec.add_development_dependency "matplotlib"
41
39
  end
data/lib/charty.rb CHANGED
@@ -6,10 +6,14 @@ require "palette"
6
6
  require_relative "charty/backends"
7
7
  require_relative "charty/backend_methods"
8
8
  require_relative "charty/plotter"
9
+ require_relative "charty/index"
9
10
  require_relative "charty/layout"
10
11
  require_relative "charty/linspace"
12
+ require_relative "charty/missing_value_support"
11
13
  require_relative "charty/plotters"
12
14
  require_relative "charty/plot_methods"
13
15
  require_relative "charty/table_adapters"
14
16
  require_relative "charty/table"
15
17
  require_relative "charty/statistics"
18
+ require_relative "charty/vector_adapters"
19
+ require_relative "charty/vector"
@@ -83,7 +83,7 @@ module Charty
83
83
  p.x_axis_label = context.xlabel if context.xlabel
84
84
  p.y_axis_label = context.ylabel if context.ylabel
85
85
  context.series.each do |data|
86
- p.data(data.label, data.xs.to_a)
86
+ p.dataxy(data.label, data.xs.to_a, data.ys.to_a)
87
87
  end
88
88
  p
89
89
  when :scatter
@@ -99,7 +99,18 @@ module Charty
99
99
  # refs. https://github.com/topfunky/gruff/issues/163
100
100
  raise NotImplementedError
101
101
  when :hist
102
- raise NotImplementedError
102
+ p = plot::Histogram.new
103
+ p.title = context.title if context.title
104
+ p.x_axis_label = context.xlabel if context.xlabel
105
+ p.y_axis_label = context.ylabel if context.ylabel
106
+ if context.range_x
107
+ p.minimum_bin = context.range_x.first
108
+ p.maximum_bin = context.range_x.last
109
+ end
110
+ context.data.each do |data|
111
+ p.data('', data.to_a)
112
+ end
113
+ p
103
114
  end
104
115
  end
105
116
  end
@@ -1,4 +1,5 @@
1
- require 'json'
1
+ require "json"
2
+ require "securerandom"
2
3
 
3
4
  module Charty
4
5
  module Backends
@@ -117,32 +118,254 @@ module Charty
117
118
 
118
119
  def begin_figure
119
120
  @traces = []
120
- @layout = {}
121
+ @layout = {showlegend: false}
121
122
  end
122
123
 
123
- def bar(bar_pos, values, color: nil, width: 0.8r, align: :center, orient: :v)
124
- color = Array(color).map(&:to_hex_string)
125
- @traces << {
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 = {
126
139
  type: :bar,
127
- x: bar_pos,
128
- y: values,
129
- marker: {color: color}
140
+ orientation: orient,
141
+ x: x,
142
+ y: y,
143
+ width: width,
144
+ marker: {color: colors}
130
145
  }
131
- @layout[:showlegend] = false
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)
132
248
  end
133
249
 
134
- def box_plot(plot_data, positions, color:, gray:,
135
- width: 0.8r, flier_size: 5, whisker: 1.5, notch: false)
136
- color = Array(color).map(&:to_hex_string)
137
- plot_data.each_with_index do |group_data, i|
138
- data = if group_data.empty?
139
- {type: :box, y: [] }
140
- else
141
- {type: :box, y: group_data, marker: {color: color[i]}}
142
- end
143
- @traces << data
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
144
284
  end
145
- @layout[:showlegend] = false
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)
146
369
  end
147
370
 
148
371
  def set_xlabel(label)
@@ -161,21 +384,100 @@ module Charty
161
384
  @layout[:xaxis][:tickvals] = values
162
385
  end
163
386
 
387
+ def set_yticks(values)
388
+ @layout[:yaxis] ||= {}
389
+ @layout[:yaxis][:tickmode] = "array"
390
+ @layout[:yaxis][:tickvals] = values
391
+ end
392
+
164
393
  def set_xtick_labels(labels)
165
394
  @layout[:xaxis] ||= {}
166
395
  @layout[:xaxis][:tickmode] = "array"
167
396
  @layout[:xaxis][:ticktext] = labels
168
397
  end
169
398
 
399
+ def set_ytick_labels(labels)
400
+ @layout[:yaxis] ||= {}
401
+ @layout[:yaxis][:tickmode] = "array"
402
+ @layout[:yaxis][:ticktext] = labels
403
+ end
404
+
170
405
  def set_xlim(min, max)
171
406
  @layout[:xaxis] ||= {}
172
407
  @layout[:xaxis][:range] = [min, max]
173
408
  end
174
409
 
410
+ def set_ylim(min, max)
411
+ @layout[:yaxis] ||= {}
412
+ @layout[:yaxis][:range] = [min, max]
413
+ end
414
+
175
415
  def disable_xaxis_grid
176
416
  # do nothing
177
417
  end
178
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
+
179
481
  def show
180
482
  unless defined?(IRuby)
181
483
  raise NotImplementedError,