charty 0.2.3 → 0.2.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) 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 +172 -4
  7. data/Rakefile +4 -5
  8. data/charty.gemspec +10 -6
  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 +8 -1
  20. data/lib/charty/backends/bokeh.rb +2 -2
  21. data/lib/charty/backends/google_charts.rb +1 -1
  22. data/lib/charty/backends/gruff.rb +14 -3
  23. data/lib/charty/backends/plotly.rb +731 -32
  24. data/lib/charty/backends/plotly_helpers/html_renderer.rb +203 -0
  25. data/lib/charty/backends/plotly_helpers/notebook_renderer.rb +87 -0
  26. data/lib/charty/backends/plotly_helpers/plotly_renderer.rb +121 -0
  27. data/lib/charty/backends/pyplot.rb +514 -66
  28. data/lib/charty/backends/rubyplot.rb +1 -1
  29. data/lib/charty/cache_dir.rb +27 -0
  30. data/lib/charty/dash_pattern_generator.rb +57 -0
  31. data/lib/charty/index.rb +213 -0
  32. data/lib/charty/iruby_helper.rb +18 -0
  33. data/lib/charty/linspace.rb +1 -1
  34. data/lib/charty/plot_methods.rb +283 -8
  35. data/lib/charty/plotter.rb +2 -2
  36. data/lib/charty/plotters.rb +11 -0
  37. data/lib/charty/plotters/abstract_plotter.rb +186 -16
  38. data/lib/charty/plotters/bar_plotter.rb +189 -7
  39. data/lib/charty/plotters/box_plotter.rb +64 -11
  40. data/lib/charty/plotters/categorical_plotter.rb +272 -40
  41. data/lib/charty/plotters/count_plotter.rb +7 -0
  42. data/lib/charty/plotters/distribution_plotter.rb +143 -0
  43. data/lib/charty/plotters/estimation_support.rb +84 -0
  44. data/lib/charty/plotters/histogram_plotter.rb +186 -0
  45. data/lib/charty/plotters/line_plotter.rb +300 -0
  46. data/lib/charty/plotters/random_support.rb +25 -0
  47. data/lib/charty/plotters/relational_plotter.rb +635 -0
  48. data/lib/charty/plotters/scatter_plotter.rb +80 -0
  49. data/lib/charty/plotters/vector_plotter.rb +6 -0
  50. data/lib/charty/statistics.rb +96 -2
  51. data/lib/charty/table.rb +160 -15
  52. data/lib/charty/table_adapters.rb +2 -0
  53. data/lib/charty/table_adapters/active_record_adapter.rb +17 -9
  54. data/lib/charty/table_adapters/base_adapter.rb +166 -0
  55. data/lib/charty/table_adapters/daru_adapter.rb +39 -3
  56. data/lib/charty/table_adapters/datasets_adapter.rb +13 -2
  57. data/lib/charty/table_adapters/hash_adapter.rb +141 -16
  58. data/lib/charty/table_adapters/narray_adapter.rb +25 -6
  59. data/lib/charty/table_adapters/nmatrix_adapter.rb +15 -5
  60. data/lib/charty/table_adapters/pandas_adapter.rb +163 -0
  61. data/lib/charty/util.rb +28 -0
  62. data/lib/charty/vector.rb +69 -0
  63. data/lib/charty/vector_adapters.rb +187 -0
  64. data/lib/charty/vector_adapters/array_adapter.rb +101 -0
  65. data/lib/charty/vector_adapters/daru_adapter.rb +163 -0
  66. data/lib/charty/vector_adapters/narray_adapter.rb +182 -0
  67. data/lib/charty/vector_adapters/nmatrix_adapter.rb +37 -0
  68. data/lib/charty/vector_adapters/numpy_adapter.rb +168 -0
  69. data/lib/charty/vector_adapters/pandas_adapter.rb +199 -0
  70. data/lib/charty/version.rb +1 -1
  71. metadata +92 -25
@@ -1,25 +1,78 @@
1
1
  module Charty
2
2
  module Plotters
3
3
  class BoxPlotter < CategoricalPlotter
4
- def render
5
- backend = Backends.current
6
- backend.begin_figure
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, **)
7
31
  draw_box_plot(backend)
8
32
  annotate_axes(backend)
9
- backend.show
33
+ backend.invert_yaxis if orient == :h
10
34
  end
11
35
 
12
36
  private def draw_box_plot(backend)
13
- plot_data = @plot_data.each do |group_data|
14
- next nil if group_data.empty?
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
15
44
 
16
- group_data = Array(group_data)
17
- remove_na!(group_data)
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
18
64
 
19
- group_data
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)
20
75
  end
21
- backend.box_plot(plot_data, (0 ... @plot_data.length).to_a,
22
- color: @colors, gray: @gray)
23
76
  end
24
77
  end
25
78
  end
