charty 0.2.4 → 0.2.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +64 -15
  3. data/charty.gemspec +10 -3
  4. data/lib/charty.rb +5 -2
  5. data/lib/charty/backends/bokeh.rb +2 -2
  6. data/lib/charty/backends/google_charts.rb +1 -1
  7. data/lib/charty/backends/gruff.rb +1 -1
  8. data/lib/charty/backends/plotly.rb +434 -32
  9. data/lib/charty/backends/plotly_helpers/html_renderer.rb +203 -0
  10. data/lib/charty/backends/plotly_helpers/notebook_renderer.rb +87 -0
  11. data/lib/charty/backends/plotly_helpers/plotly_renderer.rb +121 -0
  12. data/lib/charty/backends/pyplot.rb +187 -48
  13. data/lib/charty/backends/rubyplot.rb +1 -1
  14. data/lib/charty/cache_dir.rb +27 -0
  15. data/lib/charty/dash_pattern_generator.rb +57 -0
  16. data/lib/charty/index.rb +1 -1
  17. data/lib/charty/iruby_helper.rb +18 -0
  18. data/lib/charty/plot_methods.rb +115 -3
  19. data/lib/charty/plotter.rb +2 -2
  20. data/lib/charty/plotters.rb +4 -0
  21. data/lib/charty/plotters/abstract_plotter.rb +106 -11
  22. data/lib/charty/plotters/bar_plotter.rb +1 -16
  23. data/lib/charty/plotters/box_plotter.rb +1 -16
  24. data/lib/charty/plotters/distribution_plotter.rb +150 -0
  25. data/lib/charty/plotters/histogram_plotter.rb +242 -0
  26. data/lib/charty/plotters/line_plotter.rb +300 -0
  27. data/lib/charty/plotters/relational_plotter.rb +213 -96
  28. data/lib/charty/plotters/scatter_plotter.rb +8 -43
  29. data/lib/charty/statistics.rb +11 -2
  30. data/lib/charty/table.rb +124 -14
  31. data/lib/charty/table_adapters/base_adapter.rb +97 -0
  32. data/lib/charty/table_adapters/daru_adapter.rb +2 -0
  33. data/lib/charty/table_adapters/datasets_adapter.rb +7 -0
  34. data/lib/charty/table_adapters/hash_adapter.rb +19 -3
  35. data/lib/charty/table_adapters/pandas_adapter.rb +82 -0
  36. data/lib/charty/util.rb +28 -0
  37. data/lib/charty/vector_adapters.rb +5 -1
  38. data/lib/charty/vector_adapters/array_adapter.rb +2 -10
  39. data/lib/charty/vector_adapters/daru_adapter.rb +3 -11
  40. data/lib/charty/vector_adapters/narray_adapter.rb +1 -6
  41. data/lib/charty/vector_adapters/numpy_adapter.rb +1 -1
  42. data/lib/charty/vector_adapters/pandas_adapter.rb +0 -1
  43. data/lib/charty/version.rb +1 -1
  44. metadata +104 -11
  45. data/lib/charty/missing_value_support.rb +0 -14
@@ -42,25 +42,10 @@ module Charty
42
42
  @cap_size = check_number(cap_size, :cap_size, allow_nil: true)
43
43
  end
44
44
 
45
- def render
46
- backend = Backends.current
47
- backend.begin_figure
45
+ private def render_plot(backend, **)
48
46
  draw_bars(backend)
49
47
  annotate_axes(backend)
50
48
  backend.invert_yaxis if orient == :h
51
- backend.show
52
- end
53
-
54
- # TODO:
55
- # - Should infer mime type from file's extname
56
- # - Should check backend's supported mime type before begin_figure
57
- def save(filename, **opts)
58
- backend = Backends.current
59
- backend.begin_figure
60
- draw_bars(backend)
61
- annotate_axes(backend)
62
- backend.invert_yaxis if orient == :h
63
- backend.save(filename, **opts)
64
49
  end
65
50
 
66
51
  private def draw_bars(backend)
@@ -27,25 +27,10 @@ module Charty
27
27
  @whisker = check_number(val, :whisker, allow_nil: true)
