charty 0.2.3 → 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 (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,