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