charty 0.2.1 → 0.2.7

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