28
28
  end
29
29
 
30
- def render
31
- backend = Backends.current
32
- backend.begin_figure
30
+ private def render_plot(backend, **)
33
31
  draw_box_plot(backend)
34
32
  annotate_axes(backend)
35
33
  backend.invert_yaxis if orient == :h
36
- backend.show
37
- end
38
-
39
- # TODO:
40
- # - Should infer mime type from file's extname
41
- # - Should check backend's supported mime type before begin_figure
42
- def save(filename, **opts)
43
- backend = Backends.current
44
- backend.begin_figure
45
- draw_box_plot(backend)
46
- annotate_axes(backend)
47
- backend.invert_yaxis if orient == :h
48
- backend.save(filename, **opts)
49
34
  end
50
35
 
51
36
  private def draw_box_plot(backend)
@@ -0,0 +1,150 @@
1
+ module Charty
2
+ module Plotters
3
+ class DistributionPlotter < AbstractPlotter
4
+ def flat_structure
5
+ {
6
+ x: :values
7
+ }
8
+ end
9
+
10
+ def initialize(data:, variables:, **options, &block)
11
+ x, y, color = variables.values_at(:x, :y, :color)
12
+ super(x, y, color, data: data, **options, &block)
13
+
14
+ setup_variables
15
+ end
16
+
17
+ attr_reader :weights
18
+
19
+ def weights=(val)
20
+ @weights = check_dimension(val, :weights)
21
+ end
22
+
23
+ attr_reader :variables
24
+
25
+ attr_reader :color_norm
26
+
27
+ def color_norm=(val)
28
+ unless val.nil?
29
+ raise NotImplementedError,
30
+ "Specifying color_norm is not supported yet"
31
+ end
32
+ end
33
+
34
+ attr_reader :legend
35
+
36
+ def legend=(val)
37
+ @legend = check_legend(val)
38
+ end
39
+
40
+ private def check_legend(val)
41
+ check_boolean(val, :legend)
42
+ end
43
+
44
+ attr_reader :input_format, :plot_data, :variables, :var_types
45
+
46
+ # This should be the same as one in RelationalPlotter
47
+ # TODO: move this to AbstractPlotter and refactor with CategoricalPlotter
48
+ private def setup_variables
49
+ if x.nil? && y.nil?
50
+ @input_format = :wide
51
+ setup_variables_with_wide_form_dataset
52
+ else
53
+ @input_format = :long
54
+ setup_variables_with_long_form_dataset
55
+ end
56
+
57
+ @var_types = @plot_data.columns.map { |k|
58
+ [k, variable_type(@plot_data[k], :categorical)]
59
+ }.to_h
60
+ end
61
+
62
+ private def setup_variables_with_wide_form_dataset
63
+ unless color.nil?
64
+ raise ArgumentError,
65
+ "Unable to assign the following variables in wide-form data: color"
66
+ end
67
+
68
+ if data.nil? || data.empty?
69
+ @plot_data = Charty::Table.new({})
70
+ @variables = {}
71
+ return
72
+ end
73
+
74
+ # TODO: detect flat data
75
+ flat = data.is_a?(Charty::Vector)
76
+ if flat
77
+ @plot_data = {}
78
+ @variables = {}
79
+
80
+ [:x, :y].each do |var|
81
+ case self.flat_structure[var]
82
+ when :index
83
+ @plot_data[var] = data.index.to_a
84
+ @variables[var] = data.index.name
85
+ when :values
86
+ @plot_data[var] = data.to_a
87
+ @variables[var] = data.name
88
+ end
89
+ end
90
+
91
+ @plot_data = Charty::Table.new(@plot_data)
92
+ else
93
+ raise NotImplementedError,
94
+ "wide-form input is not supported"
95
+ end
96
+ end
97
+
98
+ private def setup_variables_with_long_form_dataset
99
+ if data.nil? || data.empty?
100
+ @plot_data = Charty::Table.new({})
101
+ @variables = {}
102
+ return
103
+ end
104
+
105
+ plot_data = {}
106
+ variables = {}
107
+
108
+ {
109
+ x: self.x,
110
+ y: self.y,
111
+ color: self.color,
112
+ weights: self.weights
113
+ }.each do |key, val|
114
+ next if val.nil?
115
+
116
+ if data.column?(val)
117
+ plot_data[key] = data[val]
118
+ variables[key] = val
119
+ else
120
+ case val
121
+ when Charty::Vector
122
+ plot_data[key] = val
123
+ variables[key] = val.name
124
+ else
125
+ raise ArgumentError,
126
+ "Could not interpret value %p for parameter %p" % [val, key]
127
+ end
128
+ end
129
+ end
130
+
131
+ @plot_data = Charty::Table.new(plot_data)
132
+ @variables = variables.select do |var, name|
133
+ @plot_data[var].notnull.any?
134
+ end
135
+ end
136
+
137
+ private def map_color(palette: nil, order: nil, norm: nil)
138
+ @color_mapper = ColorMapper.new(self, palette, order, norm)
139
+ end
140
+
141
+ private def map_size(sizes: nil, order: nil, norm: nil)
142
+ @size_mapper = SizeMapper.new(self, sizes, order, norm)
143
+ end
144
+
145
+ private def map_style(markers: nil, dashes: nil, order: nil)
146
+ @style_mapper = StyleMapper.new(self, markers, dashes, order)
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,242 @@
1
+ module Charty
2
+ module Plotters
3
+ class HistogramPlotter < DistributionPlotter
4
+ def univariate?
5
+ self.variables.key?(:x) != self.variables.key?(:y)
6
+ end
7
+
8
+ def univariate_variable
9
+ unless univariate?
10
+ raise TypeError, "This is not a univariate plot"
11
+ end
12
+ ([:x, :y] & self.variables.keys)[0]
13
+ end
14
+
15
+ attr_reader :stat
16
+
17
+ def stat=(val)
18
+ @stat = check_stat(val)
19
+ end
20
+
21
+ private def check_stat(val)
22
+ case val
23
+ when :count, "count"
24
+ val.to_sym
25
+ when :frequency, "frequency",
26
+ :density, "density",
27
+ :probability, "probability"
28
+ raise ArgumentError,
29
+ "%p for `stat` is not supported yet" % val,
30
+ caller
31
+ else
32
+ raise ArgumentError,
33
+ "Invalid value for `stat` (%p)" % val,
34
+ caller
35
+ end
36
+ end
37
+
38
+ attr_reader :bins
39
+
40
+ def bins=(val)
41
+ @bins = check_bins(val)
42
+ end
43
+
44
+ private def check_bins(val)
45
+ case val
46
+ when :auto, "auto"
47
+ val.to_sym
48
+ when Integer
49
+ val
50
+ else
51
+ raise ArgumentError,
52
+ "Invalid value for `bins` (%p)" % val,
53
+ caller
54
+ end
55
+ end
56
+
57
+ # TODO: bin_width
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
+
87
+ # TODO: discrete
88
+ # TODO: cumulative
89
+
90
+ attr_reader :common_bins
91
+
92
+ def common_bins=(val)
93
+ @common_bins = check_boolean(val, :common_bins)
94
+ end
95
+
96
+ # TODO: common_norm
97
+
98
+ attr_reader :multiple
99
+
100
+ def multiple=(val)
101
+ @multiple = check_multiple(val)
102
+ end
103
+
104
+ private def check_multiple(val)
105
+ case val
106
+ when :layer, "layer"
107
+ val.to_sym
108
+ when :dodge, "dodge",
109
+ :stack, "stack",
110
+ :fill, "fill"
111
+ val = val.to_sym
112
+ raise NotImplementedError,
113
+ "%p for `multiple` is not supported yet" % val,
114
+ caller
115
+ else
116
+ raise ArgumentError,
117
+ "Invalid value for `multiple` (%p)" % val,
118
+ caller
119
+ end
120
+ end
121
+
122
+ # TODO: element
123
+ # TODO: fill
124
+ # TODO: shrink
125
+
126
+ attr_reader :kde
127
+
128
+ def kde=(val)
129
+ raise NotImplementedError, "kde is not supported yet"
130
+ end
131
+
132
+ attr_reader :kde_params
133
+
134
+ def kde_params=(val)
135
+ raise NotImplementedError, "kde_params is not supported yet"
136
+ end
137
+
138
+ # TODO: thresh
139
+ # TODO: pthresh
140
+ # TODO: pmax
141
+ # TODO: cbar
142
+ # TODO: cbar_params
143
+ # TODO: x_log_scale
144
+ # TODO: y_log_scale
145
+
146
+ private def render_plot(backend, **)
147
+ draw_univariate_histogram(backend)
148
+ annotate_axes(backend)
149
+ end
150
+
151
+ private def draw_univariate_histogram(backend)
152
+ map_color(palette: palette, order: color_order, norm: color_norm)
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
+
167
+ # TODO: calculate histogram here and use bar plot to visualize
168
+ data_variable = self.univariate_variable
169
+
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
200
+ end
201
+
202
+ if self.variables.key?(:color)
203
+ alpha = 0.5
204
+ else
205
+ alpha = 0.75
206
+ end
207
+
208
+ each_subset([:color], processed: true) do |sub_vars, sub_data|
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)
214
+
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)
219
+ end
220
+ end
221
+
222
+ private def annotate_axes(backend)
223
+ if univariate?
224
+ xlabel = self.variables[:x]
225
+ ylabel = self.variables[:y]
226
+ case self.univariate_variable
227
+ when :x
228
+ ylabel = self.stat.to_s.capitalize
229
+ else
230
+ xlabel = self.stat.to_s.capitalize
231
+ end
232
+ backend.set_ylabel(ylabel) if ylabel
233
+ backend.set_xlabel(xlabel) if xlabel
234
+
235
+ if self.variables.key?(:color)
236
+ backend.legend(loc: :best, title: self.variables[:color])
237
+ end
238
+ end
239
+ end
240
+ end
241
+ end
242
+ end
@@ -0,0 +1,300 @@
1
+ module Charty
2
+ module Plotters
3
+ class EstimateAggregator
4
+ def initialize(estimator, error_bar, n_boot, random)
5
+ @estimator = estimator
6
+ @method, @level = error_bar
7
+ @n_boot = n_boot
8
+ @random = random
9
+ end
10
+
11
+ # Perform aggregation
12
+ #
13
+ # @param data [Hash<Any, Charty::Table>]
14
+ # @param var_name [Symbol, String] A column name to be aggregated
15
+ def aggregate(data, var_name)
16
+ values = data[var_name]
17
+ estimation = case @estimator
18
+ when :count
19
+ values.length
20
+ when :mean
21
+ values.mean
22
+ end
23
+
24
+ n = values.length
25
+ case
26
+ # No error bars
27
+ when @method.nil?
28
+ err_min = err_max = Float::NAN
29
+ when n <= 1
30
+ err_min = err_max = Float::NAN
31
+
32
+ # User-defined method
33
+ when @method.respond_to?(:call)
34
+ err_min, err_max = @method.call(values)
35
+
36
+ # Parametric
37
+ when @method == :sd
38
+ err_radius = values.stdev * @level
39
+ err_min = estimation - err_radius
40
+ err_max = estimation + err_radius
41
+ when @method == :se
42
+ err_radius = values.stdev / Math.sqrt(n)
43
+ err_min = estimation - err_radius
44
+ err_max = estimation + err_radius
45
+
46
+ # Nonparametric
47
+ when @method == :pi
48
+ err_min, err_max = percentile_interval(values, @level)
49
+ when @method == :ci
50
+ # TODO: Support units
51
+ err_min, err_max =
52
+ Statistics.bootstrap_ci(values, @level, units: nil, func: @estimator,
53
+ n_boot: @n_boot, random: @random)
54
+ end
55
+
56
+ {
57
+ var_name => estimation,
58
+ "#{var_name}_min" => err_min,
59
+ "#{var_name}_max" => err_max
60
+ }
61
+ end
62
+
63
+ def percentile_interval(values, width)
64
+ q = [50 - width / 2, 50 + width / 2]
65
+ Statistics.percentile(values, q)
66
+ end
67
+ end
68
+
69
+ class LinePlotter < RelationalPlotter
70
+ def initialize(data: nil, variables: {}, **options, &block)
71
+ x, y, color, style, size = variables.values_at(:x, :y, :color, :style, :size)
72
+ super(x, y, color, style, size, data: data, **options, &block)
73
+
74
+ @comp_data = nil
75
+ end
76
+
77
+ attr_reader :estimator
78
+
79
+ def estimator=(estimator)
80
+ @estimator = check_estimator(estimator)
81
+ end
82
+
83
+ private def check_estimator(value)
84
+ case value
85
+ when nil, false
86
+ nil
87
+ when :count, "count"
88
+ :count
89
+ when :mean, "mean"
90
+ :mean
91
+ when :median
92
+ raise NotImplementedError,
93
+ "median estimator has not been supported yet"
94
+ when Proc
95
+ raise NotImplementedError,
96
+ "a callable estimator has not been supported yet"
97
+ else
98
+ raise ArgumentError,
99
+ "invalid value for estimator (%p for :mean)" % value
100
+ end
101
+ end
102
+
103
+ attr_reader :n_boot
104
+
105
+ def n_boot=(n_boot)
106
+ @n_boot = check_n_boot(n_boot)
107
+ end
108
+
109
+ private def check_n_boot(value)
110
+ case value
111
+ when Integer
112
+ if value <= 0
113
+ raise ArgumentError,
114
+ "n_boot must be larger than zero, but %p is given" % value
115
+ end
116
+ value
117
+ else
118
+ raise ArgumentError,
119
+ "invalid value for n_boot (%p for an integer > 0)" % value
120
+ end
121
+ end
122
+
123
+ include RandomSupport
124
+
125
+ attr_reader :sort, :err_style, :err_kws, :error_bar, :x_scale, :y_scale
126
+
127
+ def sort=(val)
128
+ @sort = check_boolean(val, :sort)
129
+ end
130
+
131
+ def err_style=(val)
132
+ @err_style = check_err_style(val)
133
+ end
134
+
135
+ private def check_err_style(val)
136
+ case val
137
+ when :bars, "bars", :band, "band"
138
+ val.to_sym
139
+ else
140
+ raise ArgumentError,
141
+ "Invalid value for err_style (%p for :band or :bars)" % val
142
+ end
143
+ end
144
+
145
+ # parameters to draw error bars/bands
146
+ def err_params=(val)
147
+ unless val.nil?
148
+ raise NotImplementedError,
149
+ "Specifying `err_params` is not supported"
150
+ end
151
+ end
152
+
153
+ # The method and level to calculate error bars/bands
154
+ def error_bar=(val)
155
+ @error_bar = check_error_bar(val)
156
+ end
157
+
158
+ DEFAULT_ERROR_BAR_LEVELS = {
159
+ ci: 95,
160
+ pi: 95,
161
+ se: 1,
162
+ sd: 1
163
+ }.freeze
164
+
165
+ VALID_ERROR_BAR_METHODS = DEFAULT_ERROR_BAR_LEVELS.keys
166
+ VALID_ERROR_BAR_METHODS.concat(VALID_ERROR_BAR_METHODS.map(&:to_s))
167
+ VALID_ERROR_BAR_METHODS.freeze
168
+
169
+ private def check_error_bar(val)
170
+ case val
171
+ when nil
172
+ return [nil, nil]
173
+ when ->(x) { x.respond_to?(:call) }
174
+ return [val, nil]
175
+ when *VALID_ERROR_BAR_METHODS
176
+ method = val.to_sym
177
+ level = nil
178
+ when Array
179
+ if val.length != 2
180
+ raise ArgumentError,
181
+ "The `error_bar` array has the wrong number of items " +
182
+ "(%d for 2)" % val.length
183
+ end
184
+ method, level = *val
185
+ else
186
+ raise ArgumentError,
187
+ "Unable to recognize the value for `error_bar`: %p" % val
188
+ end
189
+
190
+ case method
191
+ when *VALID_ERROR_BAR_METHODS
192
+ method = method.to_sym
193
+ else
194
+ error_message = "The value for method in `error_bar` array must be in %p, but %p was passed" % [
195
+ DEFAULT_ERROR_BAR_LEVELS.keys,
196
+ method
197
+ ]
198
+ raise ArgumentError, error_message
199
+ end
200
+
201
+ case level
202
+ when Numeric
203
+ # nothing to do
204
+ when nil
205
+ level = DEFAULT_ERROR_BAR_LEVELS[method]
206
+ else
207
+ raise ArgumentError,
208
+ "The value of level in `error_bar` array must be a number "
209
+ end
210
+
211
+ [method, level]
212
+ end
213
+
214
+ def x_scale=(val)
215
+ @x_scale = check_axis_scale(val, :x)
216
+ end
217
+
218
+ def y_scale=(val)
219
+ @y_scale = check_axis_scale(val, :y)
220
+ end
221
+
222
+ private def check_axis_scale(val, axis)
223
+ case val
224
+ when :linear, "linear", :log10, "log10"
225
+ val.to_sym
226
+ else
227
+ raise ArgumentError,
228
+ "The value of `#{axis}_scale` is worng: %p" % val,
229
+ caller
230
+ end
231
+ end
232
+
233
+ private def render_plot(backend, **)
234
+ draw_lines(backend)
235
+ annotate_axes(backend)
236
+ end
237
+
238
+ private def draw_lines(backend)
239
+ map_color(palette: palette, order: color_order, norm: color_norm)
240
+ map_size(sizes: sizes, order: size_order, norm: size_norm)
241
+ map_style(markers: markers, dashes: dashes, order: style_order)
242
+
243
+ aggregator = EstimateAggregator.new(estimator, error_bar, n_boot, random)
244
+
245
+ agg_var = :y
246
+ grouper = :x
247
+ grouping_vars = [:color, :size, :style]
248
+
249
+ each_subset(grouping_vars, processed: true) do |sub_vars, sub_data|
250
+ if self.sort
251
+ sort_cols = [:units, :x, :y] & self.variables.keys
252
+ sub_data = sub_data.sort_values(sort_cols)
253
+ end
254
+
255
+ unless estimator.nil?
256
+ if self.variables.include?(:units)
257
+ raise "`estimator` is must be nil when specifying `units`"
258
+ end
259
+
260
+ grouped = sub_data.group_by(grouper, sort: self.sort)
261
+ sub_data = grouped.apply(agg_var, &aggregator.method(:aggregate)).reset_index
262
+ end
263
+
264
+ # TODO: perform inverse conversion of axis scaling before plot
265
+
266
+ unit_grouping = if self.variables.include?(:units)
267
+ sub_data.group_by(:units).each_group
268
+ else
269
+ { nil => sub_data }
270
+ end
271
+ unit_grouping.each do |_unit_value, unit_data|
272
+ ci_params = unless self.estimator.nil? || self.error_bar.nil?
273
+ {
274
+ style: self.err_style,
275
+ y_min: sub_data[:y_min],
276
+ y_max: sub_data[:y_max]
277
+ }
278
+ end
279
+ backend.line(unit_data[:x], unit_data[:y], self.variables,
280
+ color: sub_vars[:color], color_mapper: @color_mapper,
281
+ size: sub_vars[:size], size_mapper: @size_mapper,
282
+ style: sub_vars[:style], style_mapper: @style_mapper,
283
+ ci_params: ci_params)
284
+ end
285
+ end
286
+
287
+ if legend
288
+ backend.add_line_plot_legend(@variables, @color_mapper, @size_mapper, @style_mapper, legend)
289
+ end
290
+ end
291
+
292
+ private def annotate_axes(backend)
293
+ xlabel = self.variables[:x]
294
+ ylabel = self.variables[:y]
295
+ backend.set_xlabel(xlabel) unless xlabel.nil?
296
+ backend.set_ylabel(ylabel) unless ylabel.nil?
297
+ end
298
+ end
299
+ end
300
+ end