charty 0.2.7 → 0.2.11

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 (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