charty 0.2.3 → 0.2.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) 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/Gemfile +18 -0
  6. data/README.md +123 -4
  7. data/Rakefile +4 -5
  8. data/charty.gemspec +1 -3
  9. data/examples/sample_images/hist_gruff.png +0 -0
  10. data/images/penguins_body_mass_g_flipper_length_mm_scatter_plot.png +0 -0
  11. data/images/penguins_body_mass_g_flipper_length_mm_species_scatter_plot.png +0 -0
  12. data/images/penguins_body_mass_g_flipper_length_mm_species_sex_scatter_plot.png +0 -0
  13. data/images/penguins_species_body_mass_g_bar_plot_h.png +0 -0
  14. data/images/penguins_species_body_mass_g_bar_plot_v.png +0 -0
  15. data/images/penguins_species_body_mass_g_box_plot_h.png +0 -0
  16. data/images/penguins_species_body_mass_g_box_plot_v.png +0 -0
  17. data/images/penguins_species_body_mass_g_sex_bar_plot_v.png +0 -0
  18. data/images/penguins_species_body_mass_g_sex_box_plot_v.png +0 -0
  19. data/lib/charty.rb +4 -0
  20. data/lib/charty/backends/gruff.rb +13 -2
  21. data/lib/charty/backends/plotly.rb +322 -20
  22. data/lib/charty/backends/pyplot.rb +416 -64
  23. data/lib/charty/index.rb +213 -0
  24. data/lib/charty/linspace.rb +1 -1
  25. data/lib/charty/missing_value_support.rb +14 -0
  26. data/lib/charty/plot_methods.rb +173 -8
  27. data/lib/charty/plotters.rb +7 -0
  28. data/lib/charty/plotters/abstract_plotter.rb +87 -12
  29. data/lib/charty/plotters/bar_plotter.rb +200 -3
  30. data/lib/charty/plotters/box_plotter.rb +75 -7
  31. data/lib/charty/plotters/categorical_plotter.rb +272 -40
  32. data/lib/charty/plotters/count_plotter.rb +7 -0
  33. data/lib/charty/plotters/estimation_support.rb +84 -0
  34. data/lib/charty/plotters/random_support.rb +25 -0
  35. data/lib/charty/plotters/relational_plotter.rb +518 -0
  36. data/lib/charty/plotters/scatter_plotter.rb +115 -0
  37. data/lib/charty/plotters/vector_plotter.rb +6 -0
  38. data/lib/charty/statistics.rb +87 -2
  39. data/lib/charty/table.rb +50 -15
  40. data/lib/charty/table_adapters.rb +2 -0
  41. data/lib/charty/table_adapters/active_record_adapter.rb +17 -9
  42. data/lib/charty/table_adapters/base_adapter.rb +69 -0
  43. data/lib/charty/table_adapters/daru_adapter.rb +37 -3
  44. data/lib/charty/table_adapters/datasets_adapter.rb +6 -2
  45. data/lib/charty/table_adapters/hash_adapter.rb +130 -16
  46. data/lib/charty/table_adapters/narray_adapter.rb +25 -6
  47. data/lib/charty/table_adapters/nmatrix_adapter.rb +15 -5
  48. data/lib/charty/table_adapters/pandas_adapter.rb +81 -0
  49. data/lib/charty/vector.rb +69 -0
  50. data/lib/charty/vector_adapters.rb +183 -0
  51. data/lib/charty/vector_adapters/array_adapter.rb +109 -0
  52. data/lib/charty/vector_adapters/daru_adapter.rb +171 -0
  53. data/lib/charty/vector_adapters/narray_adapter.rb +187 -0
  54. data/lib/charty/vector_adapters/nmatrix_adapter.rb +37 -0
  55. data/lib/charty/vector_adapters/numpy_adapter.rb +168 -0
  56. data/lib/charty/vector_adapters/pandas_adapter.rb +200 -0
  57. data/lib/charty/version.rb +1 -1
  58. metadata +33 -45
