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
@@ -237,22 +237,22 @@ module Charty
237
237
  end
238
238
 
239
239
  def render(filename=nil)
240
- @backend.render(self, filename)
240
+ @backend.old_style_render(self, filename)
241
241
  end
242
242
 
243
- def save(filename=nil)
244
- @backend.save(self, filename)
243
+ def save(filename=nil, **kw)
244
+ @backend.old_style_save(self, filename, **kw)
245
245
  end
246
246
 
247
247
  def apply(backend)
248
248
  case
249
- when !@series.empty?
250
- backend.series = @series
251
- when @function
252
- linspace = Linspace.new(@range[:x], 100)
253
- # TODO: set label with function
254
- # TODO: set ys to xs when gruff curve with function
255
- @series << Series.new(linspace.to_a, linspace.map{|x| @function.call(x) }, label: "function" )
249
+ when !@series.empty?
250
+ backend.series = @series
251
+ when @function
252
+ linspace = Linspace.new(@range[:x], 100)
253
+ # TODO: set label with function
254
+ # TODO: set ys to xs when gruff curve with function
255
+ @series << Series.new(linspace.to_a, linspace.map{|x| @function.call(x) }, label: "function" )
256
256
  end
257
257
 
258
258
  @backend = backend
@@ -0,0 +1,12 @@
1
+ require_relative "plotters/abstract_plotter"
2
+ require_relative "plotters/random_support"
3
+ require_relative "plotters/estimation_support"
4
+ require_relative "plotters/categorical_plotter"
5
+ require_relative "plotters/bar_plotter"
6
+ require_relative "plotters/box_plotter"
7
+ require_relative "plotters/count_plotter"
8
+
9
+ require_relative "plotters/vector_plotter"
10
+ require_relative "plotters/relational_plotter"
11
+ require_relative "plotters/scatter_plotter"
12
+ require_relative "plotters/line_plotter"
@@ -0,0 +1,243 @@
1
+ module Charty
2
+ module Plotters
3
+ class AbstractPlotter
4
+ def initialize(x, y, color, **options)
5
+ self.x = x
6
+ self.y = y
7
+ self.color = color
8
+ self.data = data
9
+ self.palette = palette
10
+ substitute_options(options)
11
+
12
+ @var_levels = {}
13
+ @var_ordered = {x: false, y: false}
14
+
15
+ yield self if block_given?
16
+ end
17
+
18
+ attr_reader :data, :x, :y, :color
19
+ attr_reader :color_order, :key_color, :palette
20
+
21
+ def var_levels
22
+ variables.each_key do |var|
23
+ # TODO: Move mappers from RelationalPlotter to here,
24
+ # and remove the use of instance_variable_get
25
+ if instance_variable_defined?(:"@#{var}_mapper")
26
+ mapper = instance_variable_get(:"@#{var}_mapper")
27
+ @var_levels[var] = mapper.levels
28
+ end
29
+ end
30
+ @var_levels
31
+ end
32
+
33
+ def inspect
34
+ "#<#{self.class}:0x%016x>" % self.object_id
35
+ end
36
+
37
+ def data=(data)
38
+ @data = case data
39
+ when nil, Charty::Table
40
+ data
41
+ when method(:array?)
42
+ Charty::Vector.new(data)
43
+ else
44
+ Charty::Table.new(data)
45
+ end
46
+ end
47
+
48
+ def x=(x)
49
+ @x = check_dimension(x, :x)
50
+ end
51
+
52
+ def y=(y)
53
+ @y = check_dimension(y, :y)
54
+ end
55
+
56
+ def color=(color)
57
+ @color = check_dimension(color, :color)
58
+ end
59
+
60
+ def color_order=(color_order)
61
+ @color_order = color_order
62
+ end
63
+
64
+ # TODO: move to categorical_plotter
65
+ def key_color=(key_color)
66
+ #@key_color = XXX
67
+ unless key_color.nil?
68
+ raise NotImplementedError,
69
+ "Specifying key_color is not supported yet"
70
+ end
71
+ end
72
+
73
+ def palette=(palette)
74
+ @palette = case palette
75
+ when nil, Palette, Symbol, String
76
+ palette
77
+ else
78
+ raise ArgumentError,
79
+ "invalid type for palette (given #{palette.class}, " +
80
+ "expected Palette, Symbol, or String)"
81
+ end
82
+ end
83
+
84
+ private def substitute_options(options)
85
+ options.each do |key, val|
86
+ send("#{key}=", val)
87
+ end
88
+ end
89
+
90
+ private def check_dimension(value, name)
91
+ case value
92
+ when nil, Symbol, String
93
+ value
94
+ when ->(x) { x.respond_to?(:to_str) }
95
+ value.to_str
96
+ when method(:array?)
97
+ Charty::Vector.new(value)
98
+ else
99
+ raise ArgumentError,
100
+ "invalid type of dimension for #{name} (given #{value.inspect})",
101
+ caller
102
+ end
103
+ end
104
+
105
+ private def check_number(value, name, allow_nil: false)
106
+ case value
107
+ when Numeric
108
+ value
109
+ else
110
+ if allow_nil && value.nil?
111
+ nil
112
+ else
113
+ expected = if allow_nil
114
+ "number or nil"
115
+ else
116
+ "number"
117
+ end
118
+ raise ArgumentError,
119
+ "invalid value for #{name} (%p for #{expected})" % value,
120
+ caller
121
+ end
122
+ end
123
+ end
124
+
125
+ private def check_boolean(value, name, allow_nil: false)
126
+ case value
127
+ when true, false
128
+ value
129
+ else
130
+ expected = if allow_nil
131
+ "true, false, or nil"
132
+ else
133
+ "true or false"
134
+ end
135
+ raise ArgumentError,
136
+ "invalid value for #{name} (%p for #{expected})" % value,
137
+ caller
138
+ end
139
+ end
140
+
141
+ private def variable_type(vector, boolean_type=:numeric)
142
+ if vector.numeric?
143
+ :numeric
144
+ elsif vector.categorical?
145
+ :categorical
146
+ else
147
+ case vector[0]
148
+ when true, false
149
+ boolean_type
150
+ else
151
+ :categorical
152
+ end
153
+ end
154
+ end
155
+
156
+ private def array?(value)
157
+ TableAdapters::HashAdapter.array?(value)
158
+ end
159
+
160
+ private def remove_na!(ary)
161
+ ary.reject! {|x| Util.missing?(x) }
162
+ ary
163
+ end
164
+
165
+ private def each_subset(grouping_vars, reverse: false, processed: false, by_facet: true, allow_empty: false, drop_na: true)
166
+ case grouping_vars
167
+ when nil
168
+ grouping_vars = []
169
+ when String, Symbol
170
+ grouping_vars = [grouping_vars.to_sym]
171
+ end
172
+
173
+ if by_facet
174
+ [:col, :row].each do |facet_var|
175
+ grouping_vars << facet_var if variables.key?(facet_var)
176
+ end
177
+ end
178
+
179
+ grouping_vars = grouping_vars.select {|var| variables.key?(var) }
180
+
181
+ data = processed ? processed_data : plot_data
182
+ data = data.drop_na if drop_na
183
+
184
+ levels = var_levels.dup
185
+
186
+ [:x, :y].each do |axis|
187
+ levels[axis] = plot_data[axis].categorical_order()
188
+ if processed
189
+ # TODO: perform inverse conversion of axis scaling here
190
+ end
191
+ end
192
+
193
+ if not grouping_vars.empty?
194
+ grouped = data.group_by(grouping_vars, sort: false)
195
+ grouped.each_group do |group_key, group_data|
196
+ next if group_data.empty? && !allow_empty
197
+
198
+ yield(grouping_vars.zip(group_key).to_h, group_data)
199
+ end
200
+ else
201
+ yield({}, data.dup)
202
+ end
203
+ end
204
+
205
+ def processed_data
206
+ @processed_data ||= calculate_processed_data
207
+ end
208
+
209
+ private def calculate_processed_data
210
+ # TODO: axis scaling support
211
+ plot_data
212
+ end
213
+
214
+ def save(filename, **kwargs)
215
+ backend = Backends.current
216
+ backend.begin_figure
217
+ render_plot(backend, **kwargs)
218
+ backend.save(filename, **kwargs)
219
+ end
220
+
221
+ def render(notebook: false, **kwargs)
222
+ backend = Backends.current
223
+ backend.begin_figure
224
+ render_plot(backend, notebook: notebook, **kwargs)
225
+ backend.render(notebook: notebook, **kwargs)
226
+ end
227
+
228
+ private def render_plot(*, **)
229
+ raise NotImplementedError,
230
+ "subclass must implement #{__method__}"
231
+ end
232
+
233
+ def to_iruby
234
+ render(notebook: iruby_notebook?)
235
+ end
236
+
237
+ private def iruby_notebook?
238
+ return false unless defined?(IRuby)
239
+ true # TODO: Check the server is notebook or not
240
+ end
241
+ end
242
+ end
243
+ end
@@ -0,0 +1,201 @@
1
+ module Charty
2
+ module Plotters
3
+ class BarPlotter < 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 :error_color
13
+
14
+ def error_color=(error_color)
15
+ @error_color = check_error_color(error_color)
16
+ end
17
+
18
+ private def check_error_color(value)
19
+ case value
20
+ when Colors::AbstractColor
21
+ value
22
+ when Array
23
+ Colors::RGB.new(*value)
24
+ when String
25
+ # TODO: Use Colors.parse when it'll be available
26
+ Colors::RGB.parse(value)
27
+ else
28
+ raise ArgumentError,
29
+ "invalid value for error_color (%p for a color, a RGB tripret, or a RGB hex string)" % value
30
+ end
31
+ end
32
+
33
+ attr_reader :error_width
34
+
35
+ def error_width=(error_width)
36
+ @error_width = check_number(error_width, :error_width, allow_nil: true)
37
+ end
38
+
39
+ attr_reader :cap_size
40
+
41
+ def cap_size=(cap_size)
42
+ @cap_size = check_number(cap_size, :cap_size, allow_nil: true)
43
+ end
44
+
45
+ private def render_plot(backend, **)
46
+ draw_bars(backend)
47
+ annotate_axes(backend)
48
+ backend.invert_yaxis if orient == :h
49
+ end
50
+
51
+ private def draw_bars(backend)
52
+ setup_estimations
53
+
54
+ if @plot_colors.nil?
55
+ bar_pos = (0 ... @estimations.length).to_a
56
+ error_colors = bar_pos.map { error_color }
57
+ if @conf_int.empty?
58
+ ci_params = {}
59
+ else
60
+ ci_params = {conf_int: @conf_int, error_colors: error_colors,
61
+ error_width: error_width, cap_size: cap_size}
62
+ end
63
+ backend.bar(bar_pos, nil, @estimations, @colors, orient, **ci_params)
64
+ else
65
+ bar_pos = (0 ... @estimations[0].length).to_a
66
+ error_colors = bar_pos.map { error_color }
67
+ offsets = color_offsets
68
+ width = nested_width
69
+ @color_names.each_with_index do |color_name, i|
70
+ pos = bar_pos.map {|x| x + offsets[i] }
71
+ colors = Array.new(@estimations[i].length) { @colors[i] }
72
+ if @conf_int[i].empty?
73
+ ci_params = {}
74
+ else
75
+ ci_params = {conf_int: @conf_int[i], error_colors: error_colors,
76
+ error_width: error_width, cap_size: cap_size}
77
+ end
78
+ backend.bar(pos, @group_names, @estimations[i], colors, orient,
79
+ label: color_name, width: width, **ci_params)
80
+ end
81
+ end
82
+ end
83
+
84
+ private def setup_estimations
85
+ if @color_names.nil?
86
+ setup_estimations_with_single_color_group
87
+ else
88
+ setup_estimations_with_multiple_color_groups
89
+ end
90
+ end
91
+
92
+ private def setup_estimations_with_single_color_group
93
+ estimations = []
94
+ conf_int = []
95
+
96
+ @plot_data.each do |group_data|
97
+ # Single color group
98
+ if @plot_units.nil?
99
+ stat_data = group_data.drop_na
100
+ unit_data = nil
101
+ else
102
+ # TODO: Support units
103
+ end
104
+
105
+ estimation = if stat_data.size == 0
106
+ Float::NAN
107
+ else
108
+ estimate(estimator, stat_data)
109
+ end
110
+ estimations << estimation
111
+
112
+ unless ci.nil?
113
+ if stat_data.size < 2
114
+ conf_int << [Float::NAN, Float::NAN]
115
+ next
116
+ end
117
+
118
+ if ci == :sd
119
+ sd = stat_data.stdev
120
+ conf_int << [estimation - sd, estimation + sd]
121
+ else
122
+ conf_int << Statistics.bootstrap_ci(stat_data, ci, func: estimator, n_boot: n_boot,
123
+ units: unit_data, random: random)
124
+ end
125
+ end
126
+ end
127
+
128
+ @estimations = estimations
129
+ @conf_int = conf_int
130
+ end
131
+
132
+ private def setup_estimations_with_multiple_color_groups
133
+ estimations = Array.new(@color_names.length) { [] }
134
+ conf_int = Array.new(@color_names.length) { [] }
135
+
136
+ @plot_data.each_with_index do |group_data, i|
137
+ @color_names.each_with_index do |color_name, j|
138
+ if @plot_colors[i].length == 0
139
+ estimations[j] << Float::NAN
140
+ unless ci.nil?
141
+ conf_int[j] << [Float::NAN, Float::NAN]
142
+ end
143
+ next
144
+ end
145
+
146
+ color_mask = @plot_colors[i].eq(color_name)
147
+ if @plot_units.nil?
148
+ begin
149
+ stat_data = group_data[color_mask].drop_na
150
+ rescue
151
+ @plot_data.each_with_index {|pd, k| p k => pd }
152
+ @plot_colors.each_with_index {|pc, k| p k => pc }
153
+ raise
154
+ end
155
+ unit_data = nil
156
+ else
157
+ # TODO: Support units
158
+ end
159
+
160
+ estimation = if stat_data.size == 0
161
+ Float::NAN
162
+ else
163
+ estimate(estimator, stat_data)
164
+ end
165
+ estimations[j] << estimation
166
+
167
+ unless ci.nil?
168
+ if stat_data.size < 2
169
+ conf_int[j] << [Float::NAN, Float::NAN]
170
+ next
171
+ end
172
+
173
+ if ci == :sd
174
+ sd = stat_data.stdev
175
+ conf_int[j] << [estimation - sd, estimation + sd]
176
+ else
177
+ conf_int[j] << Statistics.bootstrap_ci(stat_data, ci, func: estimator, n_boot: n_boot,
178
+ units: unit_data, random: random)
179
+ end
180
+ end
181
+ end
182
+ end
183
+
184
+ @estimations = estimations
185
+ @conf_int = conf_int
186
+ end
187
+
188
+ private def estimate(estimator, data)
189
+ case estimator
190
+ when :count
191
+ data.length
192
+ when :mean
193
+ data.mean
194
+ else
195
+ # TODO: Support other estimations
196
+ raise NotImplementedError, "#{estimator} estimator is not supported yet"
197
+ end
198
+ end
199
+ end
200
+ end
201
+ end