charty 0.2.7 → 0.2.11

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/charty.gemspec +1 -0
  3. data/examples/bar_plot.rb +19 -0
  4. data/examples/box_plot.rb +17 -0
  5. data/examples/scatter_plot.rb +17 -0
  6. data/images/penguins_body_mass_g_flipper_length_mm_species_scatter_plot.png +0 -0
  7. data/images/penguins_body_mass_g_flipper_length_mm_species_sex_scatter_plot.png +0 -0
  8. data/images/penguins_species_body_mass_g_bar_plot_h.png +0 -0
  9. data/images/penguins_species_body_mass_g_bar_plot_v.png +0 -0
  10. data/images/penguins_species_body_mass_g_box_plot_h.png +0 -0
  11. data/images/penguins_species_body_mass_g_box_plot_v.png +0 -0
  12. data/images/penguins_species_body_mass_g_sex_bar_plot_v.png +0 -0
  13. data/images/penguins_species_body_mass_g_sex_box_plot_v.png +0 -0
  14. data/lib/charty/backends/plotly.rb +53 -22
  15. data/lib/charty/backends/plotly_helpers/notebook_renderer.rb +4 -1
  16. data/lib/charty/backends/pyplot.rb +73 -0
  17. data/lib/charty/backends/unicode_plot.rb +16 -11
  18. data/lib/charty/index.rb +9 -0
  19. data/lib/charty/plot_methods.rb +46 -10
  20. data/lib/charty/plotters/abstract_plotter.rb +41 -9
  21. data/lib/charty/plotters/bar_plotter.rb +39 -0
  22. data/lib/charty/plotters/categorical_plotter.rb +9 -1
  23. data/lib/charty/plotters/distribution_plotter.rb +44 -7
  24. data/lib/charty/plotters/histogram_plotter.rb +97 -35
  25. data/lib/charty/plotters/line_plotter.rb +38 -5
  26. data/lib/charty/plotters/scatter_plotter.rb +4 -2
  27. data/lib/charty/statistics.rb +2 -2
  28. data/lib/charty/table.rb +30 -23
  29. data/lib/charty/table_adapters/arrow_adapter.rb +53 -0
  30. data/lib/charty/table_adapters/base_adapter.rb +88 -0
  31. data/lib/charty/table_adapters/daru_adapter.rb +41 -1
  32. data/lib/charty/table_adapters/hash_adapter.rb +58 -10
  33. data/lib/charty/table_adapters/pandas_adapter.rb +49 -1
  34. data/lib/charty/table_adapters.rb +1 -0
  35. data/lib/charty/vector.rb +30 -2
  36. data/lib/charty/vector_adapters/array_adapter.rb +1 -1
  37. data/lib/charty/vector_adapters/arrow_adapter.rb +156 -0
  38. data/lib/charty/vector_adapters/daru_adapter.rb +3 -6
  39. data/lib/charty/vector_adapters/narray_adapter.rb +10 -1
  40. data/lib/charty/vector_adapters/nmatrix_adapter.rb +1 -5
  41. data/lib/charty/vector_adapters/numpy_adapter.rb +4 -0
  42. data/lib/charty/vector_adapters/pandas_adapter.rb +10 -1
  43. data/lib/charty/vector_adapters/vector_adapter.rb +62 -0
  44. data/lib/charty/vector_adapters.rb +22 -0
  45. data/lib/charty/version.rb +1 -1
  46. metadata +23 -3
@@ -35,6 +35,8 @@ module Charty
35
35
  end
36
36
 
37
37
  def data=(data)
38
+ # TODO: Convert a Charty::Vector to a Charty::Table so that
39
+ # the Charty::Vector is handled as a wide form data
38
40
  @data = case data
39
41
  when nil, Charty::Table
40
42
  data
@@ -81,6 +83,24 @@ module Charty
81
83
  end
82
84
  end
83
85
 
86
+ attr_reader :x_label
87
+
88
+ def x_label=(val)
89
+ @x_label = check_string(val, :x_label, allow_nil: true)
90
+ end
91
+
92
+ attr_reader :y_label
93
+
94
+ def y_label=(val)
95
+ @y_label = check_string(val, :y_label, allow_nil: true)
96
+ end
97
+
98
+ attr_reader :title
99
+
100
+ def title=(val)
101
+ @title = check_string(val, :title, allow_nil: true)
102
+ end
103
+
84
104
  private def substitute_options(options)
85
105
  options.each do |key, val|