@@ -1,18 +1,215 @@
1
1
  module Charty
2
2
  module Plotters
3
3
  class BarPlotter < CategoricalPlotter
4
+ self.default_palette = :light
5
+ self.require_numeric = true
6
+
7
+ def initialize(data: nil, variables: {}, **options, &block)
8
+ x, y, color = variables.values_at(:x, :y, :color)
9
+ super(x, y, color, data: data, **options, &block)
10
+ end
11
+
12
+ attr_reader :error_color
13
+
14
+ def error_color=(error_color)
15
+ @error_color = check_error_color(error_color)
16
+ end
17
+
18
+ private def check_error_color(value)
19
+ case value
20
+ when Colors::AbstractColor
21
+ value
22
+ when Array
23
+ Colors::RGB.new(*value)
24
+ when String
25
+ # TODO: Use Colors.parse when it'll be available
26
+ Colors::RGB.parse(value)
27
+ else
28
+ raise ArgumentError,
29
+ "invalid value for error_color (%p for a color, a RGB tripret, or a RGB hex string)" % value
30
+ end
31
+ end
32
+
33
+ attr_reader :error_width
34
+
35
+ def error_width=(error_width)
36
+ @error_width = check_number(error_width, :error_width, allow_nil: true)
37
+ end
38
+
39
+ attr_reader :cap_size
40
+
41
+ def cap_size=(cap_size)
42
+ @cap_size = check_number(cap_size, :cap_size, allow_nil: true)
43
+ end
44
+
4
45
  def render
5
46
  backend = Backends.current
6
47
  backend.begin_figure
7
48
  draw_bars(backend)
8
49
  annotate_axes(backend)
50
+ backend.invert_yaxis if orient == :h
9
51
  backend.show
10
52
  end
11
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
+ end
65
+
12
66
  private def draw_bars(backend)
