charty 0.2.4 → 0.2.9

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