charty 0.2.1 → 0.2.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +56 -23
  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 -5
  10. data/examples/palette.rb +1 -1
  11. data/examples/sample_images/hist_gruff.png +0 -0
  12. data/images/penguins_body_mass_g_flipper_length_mm_scatter_plot.png +0 -0
  13. data/images/penguins_body_mass_g_flipper_length_mm_species_scatter_plot.png +0 -0
  14. data/images/penguins_body_mass_g_flipper_length_mm_species_sex_scatter_plot.png +0 -0
  15. data/images/penguins_species_body_mass_g_bar_plot_h.png +0 -0
  16. data/images/penguins_species_body_mass_g_bar_plot_v.png +0 -0
  17. data/images/penguins_species_body_mass_g_box_plot_h.png +0 -0
  18. data/images/penguins_species_body_mass_g_box_plot_v.png +0 -0
  19. data/images/penguins_species_body_mass_g_sex_bar_plot_v.png +0 -0
  20. data/images/penguins_species_body_mass_g_sex_box_plot_v.png +0 -0
  21. data/lib/charty.rb +9 -2
  22. data/lib/charty/backends.rb +1 -0
  23. data/lib/charty/backends/bokeh.rb +2 -2
  24. data/lib/charty/backends/google_charts.rb +1 -1
  25. data/lib/charty/backends/gruff.rb +14 -3
  26. data/lib/charty/backends/plotly.rb +731 -32
  27. data/lib/charty/backends/plotly_helpers/html_renderer.rb +203 -0
  28. data/lib/charty/backends/plotly_helpers/notebook_renderer.rb +86 -0
  29. data/lib/charty/backends/plotly_helpers/plotly_renderer.rb +121 -0
  30. data/lib/charty/backends/pyplot.rb +515 -67
  31. data/lib/charty/backends/rubyplot.rb +1 -1
  32. data/lib/charty/backends/unicode_plot.rb +79 -0
  33. data/lib/charty/cache_dir.rb +27 -0
  34. data/lib/charty/dash_pattern_generator.rb +57 -0
  35. data/lib/charty/index.rb +213 -0
  36. data/lib/charty/iruby_helper.rb +18 -0
  37. data/lib/charty/linspace.rb +1 -1
  38. data/lib/charty/plot_methods.rb +283 -8
  39. data/lib/charty/plotter.rb +2 -2
  40. data/lib/charty/plotters.rb +11 -0
  41. data/lib/charty/plotters/abstract_plotter.rb +188 -18
  42. data/lib/charty/plotters/bar_plotter.rb +189 -7
  43. data/lib/charty/plotters/box_plotter.rb +64 -11
  44. data/lib/charty/plotters/categorical_plotter.rb +272 -40
  45. data/lib/charty/plotters/count_plotter.rb +7 -0
  46. data/lib/charty/plotters/distribution_plotter.rb +143 -0
  47. data/lib/charty/plotters/estimation_support.rb +84 -0
  48. data/lib/charty/plotters/histogram_plotter.rb +182 -0
  49. data/lib/charty/plotters/line_plotter.rb +300 -0
  50. data/lib/charty/plotters/random_support.rb +25 -0
  51. data/lib/charty/plotters/relational_plotter.rb +635 -0
  52. data/lib/charty/plotters/scatter_plotter.rb +80 -0
  53. data/lib/charty/plotters/vector_plotter.rb +6 -0
  54. data/lib/charty/statistics.rb +96 -2
  55. data/lib/charty/table.rb +160 -15
  56. data/lib/charty/table_adapters.rb +2 -0
  57. data/lib/charty/table_adapters/active_record_adapter.rb +17 -9
  58. data/lib/charty/table_adapters/base_adapter.rb +166 -0
  59. data/lib/charty/table_adapters/daru_adapter.rb +39 -3
  60. data/lib/charty/table_adapters/datasets_adapter.rb +13 -2
  61. data/lib/charty/table_adapters/hash_adapter.rb +141 -16
  62. data/lib/charty/table_adapters/narray_adapter.rb +25 -6
  63. data/lib/charty/table_adapters/nmatrix_adapter.rb +15 -5
  64. data/lib/charty/table_adapters/pandas_adapter.rb +163 -0
  65. data/lib/charty/util.rb +28 -0
  66. data/lib/charty/vector.rb +69 -0
  67. data/lib/charty/vector_adapters.rb +187 -0
  68. data/lib/charty/vector_adapters/array_adapter.rb +101 -0
  69. data/lib/charty/vector_adapters/daru_adapter.rb +163 -0
  70. data/lib/charty/vector_adapters/narray_adapter.rb +182 -0
  71. data/lib/charty/vector_adapters/nmatrix_adapter.rb +37 -0
  72. data/lib/charty/vector_adapters/numpy_adapter.rb +168 -0
  73. data/lib/charty/vector_adapters/pandas_adapter.rb +199 -0
  74. data/lib/charty/version.rb +1 -1
  75. metadata +105 -24
  76. data/lib/charty/palette.rb +0 -235