13
- statistic = @plot_data.map {|xs| Statistics.mean(xs) }
14
- bar_pos = (0 ... statistic.length).to_a
15
- backend.bar(bar_pos, statistic, color: @colors)
67
+ setup_estimations
68
+
69
+ if @plot_colors.nil?
70
+ bar_pos = (0 ... @estimations.length).to_a
71
+ error_colors = bar_pos.map { error_color }
72
+ if @conf_int.empty?
73
+ ci_params = {}
74
+ else
75
+ ci_params = {conf_int: @conf_int, error_colors: error_colors,
76
+ error_width: error_width, cap_size: cap_size}
77
+ end
78
+ backend.bar(bar_pos, nil, @estimations, @colors, orient, **ci_params)
79
+ else
80
+ bar_pos = (0 ... @estimations[0].length).to_a
81
+ error_colors = bar_pos.map { error_color }
82
+ offsets = color_offsets
83
+ width = nested_width
84
+ @color_names.each_with_index do |color_name, i|
85
+ pos = bar_pos.map {|x| x + offsets[i] }
86
+ colors = Array.new(@estimations[i].length) { @colors[i] }
87
+ if @conf_int[i].empty?
88
+ ci_params = {}
89
+ else
90
+ ci_params = {conf_int: @conf_int[i], error_colors: error_colors,
91
+ error_width: error_width, cap_size: cap_size}
92
+ end
93
+ backend.bar(pos, @group_names, @estimations[i], colors, orient,
94
+ label: color_name, width: width, **ci_params)
95
+ end
96
+ end
97
+ end
98
+
99
+ private def setup_estimations
100
+ if @color_names.nil?
101
+ setup_estimations_with_single_color_group
102
+ else
103
+ setup_estimations_with_multiple_color_groups
104
+ end
105
+ end
106
+
107
+ private def setup_estimations_with_single_color_group
108
+ estimations = []
109
+ conf_int = []
110
+
111
+ @plot_data.each do |group_data|
112
+ # Single color group
113
+ if @plot_units.nil?
114
+ stat_data = group_data.drop_na
115
+ unit_data = nil
116
+ else
117
+ # TODO: Support units
118
+ end
119
+
120
+ estimation = if stat_data.size == 0
121
+ Float::NAN
122
+ else
123
+ estimate(estimator, stat_data)
124
+ end
125
+ estimations << estimation
126
+
127
+ unless ci.nil?
128
+ if stat_data.size < 2
129
+ conf_int << [Float::NAN, Float::NAN]
130
+ next
131
+ end
132
+
133
+ if ci == :sd
134
+ sd = stat_data.stdev
135
+ conf_int << [estimation - sd, estimation + sd]
136
+ else
137
+ conf_int << Statistics.bootstrap_ci(stat_data, ci, func: estimator, n_boot: n_boot,
138
+ units: unit_data, random: random)
139
+ end
140
+ end
141
+ end
142
+
143
+ @estimations = estimations
144
+ @conf_int = conf_int
145
+ end
146
+
147
+ private def setup_estimations_with_multiple_color_groups
148
+ estimations = Array.new(@color_names.length) { [] }
149
+ conf_int = Array.new(@color_names.length) { [] }
150
+
151
+ @plot_data.each_with_index do |group_data, i|
152
+ @color_names.each_with_index do |color_name, j|
153
+ if @plot_colors[i].length == 0
154
+ estimations[j] << Float::NAN
155
+ unless ci.nil?
156
+ conf_int[j] << [Float::NAN, Float::NAN]
157
+ end
158
+ next
159
+ end
160
+
161
+ color_mask = @plot_colors[i].eq(color_name)
162
+ if @plot_units.nil?
163
+ begin
164
+ stat_data = group_data[color_mask].drop_na
165
+ rescue
166
+ @plot_data.each_with_index {|pd, k| p k => pd }
167
+ @plot_colors.each_with_index {|pc, k| p k => pc }
168
+ raise
169
+ end
170
+ unit_data = nil
171
+ else
172
+ # TODO: Support units
173
+ end
174
+
175
+ estimation = if stat_data.size == 0
176
+ Float::NAN
177
+ else
178
+ estimate(estimator, stat_data)
179
+ end
180
+ estimations[j] << estimation
181
+
182
+ unless ci.nil?
183
+ if stat_data.size < 2
184
+ conf_int[j] << [Float::NAN, Float::NAN]
185
+ next
186
+ end
187
+
188
+ if ci == :sd
189
+ sd = stat_data.stdev
190
+ conf_int[j] << [estimation - sd, estimation + sd]
191
+ else
192
+ conf_int[j] << Statistics.bootstrap_ci(stat_data, ci, func: estimator, n_boot: n_boot,
193
+ units: unit_data, random: random)
194
+ end
195
+ end
196
+ end
197
+ end
198
+
199
+ @estimations = estimations
200
+ @conf_int = conf_int
201
+ end
202
+
203
+ private def estimate(estimator, data)
204
+ case estimator
205
+ when :count
206
+ data.length
207
+ when :mean
208
+ data.mean
209
+ else
210
+ # TODO: Support other estimations
211
+ raise NotImplementedError, "#{estimator} estimator is not supported yet"
212
+ end
16
213
  end
17
214
  end
18
215
  end
@@ -1,25 +1,93 @@
1
1
  module Charty
2
2
  module Plotters
3
3
  class BoxPlotter < CategoricalPlotter
4
+ self.default_palette = :light
5
+ self.require_numeric = true
6
+
7
+ def initialize(data: nil, variables: {}, **options, &block)
8
+ x, y, color = variables.values_at(:x, :y, :color)
9
+ super(x, y, color, data: data, **options, &block)
10
+ end
11
+
12
+ attr_reader :flier_size
13
+
14
+ def flier_size=(val)
15
+ @flier_size = check_number(val, :flier_size, allow_nil: true)
16
+ end
17
+
18
+ attr_reader :line_width
19
+
20
+ def line_width=(val)
21
+ @line_width = check_number(val, :line_width, allow_nil: true)
22
+ end
23
+
24
+ attr_reader :whisker
25
+
26
+ def whisker=(val)
27
+ @whisker = check_number(val, :whisker, allow_nil: true)
28
+ end
29
+
4
30
  def render
