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,380 @@
1
+ module Charty
2
+ module Plotters
3
+ class CategoricalPlotter < AbstractPlotter
4
+ class << self
5
+ attr_reader :default_palette
6
+
7
+ def default_palette=(val)
8
+ case val
9
+ when :light, :dark
10
+ @default_palette = val
11
+ when "light", "dark"
12
+ @default_palette = val.to_sym
13
+ else
14
+ raise ArgumentError, "default_palette must be :light or :dark"
15
+ end
16
+ end
17
+
18
+ attr_reader :require_numeric
19
+
20
+ def require_numeric=(val)
21
+ case val
22
+ when true, false
23
+ @require_numeric = val
24
+ else
25
+ raise ArgumentError, "require_numeric must be ture or false"
26
+ end
27
+ end
28
+ end
29
+
30
+ def initialize(x, y, color, order: nil, orient: nil, width: 0.8r, dodge: false, **options, &block)
31
+ super
32
+
33
+ setup_variables
34
+ setup_colors
35
+ end
36
+
37
+ attr_reader :order
38
+
39
+ def order=(order)
40
+ @order = order && Array(order).map(&method(:normalize_name))
41
+ end
42
+
43
+ attr_reader :orient
44
+
45
+ def orient=(orient)
46
+ @orient = check_orient(orient)
47
+ end
48
+
49
+ private def check_orient(value)
50
+ case value
51
+ when nil, :v, :h
52
+ value
53
+ when "v", "h"
54
+ value.to_sym
55
+ else
56
+ raise ArgumentError,
57
+ "invalid value for orient (#{value.inspect} for nil, :v, or :h)"
58
+ end
59
+ end
60
+
61
+ attr_reader :width
62
+
63
+ def width=(val)
64
+ @width = check_number(val, :width)
65
+ end
66
+
67
+ attr_reader :dodge
68
+
69
+ def dodge=(dodge)
70
+ @dodge = check_boolean(dodge, :dodge)
71
+ end
72
+
73
+ attr_reader :saturation
74
+
75
+ def saturation=(saturation)
76
+ @saturation = check_saturation(saturation)
77
+ end
78
+
79
+ private def check_saturation(value)
80
+ case value
81
+ when 0..1
82
+ value
83
+ when Numeric
84
+ raise ArgumentError,
85
+ "saturation is out of range (%p for 0..1)" % value
86
+ else
87
+ raise ArgumentError,
88
+ "invalid value for saturation (%p for a value in 0..1)" % value
89
+ end
90
+ end
91
+
92
+ include EstimationSupport
93
+
94
+ private def normalize_name(value)
95
+ case value
96
+ when String, Symbol
97
+ value
98
+ else
99
+ value.to_str
100
+ end
101
+ end
102
+
103
+ attr_reader :group_names, :plot_data, :group_label
104
+
105
+ attr_accessor :value_label
106
+
107
+ private def setup_variables
108
+ if x.nil? && y.nil?
109
+ @input_format = :wide
110
+ setup_variables_with_wide_form_dataset
111
+ else
112
+ @input_format = :long
113
+ setup_variables_with_long_form_dataset
114
+ end
115
+ end
116
+
117
+ private def setup_variables_with_wide_form_dataset
118
+ if @color
119
+ raise ArgumentError,
120
+ "Cannot use `color` without `x` or `y`"
121
+ end
122
+
123
+ # No color grouping with wide inputs
124
+ @plot_colors = nil
125
+ @color_title = nil
126
+ @color_names = nil
127
+
128
+ # No statistical units with wide inputs
129
+ @plot_units = nil
130
+
131
+ @value_label = nil
132
+ @group_label = nil
133
+
134
+ order = @order # TODO: supply order via parameter
135
+ unless order
136
+ order = @data.column_names.select do |cn|
137
+ @data[cn].all? {|x| Float(x, exception: false) }
138
+ end
139
+ end
140
+ order ||= @data.column_names
141
+ @plot_data = order.map {|cn| @data[cn] }
142
+ @group_names = order
143
+ end
144
+
145
+ private def setup_variables_with_long_form_dataset
146
+ x = self.x
147
+ y = self.y
148
+ color = self.color
149
+ if @data
150
+ x &&= @data[x] || x
151
+ y &&= @data[y] || y
152
+ color &&= @data[color] || color
153
+ end
154
+
155
+ # Validate inputs
156
+ [x, y, color].each do |input|
157
+ next if input.nil? || array?(input)
158
+ raise RuntimeError,
159
+ "Could not interpret input `#{input.inspect}`"
160
+ end
161
+
162
+ x = Charty::Vector.try_convert(x)
163
+ y = Charty::Vector.try_convert(y)
164
+ color = Charty::Vector.try_convert(color)
165
+
166
+ self.orient = infer_orient(x, y, orient, self.class.require_numeric)
167
+
168
+ if x.nil? || y.nil?
169
+ setup_single_data
170
+ else
171
+ if orient == :v
172
+ groups, vals = x, y
173
+ else
174
+ groups, vals = y, x
175
+ end
176
+
177
+ if groups.respond_to?(:name)
178
+ @group_label = groups.name
179
+ end
180
+
181
+ @group_names = groups.categorical_order(order)
182
+ @plot_data, @value_label = group_long_form(vals, groups, @group_names)
183
+
184
+ # Handle color variable
185
+ if color.nil?
186
+ @plot_colors = nil
187
+ @color_title = nil
188
+ @color_names = nil
189
+ else
190
+ # Get the order of color levels
191
+ @color_names = color.categorical_order(color_order)
192
+
193
+ # Group the color data
194
+ @plot_colors, @color_title = group_long_form(color, groups, @group_names)
195
+ end
196
+
197
+ # TODO: Handle units
198
+ end
199
+ end
200
+
201
+ private def setup_single_data
202
+ raise NotImplementedError,
203
+ "Single data plot is not supported yet"
204
+ end
205
+
206
+ private def infer_orient(x, y, orient, require_numeric)
207
+ x_type = x.nil? ? nil : variable_type(x)
208
+ y_type = y.nil? ? nil : variable_type(y)
209
+
210
+ nonnumeric_error = "%{orient} orientation requires numeric `%{dim}` variable"
211
+ single_variable_warning = "%{orient} orientation ignored with only `%{dim}` specified"
212
+
213
+ case
214
+ when x.nil?
215
+ case orient
216
+ when :h
217
+ warn single_variable_warning % {orient: "Horizontal", dim: "y"}
218
+ end
219
+ if require_numeric && y_type != :numeric
220
+ raise ArgumentError, nonnumeric_error % {orient: "Vertical", dim: "y"}
221
+ end
222
+ return :v
223
+ when y.nil?
224
+ case orient
225
+ when :v
226
+ warn single_variable_warning % {orient: "Vertical", dim: "x"}
227
+ end
228
+ if require_numeric && x_type != :numeric
229
+ raise ArgumentError, nonnumeric_error % {orient: "Horizontal", dim: "x"}
230
+ end
231
+ return :h
232
+ end
233
+ case orient
234
+ when :v
235
+ if require_numeric && y_type != :numeric
236
+ raise ArgumentError, nonnumeric_error % {orient: "Vertical", dim: "y"}
237
+ end
238
+ return :v
239
+ when :h
240
+ if require_numeric && x_type != :numeric
241
+ raise ArgumentError, nonnumeric_error % {orient: "Horizontal", dim: "x"}
242
+ end
243
+ return :h
244
+ when nil
245
+ case
246
+ when x_type != :categorical && y_type == :categorical
247
+ return :h
248
+ when x_type != :numeric && y_type == :numeric
249
+ return :v
250
+ when x_type == :numeric && y_type != :numeric
251
+ return :h
252
+ when require_numeric && x_type != :numeric && y_type != :numeric
253
+ raise ArgumentError, "Neither the `x` nor `y` variable appears to be numeric."
254
+ else
255
+ :v
256
+ end
257
+ else
258
+ # must be unreachable
259
+ raise RuntimeError, "BUG in Charty. Please report the issue."
260
+ end
261
+ end
262
+
263
+ private def group_long_form(vals, groups, group_order)
264
+ grouped_vals = vals.group_by(groups)
265
+
266
+ plot_data = group_order.map {|g| grouped_vals[g] || [] }
267
+
268
+ if vals.respond_to?(:name)
269
+ value_label = vals.name
270
+ end
271
+
272
+ return plot_data, value_label
273
+ end
274
+
275
+ private def setup_colors
276
+ if @color_names.nil?
277
+ n_colors = @plot_data.length
278
+ else
279
+ n_colors = @color_names.length
280
+ end
281
+
282
+ if key_color.nil? && self.palette.nil?
283
+ # Check the current palette has enough colors
284
+ current_palette = Palette.default
285
+ if n_colors <= current_palette.n_colors
286
+ colors = Palette.new(current_palette.colors, n_colors).colors
287
+ else
288
+ # Use huls palette as default when the default palette is not usable
289
+ colors = Palette.husl_colors(n_colors, l: 0.7r)
290
+ end
291
+ elsif self.palette.nil?
292
+ if @color_names.nil?
293
+ colors = Array.new(n_colors) { key_color }
294
+ else
295
+ raise NotImplementedError,
296
+ "Default palette with key_color is not supported"
297
+ # TODO: Support light_palette and dark_palette in red-palette
298
+ # if default_palette is light
299
+ # colors = Palette.light_palette(key_color, n_colors)
300
+ # elsif default_palette is dark
301
+ # colors = Palette.dark_palette(key_color, n_colors)
302
+ # else
303
+ # raise "No default palette specified"
304
+ # end
305
+ end
306
+ else
307
+ case self.palette
308
+ when Hash
309
+ if @color_names.nil?
310
+ levels = @group_names
311
+ else
312
+ levels = @color_names
313
+ end
314
+ colors = levels.map {|gn| self.palette[gn] }
315
+ end
316
+ colors = Palette.new(colors, n_colors).colors
317
+ end
318
+
319
+ if saturation < 1
320
+ colors = Palette.new(colors, n_colors, desaturate_factor: saturation).colors
321
+ end
322
+
323
+ @colors = colors.map {|c| c.to_rgb }
324
+ lightness_values = @colors.map {|c| c.to_hsl.l }
325
+ lum = lightness_values.min * 0.6r
326
+ @gray = Colors::RGB.new(lum, lum, lum) # TODO: Use Charty::Gray
327
+ end
328
+
329
+ private def color_offsets
330
+ n_names = @color_names.length
331
+ if self.dodge
332
+ each_width = @width / n_names
333
+ offsets = Charty::Linspace.new(0 .. (@width - each_width), n_names).to_a
334
+ offsets_mean = Statistics.mean(offsets)
335
+ offsets.map {|x| x - offsets_mean }
336
+ else
337
+ Array.new(n_names) { 0 }
338
+ end
339
+ end
340
+
341
+ private def nested_width
342
+ if self.dodge
343
+ @width / @color_names.length * 0.98r
344
+ else
345
+ @width
346
+ end
347
+ end
348
+
349
+ private def annotate_axes(backend)
350
+ if orient == :v
351
+ xlabel, ylabel = @group_label, @value_label
352
+ else
353
+ xlabel, ylabel = @value_label, @group_label
354
+ end
355
+ backend.set_xlabel(xlabel) unless xlabel.nil?
356
+ backend.set_ylabel(ylabel) unless ylabel.nil?
357
+
358
+ if orient == :v
359
+ backend.set_xticks((0 ... @plot_data.length).to_a)
360
+ backend.set_xtick_labels(@group_names)
361
+ else
362
+ backend.set_yticks((0 ... @plot_data.length).to_a)
363
+ backend.set_ytick_labels(@group_names)
364
+ end
365
+
366
+ if orient == :v
367
+ backend.disable_xaxis_grid
368
+ backend.set_xlim(-0.5, @plot_data.length - 0.5)
369
+ else
370
+ backend.disable_yaxis_grid
371
+ backend.set_ylim(-0.5, @plot_data.length - 0.5)
372
+ end
373
+
374
+ unless @color_names.nil?
375
+ backend.legend(loc: :best, title: @color_title)
376
+ end
377
+ end
378
+ end
379
+ end
380
+ end
@@ -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