86
106
  send("#{key}=", val)
@@ -138,6 +158,27 @@ module Charty
138
158
  end
139
159
  end
140
160
 
161
+ private def check_string(value, name, allow_nil: false)
162
+ case value
163
+ when Symbol
164
+ value.to_s
165
+ else
166
+ if allow_nil && value.nil?
167
+ nil
168
+ else
169
+ orig_value = value
170
+ value = String.try_convert(value)
171
+ if value.nil?
172
+ raise ArgumentError,
173
+ "`#{name}` must be convertible to String: %p" % orig_value,
174
+ caller
175
+ else
176
+ value
177
+ end
178
+ end
179
+ end
180
+ end
181
+
141
182
  private def variable_type(vector, boolean_type=:numeric)
142
183
  if vector.numeric?
143
184
  :numeric
@@ -181,15 +222,6 @@ module Charty
181
222
  data = processed ? processed_data : plot_data
182
223
  data = data.drop_na if drop_na
183
224
 
184
- levels = var_levels.dup
185
-
186
- ([:x, :y] & grouping_vars).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
225
  if not grouping_vars.empty?
194
226
  grouped = data.group_by(grouping_vars, sort: false)
195
227
  grouped.each_group do |group_key, group_data|
@@ -42,6 +42,12 @@ module Charty
42
42
  @cap_size = check_number(cap_size, :cap_size, allow_nil: true)
43
43
  end
44
44
 
45
+ attr_reader :log
46
+
47
+ def log=(val)
48
+ @log = check_boolean(val, :log)
49
+ end
50
+
45
51
  private def render_plot(backend, **)
46
52
  draw_bars(backend)
47
53
  annotate_axes(backend)
@@ -81,6 +87,39 @@ module Charty
81
87
  end
82
88
  end
83
89
 
90
+ private def annotate_axes(backend)
91
+ super
92
+
93
+ if self.log
94
+ min_value, max_value = @estimations.minmax
95
+ if @plot_colors
96
+ unless @conf_int.empty?
97
+ min_value = [min_value, @conf_int[0]].min
98
+ max_value = [max_value, @conf_int[1]].max
99
+ end
100
+ else
101
+ ci_min = Util.filter_map(@conf_int) { |ci| ci[0] unless ci.empty? }
102
+ ci_max = Util.filter_map(@conf_int) { |ci| ci[1] unless ci.empty? }
103
+ min_value = [min_value, ci_min.min].min unless ci_min.empty?
104
+ max_value = [max_value, ci_max.max].max unless ci_max.empty?
105
+ end
106
+ if min_value > 1
107
+ min_value = 0
108
+ else
109
+ min_value = Math.log10(min_value).floor
110
+ end
111
+ max_value = Math.log10(max_value).ceil
112
+ case self.orient
113
+ when :v
114
+ backend.set_yscale(:log)
115
+ backend.set_ylim(min_value, max_value)
116
+ else
117
+ backend.set_xscale(:log)
118
+ backend.set_xlim(min_value, max_value)
119
+ end
120
+ end
121
+ end
122
+
84
123
  private def setup_estimations
85
124
  if @color_names.nil?
86
125
  setup_estimations_with_single_color_group
@@ -134,6 +134,7 @@ module Charty
134
134
  order = @order # TODO: supply order via parameter
135
135
  unless order
136
136
  order = @data.column_names.select do |cn|
137
+ # TODO: Use Charty::Vector#numeric?
137
138
  @data[cn].all? {|x| Float(x, exception: false) }
138
139
  end
139
140
  end
@@ -230,6 +231,7 @@ module Charty
230
231
  end
231
232
  return :h
232
233
  end
234
+
233
235
  case orient
234
236
  when :v
235
237
  if require_numeric && y_type != :numeric
@@ -263,7 +265,9 @@ module Charty
263
265
  private def group_long_form(vals, groups, group_order)
264
266
  grouped_vals = vals.group_by(groups)
265
267
 
266
- plot_data = group_order.map {|g| grouped_vals[g] || [] }
268
+ plot_data = group_order.map do |g|
269
+ grouped_vals[g] || Charty::Vector.new([])
270
+ end
267
271
 
268
272
  if vals.respond_to?(:name)
269
273
  value_label = vals.name
@@ -347,11 +351,15 @@ module Charty
347
351
  end
348
352
 
349
353
  private def annotate_axes(backend)