5
31
  backend = Backends.current
6
32
  backend.begin_figure
7
33
  draw_box_plot(backend)
8
34
  annotate_axes(backend)
35
+ backend.invert_yaxis if orient == :h
9
36
  backend.show
10
37
  end
11
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
+ end
50
+
12
51
  private def draw_box_plot(backend)
13
- plot_data = @plot_data.each do |group_data|
14
- next nil if group_data.empty?
52
+ if @plot_colors.nil?
53
+ plot_data = @plot_data.map do |group_data|
54
+ unless group_data.empty?
55
+ group_data = group_data.drop_na
56
+ group_data unless group_data.empty?
57
+ end
58
+ end
15
59
 
16
- group_data = Array(group_data)
17
- remove_na!(group_data)
60
+ backend.box_plot(plot_data,
61
+ @group_names,
62
+ orient: orient,
63
+ colors: @colors,
64
+ gray: @gray,
65
+ dodge: dodge,
66
+ width: @width,
67
+ flier_size: flier_size,
68
+ whisker: whisker)
69
+ else
70
+ grouped_box_data = @color_names.map.with_index do |color_name, i|
71
+ @plot_data.map.with_index do |group_data, j|
72
+ unless group_data.empty?
73
+ color_mask = @plot_colors[j].eq(color_name)
74
+ group_data = group_data[color_mask].drop_na
75
+ group_data unless group_data.empty?
76
+ end
77
+ end
78
+ end
18
79
 
19
- group_data
80
+ backend.grouped_box_plot(grouped_box_data,
81
+ @group_names,
82
+ @color_names,
83
+ orient: orient,
84
+ colors: @colors,
85
+ gray: @gray,
86
+ dodge: dodge,
87
+ width: @width,
88
+ flier_size: flier_size,
89
+ whisker: whisker)
20
90
  end
21
- backend.box_plot(plot_data, (0 ... @plot_data.length).to_a,
22
- color: @colors, gray: @gray)
23
91
  end
24
92
  end
25
93
  end
@@ -1,7 +1,33 @@
1
1
  module Charty
2
2
  module Plotters
3
3
  class CategoricalPlotter < AbstractPlotter
4
- def initialize(x, y, color, **options, &block)
4
+ class << self
5
+ attr_reader :default_palette
6
+
7
+ def default_palette=(val)
8
+ case val
9
+ when :light, :dark
10
+ @default_palette = val
11
+ when "light", "dark"
12
+ @default_palette = val.to_sym
13
+ else
14
+ raise ArgumentError, "default_palette must be :light or :dark"
15
+ end
16
+ end
17
+
18
+ attr_reader :require_numeric
19
+
20
+ def require_numeric=(val)
21
+ case val
22
+ when true, false
23
+ @require_numeric = val
24
+ else
25
+ raise ArgumentError, "require_numeric must be ture or false"
26
+ end
27
+ end
28
+ end
29
+
30
+ def initialize(x, y, color, order: nil, orient: nil, width: 0.8r, dodge: false, **options, &block)
5
31
  super
6
32
 
7
33
  setup_variables
@@ -11,9 +37,60 @@ module Charty
11
37
  attr_reader :order
12
38
 
13
39
  def order=(order)
14
- @order = Array(order).map(&method(:normalize_name))
40
+ @order = order && Array(order).map(&method(:normalize_name))
41
+ end
42
+
43
+ attr_reader :orient
44
+
45
+ def orient=(orient)
46
+ @orient = check_orient(orient)
47
+ end
48
+
49
+ private def check_orient(value)
50
+ case value
51
+ when nil, :v, :h
52
+ value
53
+ when "v", "h"
54
+ value.to_sym
55
+ else
56
+ raise ArgumentError,
57
+ "invalid value for orient (#{value.inspect} for nil, :v, or :h)"
58
+ end
59
+ end
60
+
61
+ attr_reader :width
62
+
63
+ def width=(val)
64
+ @width = check_number(val, :width)
15
65
  end
