charty 0.2.3 → 0.2.4

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