354
+ backend.set_title(self.title) if self.title
355
+
350
356
  if orient == :v
351
357
  xlabel, ylabel = @group_label, @value_label
352
358
  else
353
359
  xlabel, ylabel = @value_label, @group_label
354
360
  end
361
+ xlabel = self.x_label if self.x_label
362
+ ylabel = self.y_label if self.y_label
355
363
  backend.set_xlabel(xlabel) unless xlabel.nil?
356
364
  backend.set_ylabel(ylabel) unless ylabel.nil?
357
365
 
@@ -3,7 +3,14 @@ module Charty
3
3
  class DistributionPlotter < AbstractPlotter
4
4
  def flat_structure
5
5
  {
6
- x: :values
6
+ x: :@values
7
+ }
8
+ end
9
+
10
+ def wide_structure
11
+ {
12
+ x: :@values,
13
+ color: :@columns
7
14
  }
8
15
  end
9
16
 
@@ -14,6 +21,12 @@ module Charty
14
21
  setup_variables
15
22
  end
16
23
 
24
+ attr_reader :weights
25
+
26
+ def weights=(val)
27
+ @weights = check_dimension(val, :weights)
28
+ end
29
+
17
30
  attr_reader :variables
18
31
 
19
32
  attr_reader :color_norm
@@ -65,7 +78,6 @@ module Charty
65
78
  return
66
79
  end
67
80
 
68
- # TODO: detect flat data
69
81
  flat = data.is_a?(Charty::Vector)
70
82
  if flat
71
83
  @plot_data = {}
@@ -73,10 +85,10 @@ module Charty
73
85
 
74
86
  [:x, :y].each do |var|
75
87
  case self.flat_structure[var]
76
- when :index
88
+ when :@index
77
89
  @plot_data[var] = data.index.to_a
78
90
  @variables[var] = data.index.name
79
- when :values
91
+ when :@values
80
92
  @plot_data[var] = data.to_a
81
93
  @variables[var] = data.name
82
94
  end
@@ -84,8 +96,32 @@ module Charty
84
96
 
85
97
  @plot_data = Charty::Table.new(@plot_data)
86
98
  else
87
- raise NotImplementedError,
88
- "wide-form input is not supported"
99
+ numeric_columns = @data.column_names.select do |cn|
100
+ @data[cn].numeric?
101
+ end
102
+ wide_data = @data[numeric_columns]
103
+
104
+ melt_params = {var_name: :@columns, value_name: :@values }
105
+ if self.wide_structure.include?(:index)
106
+ melt_params[:id_vars] = :@index
107
+ end
108
+
109
+ @plot_data = wide_data.melt(**melt_params)
110
+ @variables = {}
111
+ self.wide_structure.each do |var, attr|
112
+ @plot_data[var] = @plot_data[attr]
113
+
114
+ @variables[var] = case attr
115
+ when :@columns
116
+ wide_data.columns.name
117
+ when :@index
118
+ wide_data.index.name
119
+ else
120
+ nil
121
+ end
122
+ end
123
+
124
+ @plot_data = @plot_data[self.wide_structure.keys]
89
125
  end
90
126
  end
91
127
 
@@ -103,10 +139,11 @@ module Charty
103
139
  x: self.x,
104
140
  y: self.y,
105
141
  color: self.color,
142
+ weights: self.weights
106
143
  }.each do |key, val|
107
144
  next if val.nil?
108
145
 
109
- if data.column_names.include?(val)
146
+ if data.column?(val)
110
147
  plot_data[key] = data[val]
111
148
  variables[key] = val
112
149
  else
@@ -12,16 +12,6 @@ module Charty
12
12
  ([:x, :y] & self.variables.keys)[0]
13
13
  end
14
14
 
15
- attr_reader :weights
16
-
17
- def weights=(val)
18
- @weights = check_weights(val)
19
- end
20
-
21
- private def check_weights(val)
22
- raise NotImplementedError, "weights is not supported yet"
23
- end
24
-
25
15
  attr_reader :stat
26
16
 
27
17
  def stat=(val)
@@ -65,10 +55,44 @@ module Charty
65
55
  end
66
56
 
67
57
  # TODO: bin_width