16
66
 
67
+ attr_reader :dodge
68
+
69
+ def dodge=(dodge)
70
+ @dodge = check_boolean(dodge, :dodge)
71
+ end
72
+
73
+ attr_reader :saturation
74
+
75
+ def saturation=(saturation)
76
+ @saturation = check_saturation(saturation)
77
+ end
78
+
79
+ private def check_saturation(value)
80
+ case value
81
+ when 0..1
82
+ value
83
+ when Numeric
84
+ raise ArgumentError,
85
+ "saturation is out of range (%p for 0..1)" % value
86
+ else
87
+ raise ArgumentError,
88
+ "invalid value for saturation (%p for a value in 0..1)" % value
89
+ end
90
+ end
91
+
92
+ include EstimationSupport
93
+
17
94
  private def normalize_name(value)
18
95
  case value
19
96
  when String, Symbol
@@ -23,12 +100,16 @@ module Charty
23
100
  end
24
101
  end
25
102
 
26
- attr_reader :group_names, :plot_data, :group_label, :value_label
103
+ attr_reader :group_names, :plot_data, :group_label
104
+
105
+ attr_accessor :value_label
27
106
 
28
107
  private def setup_variables
29
108
  if x.nil? && y.nil?
109
+ @input_format = :wide
30
110
  setup_variables_with_wide_form_dataset
31
111
  else
112
+ @input_format = :long
32
113
  setup_variables_with_long_form_dataset
33
114
  end
34
115
  end
@@ -62,36 +143,58 @@ module Charty
62
143
  end
63
144
 
64
145
  private def setup_variables_with_long_form_dataset
65
- x, y = @x, @y
146
+ x = self.x
147
+ y = self.y
148
+ color = self.color
66
149
  if @data
67
- x = @data[x] || x
68
- y = @data[y] || y
150
+ x &&= @data[x] || x
151
+ y &&= @data[y] || y
152
+ color &&= @data[color] || color
69
153
  end
70
154
 
71
155
  # Validate inputs
72
- [x, y].each do |input|
73
- next if array?(input)
156
+ [x, y, color].each do |input|
157
+ next if input.nil? || array?(input)
74
158
  raise RuntimeError,
75
- "Could not interpret interpret input `#{input.inspect}`"
159
+ "Could not interpret input `#{input.inspect}`"
76
160
  end
77
161
 
162
+ x = Charty::Vector.try_convert(x)
163
+ y = Charty::Vector.try_convert(y)
164
+ color = Charty::Vector.try_convert(color)
165
+
166
+ self.orient = infer_orient(x, y, orient, self.class.require_numeric)
167
+
78
168
  if x.nil? || y.nil?
79
169
  setup_single_data
80
170
  else
81
- # FIXME: Assume vertical plot
82
- groups, vals = x, y
171
+ if orient == :v
172
+ groups, vals = x, y
173
+ else
174
+ groups, vals = y, x
175
+ end
83
176
 
84
177
  if groups.respond_to?(:name)
85
178
  @group_label = groups.name
86
179
  end
87
180
 
88
- if vals.respond_to?(:name)
89
- @value_label = vals.name
181
+ @group_names = groups.categorical_order(order)
182
+ @plot_data, @value_label = group_long_form(vals, groups, @group_names)
183
+
184
+ # Handle color variable
185
+ if color.nil?
186
+ @plot_colors = nil
187
+ @color_title = nil
188
+ @color_names = nil
189
+ else
190
+ # Get the order of color levels
191
+ @color_names = color.categorical_order(color_order)
192
+
193
+ # Group the color data
194
+ @plot_colors, @color_title = group_long_form(color, groups, @group_names)
90
195
  end
91
196
 