@@ -0,0 +1,7 @@
1
+ module Charty
2
+ module Plotters
3
+ class CountPlotter < BarPlotter
4
+ self.require_numeric = false
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,143 @@
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 :variables
18
+
19
+ attr_reader :color_norm
20
+
21
+ def color_norm=(val)
22
+ unless val.nil?
23
+ raise NotImplementedError,
24
+ "Specifying color_norm is not supported yet"
25
+ end
26
+ end
27
+
28
+ attr_reader :legend
29
+
30
+ def legend=(val)
31
+ @legend = check_legend(val)
32
+ end
33
+
34
+ private def check_legend(val)
35
+ check_boolean(val, :legend)
36
+ end
37
+
38
+ attr_reader :input_format, :plot_data, :variables, :var_types
39
+
40
+ # This should be the same as one in RelationalPlotter
41
+ # TODO: move this to AbstractPlotter and refactor with CategoricalPlotter
42
+ private def setup_variables
43
+ if x.nil? && y.nil?
44
+ @input_format = :wide
45
+ setup_variables_with_wide_form_dataset
46
+ else
47
+ @input_format = :long
48
+ setup_variables_with_long_form_dataset
49
+ end
50
+
51
+ @var_types = @plot_data.columns.map { |k|
52
+ [k, variable_type(@plot_data[k], :categorical)]
53
+ }.to_h
54
+ end
55
+
56
+ private def setup_variables_with_wide_form_dataset
57
+ unless color.nil?
58
+ raise ArgumentError,
59
+ "Unable to assign the following variables in wide-form data: color"
60
+ end
61
+
62
+ if data.nil? || data.empty?
63
+ @plot_data = Charty::Table.new({})
64
+ @variables = {}
65
+ return
66
+ end
67
+
68
+ # TODO: detect flat data
69
+ flat = data.is_a?(Charty::Vector)
70
+ if flat
71
+ @plot_data = {}
72
+ @variables = {}
73
+
74
+ [:x, :y].each do |var|
75
+ case self.flat_structure[var]
76
+ when :index
77
+ @plot_data[var] = data.index.to_a
78
+ @variables[var] = data.index.name
79
+ when :values
80
+ @plot_data[var] = data.to_a
81
+ @variables[var] = data.name
82
+ end
83
+ end
84
+
85
+ @plot_data = Charty::Table.new(@plot_data)
86
+ else
87
+ raise NotImplementedError,
88
+ "wide-form input is not supported"
89
+ end
90
+ end
91
+
92
+ private def setup_variables_with_long_form_dataset
93
+ if data.nil? || data.empty?
94
+ @plot_data = Charty::Table.new({})
95
+ @variables = {}
96
+ return
97
+ end
98
+
99
+ plot_data = {}
100
+ variables = {}
101
+
102
+ {
103
+ x: self.x,
104
+ y: self.y,
105
+ color: self.color,
106
+ }.each do |key, val|
107
+ next if val.nil?
108
+
109
+ if data.column_names.include?(val)
110
+ plot_data[key] = data[val]
111
+ variables[key] = val
112
+ else
113
+ case val
114
+ when Charty::Vector
115
+ plot_data[key] = val
116
+ variables[key] = val.name
117
+ else
118
+ raise ArgumentError,
119
+ "Could not interpret value %p for parameter %p" % [val, key]
120
+ end
121
+ end
122
+ end
123
+
124
+ @plot_data = Charty::Table.new(plot_data)
125
+ @variables = variables.select do |var, name|
126
+ @plot_data[var].notnull.any?
127
+ end
128
+ end
129
+
130
+ private def map_color(palette: nil, order: nil, norm: nil)
131
+ @color_mapper = ColorMapper.new(self, palette, order, norm)
132
+ end
133
+
134
+ private def map_size(sizes: nil, order: nil, norm: nil)
135
+ @size_mapper = SizeMapper.new(self, sizes, order, norm)
136
+ end
137
+
138
+ private def map_style(markers: nil, dashes: nil, order: nil)
139
+ @style_mapper = StyleMapper.new(self, markers, dashes, order)
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,84 @@
1
+ module Charty
2
+ module Plotters
3
+ module EstimationSupport
4
+ attr_reader :estimator
5
+
6
+ def estimator=(estimator)
7
+ @estimator = check_estimator(estimator)
8
+ end
9
+
10
+ module_function def check_estimator(value)
11
+ case value
12
+ when :count, "count"
13
+ :count
14
+ when :mean, "mean"
15
+ :mean
16
+ when :median
17
+ raise NotImplementedError,
18
+ "median estimator has not been supported yet"
19
+ when Proc
20
+ raise NotImplementedError,
21
+ "a callable estimator has not been supported yet"
22
+ else
23
+ raise ArgumentError,
24
+ "invalid value for estimator (%p for :mean)" % value
25
+ end
26
+ end
27
+
28
+ attr_reader :ci
29
+
30
+ def ci=(ci)
31
+ @ci = check_ci(ci)
32
+ end
33
+
34
+ private def check_ci(value)
35
+ case value
36
+ when nil
37
+ nil
38
+ when :sd, "sd"
39
+ :sd
40
+ when 0..100
41
+ value
42
+ when Numeric
43
+ raise ArgumentError,
44
+ "ci must be in 0..100, but %p is given" % value
45
+ else
46
+ raise ArgumentError,
47
+ "invalid value for ci (%p for nil, :sd, or a number in 0..100)" % value
48
+ end
49
+ end
50
+
51
+ attr_reader :n_boot
52
+
53
+ def n_boot=(n_boot)
54
+ @n_boot = check_n_boot(n_boot)
55
+ end
56
+
57
+ private def check_n_boot(value)
58
+ case value
59
+ when Integer
60
+ if value <= 0
61
+ raise ArgumentError,
62
+ "n_boot must be larger than zero, but %p is given" % value
63
+ end
64
+ value
65
+ else
66
+ raise ArgumentError,
67
+ "invalid value for n_boot (%p for an integer > 0)" % value
68
+ end
69
+ end
70
+
71
+ attr_reader :units
72
+
73
+ def units=(units)
74
+ @units = check_dimension(units, :units)
75
+ unless units.nil?
76
+ raise NotImplementedError,
77
+ "Specifying units variable is not supported yet"
78
+ end
79
+ end
80
+
81
+ include RandomSupport
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,182 @@
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 :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
+ attr_reader :stat
26
+
27
+ def stat=(val)
28
+ @stat = check_stat(val)
29
+ end
30
+
31
+ private def check_stat(val)
32
+ case val
33
+ when :count, "count"
34
+ val.to_sym
35
+ when :frequency, "frequency",
36
+ :density, "density",
37
+ :probability, "probability"
38
+ raise ArgumentError,
39
+ "%p for `stat` is not supported yet" % val,
40
+ caller
41
+ else
42
+ raise ArgumentError,
43
+ "Invalid value for `stat` (%p)" % val,
44
+ caller
45
+ end
46
+ end
47
+
48
+ attr_reader :bins
49
+
50
+ def bins=(val)
51
+ @bins = check_bins(val)
52
+ end
53
+
54
+ private def check_bins(val)
55
+ case val
56
+ when :auto, "auto"
57
+ val.to_sym
58
+ when Integer
59
+ val
60
+ else
61
+ raise ArgumentError,
62
+ "Invalid value for `bins` (%p)" % val,
63
+ caller
64
+ end
65
+ end
66
+
67
+ # TODO: bin_width
68
+ # TODO: bin_range
69
+ # TODO: discrete
70
+ # TODO: cumulative
71
+ # TODO: common_bins
72
+ # TODO: common_norm
73
+
74
+ attr_reader :multiple
75
+
76
+ def multiple=(val)
77
+ @multiple = check_multiple(val)
78
+ end
79
+
80
+ private def check_multiple(val)
81
+ case val
82
+ when :layer, "layer"
83
+ val.to_sym
84
+ when :dodge, "dodge",
85
+ :stack, "stack",
86
+ :fill, "fill"
87
+ val = val.to_sym
88
+ raise NotImplementedError,
89
+ "%p for `multiple` is not supported yet" % val,
90
+ caller
91
+ else
92
+ raise ArgumentError,
93
+ "Invalid value for `multiple` (%p)" % val,
94
+ caller
95
+ end
96
+ end
97
+
98
+ # TODO: element
99
+ # TODO: fill
100
+ # TODO: shrink
101
+
102
+ attr_reader :kde
103
+
104
+ def kde=(val)
105
+ raise NotImplementedError, "kde is not supported yet"
106
+ end
107
+
108
+ attr_reader :kde_params
109
+
110
+ def kde_params=(val)
111
+ raise NotImplementedError, "kde_params is not supported yet"
112
+ end
113
+
114
+ # TODO: thresh
115
+ # TODO: pthresh
116
+ # TODO: pmax
117
+ # TODO: cbar
118
+ # TODO: cbar_params
119
+ # TODO: x_log_scale
120
+ # TODO: y_log_scale
121
+
122
+ private def render_plot(backend, **)
123
+ draw_univariate_histogram(backend)
124
+ annotate_axes(backend)
125
+ end
126
+
127
+ private def draw_univariate_histogram(backend)
128
+ map_color(palette: palette, order: color_order, norm: color_norm)
129
+
130
+ # TODO: calculate histogram here and use bar plot to visualize
131
+ data_variable = self.univariate_variable
132
+
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
148
+ end
149
+
150
+ if self.variables.key?(:color)
151
+ alpha = 0.5
152
+ else
153
+ alpha = 0.75
154
+ end
155
+
156
+ each_subset([:color], processed: true) do |sub_vars, sub_data|
157
+ name = sub_vars[:color]
158
+ observations = sub_data[data_variable].drop_na.to_a
159
+
160
+ backend.univariate_histogram(observations, name, data_variable, stat,
161
+ bin_start, bin_end, bin_size, alpha,
162
+ name, @color_mapper)
163
+ end
164
+ end
165
+
166
+ private def annotate_axes(backend)
167
+ if univariate?
168
+ xlabel = self.variables[:x]
169
+ ylabel = self.variables[:y]
170
+ case self.univariate_variable
171
+ when :x
172
+ ylabel = self.stat.to_s.capitalize
173
+ else
174
+ xlabel = self.stat.to_s.capitalize
175
+ end
176
+ backend.set_ylabel(ylabel) if ylabel
177
+ backend.set_xlabel(xlabel) if xlabel
178
+ end
179
+ end
180
+ end
181
+ end
182
+ 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