68
- # TODO: bin_range
58
+
59
+ attr_reader :bin_range
60
+
61
+ def bin_range=(val)
62
+ @bin_range = check_bin_range(val)
63
+ end
64
+
65
+ private def check_bin_range(val)
66
+ case val
67
+ when nil, Range
68
+ return val
69
+ when Array
70
+ if val.length == 2
71
+ val.each_with_index do |v, i|
72
+ check_number(v, "bin_range[#{i}]")
73
+ end
74
+ return val
75
+ else
76
+ amount = val.length < 2 ? "few" : "many"
77
+ raise ArgumentError,
78
+ "Too #{amount} items in `bin_range` array (%p for 2)" % val.length
79
+ end
80
+ else
81
+ raise ArgumentError,
82
+ "Invalid value for `bin_range` " +
83
+ "(%p for a range or a pair of numbers)" % val
84
+ end
85
+ end
86
+
69
87
  # TODO: discrete
70
88
  # TODO: cumulative
71
- # TODO: common_bins
89
+
90
+ attr_reader :common_bins
91
+
92
+ def common_bins=(val)
93
+ @common_bins = check_boolean(val, :common_bins)
94
+ end
95
+
72
96
  # TODO: common_norm
73
97
 
74
98
  attr_reader :multiple
@@ -127,24 +151,52 @@ module Charty
127
151
  private def draw_univariate_histogram(backend)
128
152
  map_color(palette: palette, order: color_order, norm: color_norm)
129
153
 
154
+ key_color = self.key_color
155
+ if key_color.nil? && !self.variables.key?(:color)
156
+ palette = case self.palette
157
+ when Palette
158
+ self.palette
159
+ when nil
160
+ Palette.default
161
+ else
162
+ Palette[self.palette]
163
+ end
164
+ key_color = palette[0]
165
+ end
166
+
130
167
  # TODO: calculate histogram here and use bar plot to visualize
131
168
  data_variable = self.univariate_variable
132
169
 
133
- histograms = {}
134
- each_subset([:color], processed: true) do |sub_vars, sub_data|
135
- key = sub_vars.to_a
136
- observations = sub_data[data_variable].drop_na.to_a
137
- hist = Statistics.histogram(observations)
138
- histograms[key] = hist
139
- end
140
-
141
- bin_start, bin_end, bin_size = nil
142
- histograms.each do |_, hist|
143
- s, e = hist.edge.minmax
144
- z = (e - s).to_f / (hist.edge.length - 1)
145
- bin_start = [bin_start, s].compact.min
146
- bin_end = [bin_end, e].compact.max
147
- bin_size = [bin_size, z].compact.min
170
+ if common_bins
171
+ all_data = processed_data.drop_na
172
+ all_observations = all_data[data_variable].to_a
173
+
174
+ bins = self.bins
175
+ bins = 10 if self.variables.key?(:color) && bins == :auto
176
+
177
+ case bins
178
+ when Integer
179
+ case bin_range
180
+ when Range
181
+ start = bin_range.begin
182
+ stop = bin_range.end
183
+ when Array
184
+ start, stop = bin_range.minmax
185
+ end
186
+ data_range = all_observations.minmax
187
+ start ||= data_range[0]
188
+ stop ||= data_range[1]
189
+ if start == stop
190
+ start -= 0.5
191
+ stop += 0.5
192
+ end
193
+ common_bin_edges = Linspace.new(start .. stop, bins + 1).map(&:to_f)
194
+ else
195
+ params = {}
196
+ params[:weights] = all_data[:weights].to_a if all_data.column?(:weights)
197
+ h = Statistics.histogram(all_observations, bins, **params)
198
+ common_bin_edges = h.edges
199
+ end
148
200
  end
149
201
 
150
202
  if self.variables.key?(:color)
@@ -154,27 +206,37 @@ module Charty
154
206
  end
155
207
 
156
208
  each_subset([:color], processed: true) do |sub_vars, sub_data|
157
- name = sub_vars[:color]
158
209
  observations = sub_data[data_variable].drop_na.to_a
210
+ params = {}
211
+ params[:weights] = sub_data[:weights].to_a if sub_data.column?(:weights)
212
+ params[:edges] = common_bin_edges if common_bin_edges
213
+ hist = Statistics.histogram(observations, bins, **params)
159
214
 
160
- backend.univariate_histogram(observations, name, data_variable, stat,
161
- bin_start, bin_end, bin_size, alpha,
162
- name, @color_mapper)
215
+ name = sub_vars[:color]
216
+ backend.univariate_histogram(hist, name, data_variable, stat,
217
+ alpha, name, key_color, @color_mapper,
218
+ multiple, :bars, true, 1r)
163
219
  end
164
220
  end
165
221
 