92
- # FIXME: Assume groups has only unique values
93
- @group_names = groups
94
- @plot_data = vals.map {|v| [v] }
197
+ # TODO: Handle units
95
198
  end
96
199
  end
97
200
 
@@ -100,48 +203,177 @@ module Charty
100
203
  "Single data plot is not supported yet"
101
204
  end
102
205
 
206
+ private def infer_orient(x, y, orient, require_numeric)
207
+ x_type = x.nil? ? nil : variable_type(x)
208
+ y_type = y.nil? ? nil : variable_type(y)
209
+
210
+ nonnumeric_error = "%{orient} orientation requires numeric `%{dim}` variable"
211
+ single_variable_warning = "%{orient} orientation ignored with only `%{dim}` specified"
212
+
213
+ case
214
+ when x.nil?
215
+ case orient
216
+ when :h
217
+ warn single_variable_warning % {orient: "Horizontal", dim: "y"}
218
+ end
219
+ if require_numeric && y_type != :numeric
220
+ raise ArgumentError, nonnumeric_error % {orient: "Vertical", dim: "y"}
221
+ end
222
+ return :v
223
+ when y.nil?
224
+ case orient
225
+ when :v
226
+ warn single_variable_warning % {orient: "Vertical", dim: "x"}
227
+ end
228
+ if require_numeric && x_type != :numeric
229
+ raise ArgumentError, nonnumeric_error % {orient: "Horizontal", dim: "x"}
230
+ end
231
+ return :h
232
+ end
233
+ case orient
234
+ when :v
235
+ if require_numeric && y_type != :numeric
236
+ raise ArgumentError, nonnumeric_error % {orient: "Vertical", dim: "y"}
237
+ end
238
+ return :v
239
+ when :h
240
+ if require_numeric && x_type != :numeric
241
+ raise ArgumentError, nonnumeric_error % {orient: "Horizontal", dim: "x"}
242
+ end
243
+ return :h
244
+ when nil
245
+ case
246
+ when x_type != :categorical && y_type == :categorical
247
+ return :h
248
+ when x_type != :numeric && y_type == :numeric
249
+ return :v
250
+ when x_type == :numeric && y_type != :numeric
251
+ return :h
252
+ when require_numeric && x_type != :numeric && y_type != :numeric
253
+ raise ArgumentError, "Neither the `x` nor `y` variable appears to be numeric."
254
+ else
255
+ :v
256
+ end
257
+ else
258
+ # must be unreachable
259
+ raise RuntimeError, "BUG in Charty. Please report the issue."
260
+ end
261
+ end
262
+
263
+ private def group_long_form(vals, groups, group_order)
264
+ grouped_vals = vals.group_by(groups)
265
+
266
+ plot_data = group_order.map {|g| grouped_vals[g] || [] }
267
+
268
+ if vals.respond_to?(:name)
269
+ value_label = vals.name
270
+ end
271
+
272
+ return plot_data, value_label
273
+ end
274
+
103
275
  private def setup_colors
104
- n_colors = @plot_data.length
105
- if @palette.nil?
276
+ if @color_names.nil?
277
+ n_colors = @plot_data.length
278
+ else
279
+ n_colors = @color_names.length
280
+ end
281
+
282
+ if key_color.nil? && self.palette.nil?
283
+ # Check the current palette has enough colors
106
284
  current_palette = Palette.default
107
285
  if n_colors <= current_palette.n_colors
108
- palette = Palette.new(current_palette.colors, n_colors)
286
+ colors = Palette.new(current_palette.colors, n_colors).colors
287
+ else
288
+ # Use huls palette as default when the default palette is not usable
289
+ colors = Palette.husl_colors(n_colors, l: 0.7r)
290
+ end
291
+ elsif self.palette.nil?
292
+ if @color_names.nil?
293
+ colors = Array.new(n_colors) { key_color }
109
294
  else
