charty 0.2.0 → 0.2.6

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 (78) 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 +177 -9
  8. data/Rakefile +4 -5
  9. data/charty.gemspec +10 -4
  10. data/examples/Gemfile +1 -0
  11. data/examples/active_record.ipynb +1 -1
  12. data/examples/daru.ipynb +1 -1
  13. data/examples/iris_dataset.ipynb +1 -1
  14. data/examples/nmatrix.ipynb +1 -1
  15. data/examples/{numo-narray.ipynb → numo_narray.ipynb} +1 -1
  16. data/examples/palette.rb +71 -0
  17. data/examples/sample.png +0 -0
  18. data/examples/sample_images/hist_gruff.png +0 -0
  19. data/examples/sample_pyplot.ipynb +40 -38
  20. data/images/penguins_body_mass_g_flipper_length_mm_scatter_plot.png +0 -0
  21. data/images/penguins_body_mass_g_flipper_length_mm_species_scatter_plot.png +0 -0
  22. data/images/penguins_body_mass_g_flipper_length_mm_species_sex_scatter_plot.png +0 -0
  23. data/images/penguins_species_body_mass_g_bar_plot_h.png +0 -0
  24. data/images/penguins_species_body_mass_g_bar_plot_v.png +0 -0
  25. data/images/penguins_species_body_mass_g_box_plot_h.png +0 -0
  26. data/images/penguins_species_body_mass_g_box_plot_v.png +0 -0
  27. data/images/penguins_species_body_mass_g_sex_bar_plot_v.png +0 -0
  28. data/images/penguins_species_body_mass_g_sex_box_plot_v.png +0 -0
  29. data/lib/charty.rb +13 -1
  30. data/lib/charty/backend_methods.rb +8 -0
  31. data/lib/charty/backends.rb +26 -1
  32. data/lib/charty/backends/bokeh.rb +31 -31
  33. data/lib/charty/backends/{google_chart.rb → google_charts.rb} +75 -33
  34. data/lib/charty/backends/gruff.rb +14 -3
  35. data/lib/charty/backends/plotly.rb +774 -9
  36. data/lib/charty/backends/pyplot.rb +611 -34
  37. data/lib/charty/backends/rubyplot.rb +2 -2
  38. data/lib/charty/backends/unicode_plot.rb +79 -0
  39. data/lib/charty/dash_pattern_generator.rb +57 -0
  40. data/lib/charty/index.rb +213 -0
  41. data/lib/charty/linspace.rb +1 -1
  42. data/lib/charty/plot_methods.rb +254 -0
  43. data/lib/charty/plotter.rb +10 -10
  44. data/lib/charty/plotters.rb +12 -0
  45. data/lib/charty/plotters/abstract_plotter.rb +243 -0
  46. data/lib/charty/plotters/bar_plotter.rb +201 -0
  47. data/lib/charty/plotters/box_plotter.rb +79 -0
  48. data/lib/charty/plotters/categorical_plotter.rb +380 -0
  49. data/lib/charty/plotters/count_plotter.rb +7 -0
  50. data/lib/charty/plotters/estimation_support.rb +84 -0
  51. data/lib/charty/plotters/line_plotter.rb +300 -0
  52. data/lib/charty/plotters/random_support.rb +25 -0
  53. data/lib/charty/plotters/relational_plotter.rb +635 -0
  54. data/lib/charty/plotters/scatter_plotter.rb +80 -0
  55. data/lib/charty/plotters/vector_plotter.rb +6 -0
  56. data/lib/charty/statistics.rb +114 -0
  57. data/lib/charty/table.rb +161 -15
  58. data/lib/charty/table_adapters.rb +2 -0
  59. data/lib/charty/table_adapters/active_record_adapter.rb +17 -9
  60. data/lib/charty/table_adapters/base_adapter.rb +166 -0
  61. data/lib/charty/table_adapters/daru_adapter.rb +41 -3
  62. data/lib/charty/table_adapters/datasets_adapter.rb +17 -2
  63. data/lib/charty/table_adapters/hash_adapter.rb +143 -16
  64. data/lib/charty/table_adapters/narray_adapter.rb +25 -6
  65. data/lib/charty/table_adapters/nmatrix_adapter.rb +15 -5
  66. data/lib/charty/table_adapters/pandas_adapter.rb +163 -0
  67. data/lib/charty/util.rb +28 -0
  68. data/lib/charty/vector.rb +69 -0
  69. data/lib/charty/vector_adapters.rb +187 -0
  70. data/lib/charty/vector_adapters/array_adapter.rb +101 -0
  71. data/lib/charty/vector_adapters/daru_adapter.rb +163 -0
  72. data/lib/charty/vector_adapters/narray_adapter.rb +182 -0
  73. data/lib/charty/vector_adapters/nmatrix_adapter.rb +37 -0
  74. data/lib/charty/vector_adapters/numpy_adapter.rb +168 -0
  75. data/lib/charty/vector_adapters/pandas_adapter.rb +199 -0
  76. data/lib/charty/version.rb +1 -1
  77. metadata +121 -22
  78. data/.travis.yml +0 -10
@@ -0,0 +1,79 @@
1
+ module Charty
2
+ module Plotters
3
+ class BoxPlotter < CategoricalPlotter
4
+ self.default_palette = :light
5
+ self.require_numeric = true
6
+
7
+ def initialize(data: nil, variables: {}, **options, &block)
8
+ x, y, color = variables.values_at(:x, :y, :color)
9
+ super(x, y, color, data: data, **options, &block)
10
+ end
11
+
12
+ attr_reader :flier_size
13
+
14
+ def flier_size=(val)
15
+ @flier_size = check_number(val, :flier_size, allow_nil: true)
16
+ end
17
+
18
+ attr_reader :line_width
19
+
20
+ def line_width=(val)
21
+ @line_width = check_number(val, :line_width, allow_nil: true)
22
+ end
23
+
24
+ attr_reader :whisker
25
+
26
+ def whisker=(val)
27
+ @whisker = check_number(val, :whisker, allow_nil: true)
28
+ end
29
+
30
+ private def render_plot(backend, **)
31
+ draw_box_plot(backend)
32
+ annotate_axes(backend)
33
+ backend.invert_yaxis if orient == :h
34
+ end
35
+
36
+ private def draw_box_plot(backend)
37
+ if @plot_colors.nil?
38
+ plot_data = @plot_data.map do |group_data|
39
+ unless group_data.empty?
40
+ group_data = group_data.drop_na
41
+ group_data unless group_data.empty?
42
+ end
43
+ end
44
+
45
+ backend.box_plot(plot_data,
46
+ @group_names,
47
+ orient: orient,
48
+ colors: @colors,
49
+ gray: @gray,
50
+ dodge: dodge,
51
+ width: @width,
52
+ flier_size: flier_size,
53
+ whisker: whisker)
54
+ else
55
+ grouped_box_data = @color_names.map.with_index do |color_name, i|
56
+ @plot_data.map.with_index do |group_data, j|
57
+ unless group_data.empty?
58
+ color_mask = @plot_colors[j].eq(color_name)
59
+ group_data = group_data[color_mask].drop_na
60
+ group_data unless group_data.empty?
61
+ end
62
+ end
63
+ end
64
+
65
+ backend.grouped_box_plot(grouped_box_data,
66
+ @group_names,
67
+ @color_names,
68
+ orient: orient,
69
+ colors: @colors,
70
+ gray: @gray,
71
+ dodge: dodge,
72
+ width: @width,
73
+ flier_size: flier_size,
74
+ whisker: whisker)
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -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