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
@@ -0,0 +1,7 @@
1
+ module Charty
2
+ module Plotters
3
+ class CountPlotter < BarPlotter
4
+ self.require_numeric = false
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,84 @@
1
+ module Charty
2
+ module Plotters
3
+ module EstimationSupport
4
+ attr_reader :estimator
5
+
6
+ def estimator=(estimator)
7
+ @estimator = check_estimator(estimator)
8
+ end
9
+
10
+ module_function def check_estimator(value)
11
+ case value
12
+ when :count, "count"
13
+ :count
14
+ when :mean, "mean"
15
+ :mean
16
+ when :median
17
+ raise NotImplementedError,
18
+ "median estimator has not been supported yet"
19
+ when Proc
20
+ raise NotImplementedError,
21
+ "a callable estimator has not been supported yet"
22
+ else
23
+ raise ArgumentError,
24
+ "invalid value for estimator (%p for :mean)" % value
25
+ end
26
+ end
27
+
28
+ attr_reader :ci
29
+
30
+ def ci=(ci)
31
+ @ci = check_ci(ci)
32
+ end
33
+
34
+ private def check_ci(value)
35
+ case value
36
+ when nil
37
+ nil
38
+ when :sd, "sd"
39
+ :sd
40
+ when 0..100
41
+ value
42
+ when Numeric
43
+ raise ArgumentError,
44
+ "ci must be in 0..100, but %p is given" % value
45
+ else
46
+ raise ArgumentError,
47
+ "invalid value for ci (%p for nil, :sd, or a number in 0..100)" % value
48
+ end
49
+ end
50
+
51
+ attr_reader :n_boot
52
+
53
+ def n_boot=(n_boot)
54
+ @n_boot = check_n_boot(n_boot)
55
+ end
56
+
57
+ private def check_n_boot(value)
58
+ case value
59
+ when Integer
60
+ if value <= 0
61
+ raise ArgumentError,
62
+ "n_boot must be larger than zero, but %p is given" % value
63
+ end
64
+ value
65
+ else
66
+ raise ArgumentError,
67
+ "invalid value for n_boot (%p for an integer > 0)" % value
68
+ end
69
+ end
70
+
71
+ attr_reader :units
72
+
73
+ def units=(units)
74
+ @units = check_dimension(units, :units)
75
+ unless units.nil?
76
+ raise NotImplementedError,
77
+ "Specifying units variable is not supported yet"
78
+ end
79
+ end
80
+
81
+ include RandomSupport
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,25 @@
1
+ module Charty
2
+ module Plotters
3
+ module RandomSupport
4
+ attr_reader :random
5
+
6
+ def random=(random)
7
+ @random = check_random(random)
8
+ end
9
+
10
+ module_function def check_random(random)
11
+ case random
12
+ when nil
13
+ Random.new
14
+ when Integer
15
+ Random.new(random)
16
+ when Random
17
+ random
18
+ else
19
+ raise ArgumentError,
20
+ "invalid value for random (%p for a generator or a seed value)" % value
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,518 @@
1
+ module Charty
2
+ module Plotters
3
+ class BaseMapper
4
+ def initialize(plotter, *params)
5
+ @plotter = plotter
6
+ initialize_mapping(*params)
7
+ end
8
+
9
+ attr_reader :plotter
10
+
11
+ def [](key, *args)
12
+ case key
13
+ when Array, Charty::Vector
14
+ key.map {|k| lookup_single_value(k, *args) }
15
+ else
16
+ lookup_single_value(key, *args)
17
+ end
18
+ end
19
+ end
20
+
21
+ class ColorMapper < BaseMapper
22
+ private def initialize_mapping(palette, order, norm)
23
+ @palette = palette
24
+ @order = order
25
+ @norm = norm
26
+
27
+ if plotter.variables.key?(:color)
28
+ data = plotter.plot_data[:color]
29
+ end
30
+
31
+ if data && data.notnull.any?
32
+ @map_type = infer_map_type(@palette, @norm, @plotter.input_format, @plotter.var_types[:color])
33
+
34
+ case @map_type
35
+ when :numeric
36
+ raise NotImplementedError,
37
+ "numeric color mapping is not supported"
38
+ when :categorical
39
+ @cmap = nil
40
+ @norm = nil
41
+ @levels, @lookup_table = categorical_mapping(data, @palette, @order)
42
+ else
43
+ raise NotImplementedError,
44
+ "datetime color mapping is not supported"
45
+ end
46
+ end
47
+ end
48
+
49
+ private def categorical_mapping(data, palette, order)
50
+ levels = data.categorical_order(order)
51
+ n_colors = levels.length
52
+
53
+ case palette
54
+ when Hash
55
+ missing_keys = levels - palette.keys
56
+ unless missing_keys.empty?
57
+ raise ArgumentError,
58
+ "The palette hash is missing keys: %p" % missing_keys
59
+ end
60
+ return levels, palette
61
+
62
+ when nil
63
+ current_palette = Palette.default
64
+ if n_colors <= current_palette.n_colors
65
+ colors = Palette.new(current_palette.colors, n_colors).colors
66
+ else
67
+ colors = Palette.husl_colors(n_colors)
68
+ end
69
+ when Array
70
+ if palette.length != n_colors
71
+ raise ArgumentError,
72
+ "The palette list has the wrong number of colors"
73
+ end
74
+ colors = palette
75
+ else
76
+ colors = Palette.new(palette, n_colors).colors
77
+ end
78
+ lookup_table = levels.zip(colors).to_h
79
+
80
+ return levels, lookup_table
81
+ end
82
+
83
+ private def infer_map_type(palette, norm, input_format, var_type)
84
+ case
85
+ when false # palette is qualitative_palette
86
+ :categorical
87
+ when ! norm.nil?
88
+ :numeric
89
+ when palette.is_a?(Array),
90
+ palette.is_a?(Hash)
91
+ :categorical
92
+ when input_format == :wide
93
+ :categorical
94
+ else
95
+ var_type
96
+ end
97
+ end
98
+
99
+ attr_reader :palette, :order, :norm, :levels, :lookup_table, :map_type
100
+
101
+ def inverse_lookup_table
102
+ lookup_table.invert
103
+ end
104
+
105
+ def lookup_single_value(key)
106
+ if @lookup_table.key?(key)
107
+ @lookup_table[key]
108
+ elsif @norm
109
+ # Use the colormap to interpolate between existing datapoints
110
+ raise NotImplementedError,
111
+ "Palette interpolation is not implemented yet"
112
+ # begin
113
+ # normed = @norm.(key)
114
+ # rescue ArgumentError, TypeError => err
115
+ # if key.respond_to?(:nan?) && key.nan?
116
+ # return "#000000"
117
+ # else
118
+ # raise err
119
+ # end
120
+ # end
121
+ end
122
+ end
123
+ end
124
+
125
+ class SizeMapper < BaseMapper
126
+ # TODO: This should be replaced with red-colors's Normalize feature
127
+ class SimpleNormalizer
128
+ def initialize(vmin=nil, vmax=nil)
129
+ @vmin = vmin
130
+ @vmax = vmax
131
+ end
132
+
133
+ attr_accessor :vmin, :vmax
134
+
135
+ def call(value, clip=nil)
136
+ scalar_p = false
137
+ vector_p = false
138
+ case value
139
+ when Charty::Vector
140
+ vector_p = true
141
+ value = value.to_a
142
+ when Array
143
+ # do nothing
144
+ else
145
+ scalar_p = true
146
+ value = [value]
147
+ end
148
+
149
+ @vmin = value.min if vmin.nil?
150
+ @vmax = value.max if vmax.nil?
151
+
152
+ result = value.map {|x| (x - vmin) / (vmax - vmin).to_f }
153
+
154
+ case
155
+ when scalar_p
156
+ result[0]
157
+ when vector_p
158
+ Charty::Vector.new(result, index: value.index)
159
+ else
160
+ result
161
+ end
162
+ end
163
+ end
164
+
165
+ private def initialize_mapping(sizes, order, norm)
166
+ @sizes = sizes
167
+ @order = order
168
+ @norm = norm
169
+
170
+ return unless plotter.variables.key?(:size)
171
+
172
+ data = plotter.plot_data[:size]
173
+ return unless data.notnull.any?
174
+
175
+ @map_type = infer_map_type(sizes, norm, @plotter.var_types[:size])
176
+ case @map_type
177
+ when :numeric
178
+ @levels, @lookup_table, @norm = numeric_mapping(data, sizes, norm)
179
+ when :categorical
180
+ @levels, @lookup_table = categorical_mapping(data, sizes, order)
181
+ else
182
+ raise NotImplementedError,
183
+ "datetime color mapping is not supported"
184
+ end
185
+ end
186
+
187
+ private def infer_map_type(sizes, norm, var_type)
188
+ case
189
+ when ! norm.nil?
190
+ :numeric
191
+ when sizes.is_a?(Hash),
192
+ sizes.is_a?(Array)
193
+ :categorical
194
+ else
195
+ var_type
196
+ end
197
+ end
198
+
199
+ private def numeric_mapping(data, sizes, norm)
200
+ case sizes
201
+ when Hash
202
+ # The presence of a norm object overrides a dictionary of sizes
203
+ # in specifying a numeric mapping, so we need to process the
204
+ # dictionary here
205
+ levels = sizes.keys.sort
206
+ size_values = sizes.values
207
+ size_range = [size_values.min, size_values.max]
208
+ else
209
+ levels = Charty::Vector.new(data.unique_values).drop_na.to_a
210
+ levels.sort!
211
+
212
+ case sizes
213
+ when Range
214
+ size_range = [sizes.begin, sizes.end]
215
+ when nil
216
+ size_range = [0r, 1r]
217
+ else
218
+ raise ArgumentError,
219
+ "Unable to recognize the value for `sizes`: %p" % sizes
220
+ end
221
+ end
222
+
223
+ # Now we have the minimum and the maximum values of sizes
224
+ case norm
225
+ when nil
226
+ norm = SimpleNormalizer.new
227
+ sizes_scaled = norm.(levels)
228
+ # when Colors::Normalize
229
+ # TODO: Must support red-color's Normalize feature
230
+ else
231
+ raise ArgumentError,
232
+ "Unable to recognize the value for `norm`: %p" % norm
233
+ end
234
+
235
+ case sizes
236
+ when Hash
237
+ # do nothing
238
+ else
239
+ lo, hi = size_range
240
+ sizes = sizes_scaled.map {|x| lo + x * (hi - lo) }
241
+ lookup_table = levels.zip(sizes).to_h
242
+ end
243
+
244
+ return levels, lookup_table, norm
245
+ end
246
+
247
+ private def categorical_mapping(data, sizes, order)
248
+ raise NotImplementedError,
249
+ "A categorical variable for size is not supported"
250
+ end
251
+
252
+ attr_reader :palette, :order, :norm, :levels
253
+
254
+ def lookup_single_value(key)
255
+ if @lookup_table.key?(key)
256
+ @lookup_table[key]
257
+ else
258
+ normed = @norm.(key) || Float::NAN
259
+ size_values = @lookup_table.values
260
+ min, max = size_values.min, size_values.max
261
+ min + normed * (max - min)
262
+ end
263
+ end
264
+
265
+ # TODO
266
+ end
267
+
268
+ class StyleMapper < BaseMapper
269
+ private def initialize_mapping(markers, dashes, order)
270
+ @markers = markers
271
+ @dashes = dashes
272
+ @order = order
273
+
274
+ return unless plotter.variables.key?(:style)
275
+
276
+ data = plotter.plot_data[:style]
277
+ return unless data.notnull.any?
278
+
279
+ @levels = data.categorical_order(order)
280
+
281
+ markers = map_attributes(markers, @levels, unique_markers(@levels.length), :markers)
282
+
283
+ # TODO: dashes support
284
+
285
+ @lookup_table = @levels.map {|key|
286
+ record = {
287
+ marker: markers[key]
288
+ }
289
+ [key, record]
290
+ }.to_h
291
+ end
292
+
293
+ MARKER_NAMES = [
294
+ :circle, :x, :square, :cross, :diamond, :star_diamond,
295
+ :triangle_up, :star_square, :triangle_down, :hexagon, :star, :pentagon,
296
+ ].freeze
297
+
298
+ private def unique_markers(n)
299
+ if n > MARKER_NAMES.length
300
+ raise ArgumentError,
301
+ "Too many markers are required (%p for %p)" % [n, MARKER_NAMES.length]
302
+ end
303
+ MARKER_NAMES[0, n]
304
+ end
305
+
306
+ private def map_attributes(vals, levels, defaults, attr)
307
+ case vals
308
+ when true
309
+ return levels.zip(defaults).to_h
310
+ when Hash
311
+ missing_keys = lavels - vals.keys
312
+ unless missing_keys.empty?
313
+ raise ArgumentError,
314
+ "The `%s` levels are missing values: %p" % [attr, missing_keys]
315
+ end
316
+ return vals
317
+ when Array, Enumerable
318
+ if levels.length != vals.length
319
+ raise ArgumentError,
320
+ "%he `%s` argument has the wrong number of values" % attr
321
+ end
322
+ return levels.zip(vals).to_h
323
+ when nil
324
+ return {}
325
+ else
326
+ raise ArgumentError,
327
+ "Unable to recognize the value for `%s`: %p" % [attr, vals]
328
+ end
329
+ end
330
+
331
+ attr_reader :palette, :order, :norm, :lookup_table, :levels
332
+
333
+ def inverse_lookup_table(attr)
334
+ lookup_table.map { |k, v| [v[attr], k] }.to_h
335
+ end
336
+
337
+ def lookup_single_value(key, attr=nil)
338
+ case attr
339
+ when nil
340
+ @lookup_table[key]
341
+ else
342
+ @lookup_table[key][attr]
343
+ end
344
+ end
345
+ end
346
+
347
+ class RelationalPlotter < AbstractPlotter
348
+ def initialize(x, y, color, style, size, data: nil, **options, &block)
349
+ super(x, y, color, data: data, **options, &block)
350
+
351
+ self.style = style
352
+ self.size = size
353
+
354
+ setup_variables
355
+ end
356
+
357
+ attr_reader :style, :size
358
+
359
+ attr_reader :color_norm
360
+
361
+ attr_reader :sizes, :size_order, :size_norm
362
+
363
+ attr_reader :markers, :marker_order
364
+
365
+ def style=(val)
366
+ @style = check_dimension(val, :style)
367
+ end
368
+
369
+ def size=(val)
370
+ @size = check_dimension(val, :size)
371
+ end
372
+
373
+ def color_norm=(val)
374
+ unless val.nil?
375
+ raise NotImplementedError,
376
+ "Specifying color_norm is not supported yet"
377
+ end
378
+ end
379
+
380
+ def sizes=(val)
381
+ unless val.nil?
382
+ raise NotImplementedError,
383
+ "Specifying sizes is not supported yet"
384
+ end
385
+ end
386
+
387
+ def size_order=(val)
388
+ unless val.nil?
389
+ raise NotImplementedError,
390
+ "Specifying size_order is not supported yet"
391
+ end
392
+ end
393
+
394
+ def size_norm=(val)
395
+ unless val.nil?
396
+ raise NotImplementedError,
397
+ "Specifying size_order is not supported yet"
398
+ end
399
+ end
400
+
401
+ def markers=(val)
402
+ @markers = check_markers(val)
403
+ end
404
+
405
+ private def check_markers(val)
406
+ # TODO
407
+ val
408
+ end
409
+
410
+ def marker_order=(val)
411
+ unless val.nil?
412
+ raise NotImplementedError,
413
+ "Specifying marker_order is not supported yet"
414
+ end
415
+ end
416
+
417
+ attr_reader :input_format, :plot_data, :variables, :var_types
418
+
419
+ private def setup_variables
420
+ if x.nil? && y.nl?
421
+ @input_format = :wide
422
+ setup_variables_with_wide_form_dataset
423
+ else
424
+ @input_format = :long
425
+ setup_variables_with_long_form_dataset
426
+ end
427
+
428
+ @var_types = @plot_data.columns.map { |k|
429
+ [k, variable_type(@plot_data[k], :categorical)]
430
+ }.to_h
431
+ end
432
+
433
+ private def setup_variables_with_wide_form_dataset
434
+ unless color.nil? && style.nil? && size.nil?
435
+ vars = []
436
+ vars << "color" unless color.nil?
437
+ vars << "style" unless style.nil?
438
+ vars << "size" unless size.nil?
439
+ raise ArgumentError,
440
+ "Unable to assign the following variables in wide-form data: " +
441
+ vars.join(", ")
442
+ end
443
+
444
+ if data.nil? || data.empty?
445
+ @plot_data = Charty::Table.new({})
446
+ @variables = {}
447
+ return
448
+ end
449
+
450
+ # TODO: detect flat data
451
+ flat = false
452
+
453
+ if flat
454
+ # TODO: Support flat data
455
+ else
456
+ raise NotImplementedError,
457
+ "wide-form input is not supported"
458
+ end
459
+ end
460
+
461
+ private def setup_variables_with_long_form_dataset
462
+ if data.nil? || data.empty?
463
+ @plot_data = Charty::Table.new({})
464
+ @variables = {}
465
+ return
466
+ end
467
+
468
+ plot_data = {}
469
+ variables = {}
470
+
471
+ {
472
+ x: self.x,
473
+ y: self.y,
474
+ color: self.color,
475
+ style: self.style,
476
+ size: self.size
477
+ }.each do |key, val|
478
+ next if val.nil?
479
+
480
+ if data.column_names.include?(val)
481
+ plot_data[key] = data[val]
482
+ variables[key] = val
483
+ else
484
+ case val
485
+ when Charty::Vector
486
+ plot_data[key] = val
487
+ variables[key] = val.name
488
+ else
489
+ raise ArgumentError,
490
+ "Could not interpret value %p for parameter %p" % [val, key]
491
+ end
492
+ end
493
+ end
494
+
495
+ @plot_data = Charty::Table.new(plot_data)
496
+ @variables = variables.select do |var, name|
497
+ @plot_data[var].notnull.any?
498
+ end
499
+ end
500
+
501
+ private def annotate_axes(backend)
502
+ # TODO
503
+ end
504
+
505
+ private def map_color(palette: nil, order: nil, norm: nil)
506
+ @color_mapper = ColorMapper.new(self, palette, order, norm)
507
+ end
508
+
509
+ private def map_size(sizes: nil, order: nil, norm: nil)
510
+ @size_mapper = SizeMapper.new(self, sizes, order, norm)
511
+ end
512
+
513
+ private def map_style(markers: nil, dashes: nil, order: nil)
514
+ @style_mapper = StyleMapper.new(self, markers, dashes, order)
515
+ end
516
+ end
517
+ end
518
+ end