166
222
  private def annotate_axes(backend)
223
+ backend.set_title(self.title) if self.title
224
+
167
225
  if univariate?
168
- xlabel = self.variables[:x]
169
- ylabel = self.variables[:y]
226
+ xlabel = self.x_label || self.variables[:x]
227
+ ylabel = self.y_label || self.variables[:y]
170
228
  case self.univariate_variable
171
229
  when :x
172
- ylabel = self.stat.to_s.capitalize
230
+ ylabel ||= self.stat.to_s.capitalize
173
231
  else
174
- xlabel = self.stat.to_s.capitalize
232
+ xlabel ||= self.stat.to_s.capitalize
175
233
  end
176
234
  backend.set_ylabel(ylabel) if ylabel
177
235
  backend.set_xlabel(xlabel) if xlabel
236
+
237
+ if self.variables.key?(:color)
238
+ backend.legend(loc: :best, title: self.variables[:color])
239
+ end
178
240
  end
179
241
  end
180
242
  end
@@ -122,7 +122,7 @@ module Charty
122
122
 
123
123
  include RandomSupport
124
124
 
125
- attr_reader :sort, :err_style, :err_kws, :error_bar, :x_scale, :y_scale
125
+ attr_reader :sort, :err_style, :err_kws, :error_bar
126
126
 
127
127
  def sort=(val)
128
128
  @sort = check_boolean(val, :sort)
@@ -211,17 +211,21 @@ module Charty
211
211
  [method, level]
212
212
  end
213
213
 
214
+ attr_reader :x_scale
215
+
214
216
  def x_scale=(val)
215
217
  @x_scale = check_axis_scale(val, :x)
216
218
  end
217
219
 
220
+ attr_reader :y_scale
221
+
218
222
  def y_scale=(val)
219
223
  @y_scale = check_axis_scale(val, :y)
220
224
  end
221
225
 
222
226
  private def check_axis_scale(val, axis)
223
227
  case val
224
- when :linear, "linear", :log10, "log10"
228
+ when :linear, "linear", :log, "log"
225
229
  val.to_sym
226
230
  else
227
231
  raise ArgumentError,
@@ -252,6 +256,15 @@ module Charty
252
256
  sub_data = sub_data.sort_values(sort_cols)
253
257
  end
254
258
 
259
+ # Perform axis scaling
260
+ if x_scale != :linear
261
+ sub_data[:x] = sub_data[:x].scale(x_scale)
262
+ end
263
+ if y_scale != :linear
264
+ sub_data[:y] = sub_data[:y].scale(x_scale)
265
+ end
266
+
267
+ # Perform estimation and error calculation
255
268
  unless estimator.nil?
256
269
  if self.variables.include?(:units)
257
270
  raise "`estimator` is must be nil when specifying `units`"
@@ -261,7 +274,22 @@ module Charty
261
274
  sub_data = grouped.apply(agg_var, &aggregator.method(:aggregate)).reset_index
262
275
  end
263
276
 
264
- # TODO: perform inverse conversion of axis scaling before plot
277
+ # Perform axis inverse scaling
278
+ if x_scale != :linear
279
+ sub_data.column_names.each do |cn|
280
+ if cn.start_with?("x")
281
+ sub_data[cn] = sub_data[cn].scale_inverse(x_scale)
282
+ end
283
+ end
284
+ end
285
+
286
+ if y_scale != :linear
287
+ sub_data.column_names.each do |cn|
288
+ if cn.start_with?("y")
289
+ sub_data[cn] = sub_data[cn].scale_inverse(x_scale)
290
+ end
291
+ end
292
+ end
265
293
 
266
294
  unit_grouping = if self.variables.include?(:units)
267
295
  sub_data.group_by(:units).each_group
@@ -290,10 +318,15 @@ module Charty
290
318
  end
291
319
 
292
320
  private def annotate_axes(backend)
293
- xlabel = self.variables[:x]
294
- ylabel = self.variables[:y]
321
+ backend.set_title(self.title) if self.title
322
+
323
+ xlabel = self.x_label || self.variables[:x]
324
+ ylabel = self.y_label || self.variables[:y]
295
325
  backend.set_xlabel(xlabel) unless xlabel.nil?
296
326
  backend.set_ylabel(ylabel) unless ylabel.nil?
327
+
328
+ backend.set_xscale(x_scale)
329
+ backend.set_yscale(y_scale)
297
330
  end
298
331
  end
299
332
  end