@@ -1,7 +1,33 @@
1
1
  module Charty
2
2
  module Plotters
3
3
  class CategoricalPlotter < AbstractPlotter
4
- def initialize(x, y, color, **options, &block)
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)
5
31
  super
6
32
 
7
33
  setup_variables
@@ -11,9 +37,60 @@ module Charty
11
37
  attr_reader :order
12
38
 
13
39
  def order=(order)
14
- @order = Array(order).map(&method(:normalize_name))
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)
15
65
  end
16
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
+
17
94
  private def normalize_name(value)
18
95
  case value
19
96
  when String, Symbol
@@ -23,12 +100,16 @@ module Charty
23
100
  end
24
101
  end
25
102
 
26
- attr_reader :group_names, :plot_data, :group_label, :value_label
103
+ attr_reader :group_names, :plot_data, :group_label
104
+
105
+ attr_accessor :value_label
27
106
 
28
107
  private def setup_variables
29
108
  if x.nil? && y.nil?
109
+ @input_format = :wide
30
110
  setup_variables_with_wide_form_dataset
31
111
  else
112
+ @input_format = :long
32
113
  setup_variables_with_long_form_dataset
33
114
  end
34
115
  end
@@ -62,36 +143,58 @@ module Charty
62
143
  end
63
144
 
64
145
  private def setup_variables_with_long_form_dataset
65
- x, y = @x, @y
146
+ x = self.x
147
+ y = self.y
148
+ color = self.color
66
149
  if @data
67
- x = @data[x] || x
68
- y = @data[y] || y
150
+ x &&= @data[x] || x
151
+ y &&= @data[y] || y
152
+ color &&= @data[color] || color
69
153
  end
70
154
 
71
155
  # Validate inputs
72
- [x, y].each do |input|
73
- next if array?(input)
156
+ [x, y, color].each do |input|
157
+ next if input.nil? || array?(input)
74
158
  raise RuntimeError,
75
- "Could not interpret interpret input `#{input.inspect}`"
159
+ "Could not interpret input `#{input.inspect}`"
76
160
  end
77
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
+
78
168
  if x.nil? || y.nil?
79
169
  setup_single_data
80
170
  else
81
- # FIXME: Assume vertical plot
82
- groups, vals = x, y
171
+ if orient == :v
172
+ groups, vals = x, y
173
+ else
174
+ groups, vals = y, x
175
+ end
83
176
 
84
177
  if groups.respond_to?(:name)
85
178
  @group_label = groups.name
86
179
  end
87
180
 
88
- if vals.respond_to?(:name)
89
- @value_label = vals.name
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)
90
195
  end
91
196
 
92
- # FIXME: Assume groups has only unique values
93
- @group_names = groups
94
- @plot_data = vals.map {|v| [v] }
197
+ # TODO: Handle units
95
198
  end
96
199
  end
97
200
 
@@ -100,48 +203,177 @@ module Charty
100
203
  "Single data plot is not supported yet"
101
204
  end
102
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
+
103
275
  private def setup_colors
104
- n_colors = @plot_data.length
105
- if @palette.nil?
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
106
284
  current_palette = Palette.default
107
285
  if n_colors <= current_palette.n_colors
108
- palette = Palette.new(current_palette.colors, 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 }
109
294
  else
110
- palette = Palette.husl(n_colors, l: 0.7r)
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
111
305
  end
112
306
  else
113
- case @palette
307
+ case self.palette
114
308
  when Hash
115
- # Assume @palette has a hash table that maps
116
- # group_names to colors
117
- palette = @group_names.map {|gn| @palette[gn] }
118
- else
119
- palette = @palette
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] }
120
315
  end
121
- palette = Palette.new(palette, n_colors)
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
122
321
  end
123
322
 
124
- @colors = palette.colors.map {|c| c.to_rgb }
323
+ @colors = colors.map {|c| c.to_rgb }
125
324
  lightness_values = @colors.map {|c| c.to_hsl.l }
126
325
  lum = lightness_values.min * 0.6r
127
326
  @gray = Colors::RGB.new(lum, lum, lum) # TODO: Use Charty::Gray
128
327
  end
129
328
 
130
- private def annotate_axes(backend)
131
- backend.set_xlabel(@group_label)
132
- backend.set_ylabel(@value_label)
133
- backend.set_xticks((0 ... @plot_data.length).to_a)
134
- backend.set_xtick_labels(@group_names)
135
- backend.disable_xaxis_grid
136
- backend.set_xlim(-0.5, @plot_data.length - 0.5)
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
137
339
  end
138
340
 
139
- private def remove_na!(ary)
140
- ary.reject! do |x|
141
- next true if x.nil?
142
- x.respond_to?(:nan?) && x.nan?
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)
143
376
  end
144
- ary
145
377
  end
146
378
  end
147
379
  end