110
- palette = Palette.husl(n_colors, l: 0.7r)
295
+ raise NotImplementedError,
296
+ "Default palette with key_color is not supported"
297
+ # TODO: Support light_palette and dark_palette in red-palette
298
+ # if default_palette is light
299
+ # colors = Palette.light_palette(key_color, n_colors)
300
+ # elsif default_palette is dark
301
+ # colors = Palette.dark_palette(key_color, n_colors)
302
+ # else
303
+ # raise "No default palette specified"
304
+ # end
111
305
  end
112
306
  else
113
- case @palette
307
+ case self.palette
114
308
  when Hash
115
- # Assume @palette has a hash table that maps
116
- # group_names to colors
117
- palette = @group_names.map {|gn| @palette[gn] }
118
- else
119
- palette = @palette
309
+ if @color_names.nil?
310
+ levels = @group_names
311
+ else
312
+ levels = @color_names
313
+ end
314
+ colors = levels.map {|gn| self.palette[gn] }
120
315
  end
121
- palette = Palette.new(palette, n_colors)
316
+ colors = Palette.new(colors, n_colors).colors
317
+ end
318
+
319
+ if saturation < 1
320
+ colors = Palette.new(colors, n_colors, desaturate_factor: saturation).colors
122
321
  end
123
322
 
124
- @colors = palette.colors.map {|c| c.to_rgb }
323
+ @colors = colors.map {|c| c.to_rgb }
125
324
  lightness_values = @colors.map {|c| c.to_hsl.l }
126
325
  lum = lightness_values.min * 0.6r
127
326
  @gray = Colors::RGB.new(lum, lum, lum) # TODO: Use Charty::Gray
128
327
  end
129
328
 
130
- private def annotate_axes(backend)
131
- backend.set_xlabel(@group_label)
132
- backend.set_ylabel(@value_label)
133
- backend.set_xticks((0 ... @plot_data.length).to_a)
134
- backend.set_xtick_labels(@group_names)
135
- backend.disable_xaxis_grid
136
- backend.set_xlim(-0.5, @plot_data.length - 0.5)
329
+ private def color_offsets
330
+ n_names = @color_names.length
331
+ if self.dodge
332
+ each_width = @width / n_names
333
+ offsets = Charty::Linspace.new(0 .. (@width - each_width), n_names).to_a
334
+ offsets_mean = Statistics.mean(offsets)
335
+ offsets.map {|x| x - offsets_mean }
336
+ else
337
+ Array.new(n_names) { 0 }
338
+ end
137
339
  end
138
340
 
139
- private def remove_na!(ary)
140
- ary.reject! do |x|
141
- next true if x.nil?
142
- x.respond_to?(:nan?) && x.nan?
341
+ private def nested_width
342
+ if self.dodge
343
+ @width / @color_names.length * 0.98r
344
+ else
345
+ @width
346
+ end
347
+ end
348
+
349
+ private def annotate_axes(backend)
350
+ if orient == :v
351
+ xlabel, ylabel = @group_label, @value_label
352
+ else
353
+ xlabel, ylabel = @value_label, @group_label
354
+ end
355
+ backend.set_xlabel(xlabel) unless xlabel.nil?
356
+ backend.set_ylabel(ylabel) unless ylabel.nil?
357
+
358
+ if orient == :v
359
+ backend.set_xticks((0 ... @plot_data.length).to_a)
360
+ backend.set_xtick_labels(@group_names)
361
+ else
362
+ backend.set_yticks((0 ... @plot_data.length).to_a)
363
+ backend.set_ytick_labels(@group_names)
364
+ end
365
+
366
+ if orient == :v
367
+ backend.disable_xaxis_grid
368
+ backend.set_xlim(-0.5, @plot_data.length - 0.5)
369
+ else
370
+ backend.disable_yaxis_grid
371
+ backend.set_ylim(-0.5, @plot_data.length - 0.5)
372
+ end
373
+
374
+ unless @color_names.nil?
375
+ backend.legend(loc: :best, title: @color_title)
143
376
  end
144
- ary
145
377
  end
146
378
  end
147
379
  end