charty 0.1.5.dev → 0.2.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (87) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +71 -0
  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 +176 -9
  8. data/Rakefile +4 -5
  9. data/charty.gemspec +10 -1
  10. data/examples/Gemfile +1 -0
  11. data/examples/active_record.ipynb +1 -1
  12. data/examples/daru.ipynb +1 -1
  13. data/examples/iris_dataset.ipynb +1 -1
  14. data/examples/nmatrix.ipynb +1 -1
  15. data/examples/{numo-narray.ipynb → numo_narray.ipynb} +1 -1
  16. data/examples/palette.rb +71 -0
  17. data/examples/sample.png +0 -0
  18. data/examples/sample_bokeh.ipynb +156 -0
  19. data/examples/sample_google_chart.ipynb +229 -68
  20. data/examples/sample_images/bar_bokeh.html +85 -0
  21. data/examples/sample_images/barh_bokeh.html +85 -0
  22. data/examples/sample_images/box_plot_bokeh.html +85 -0
  23. data/examples/sample_images/curve_bokeh.html +85 -0
  24. data/examples/sample_images/curve_with_function_bokeh.html +85 -0
  25. data/examples/sample_images/hist_gruff.png +0 -0
  26. data/examples/sample_images/scatter_bokeh.html +85 -0
  27. data/examples/sample_pyplot.ipynb +40 -38
  28. data/images/penguins_body_mass_g_flipper_length_mm_scatter_plot.png +0 -0
  29. data/images/penguins_body_mass_g_flipper_length_mm_species_scatter_plot.png +0 -0
  30. data/images/penguins_body_mass_g_flipper_length_mm_species_sex_scatter_plot.png +0 -0
  31. data/images/penguins_species_body_mass_g_bar_plot_h.png +0 -0
  32. data/images/penguins_species_body_mass_g_bar_plot_v.png +0 -0
  33. data/images/penguins_species_body_mass_g_box_plot_h.png +0 -0
  34. data/images/penguins_species_body_mass_g_box_plot_v.png +0 -0
  35. data/images/penguins_species_body_mass_g_sex_bar_plot_v.png +0 -0
  36. data/images/penguins_species_body_mass_g_sex_box_plot_v.png +0 -0
  37. data/lib/charty.rb +14 -1
  38. data/lib/charty/backend_methods.rb +8 -0
  39. data/lib/charty/backends.rb +80 -0
  40. data/lib/charty/backends/bokeh.rb +32 -26
  41. data/lib/charty/backends/google_charts.rb +267 -0
  42. data/lib/charty/backends/gruff.rb +102 -83
  43. data/lib/charty/backends/plotly.rb +685 -0
  44. data/lib/charty/backends/pyplot.rb +586 -92
  45. data/lib/charty/backends/rubyplot.rb +82 -74
  46. data/lib/charty/backends/unicode_plot.rb +79 -0
  47. data/lib/charty/index.rb +213 -0
  48. data/lib/charty/linspace.rb +1 -1
  49. data/lib/charty/missing_value_support.rb +14 -0
  50. data/lib/charty/plot_methods.rb +184 -0
  51. data/lib/charty/plotter.rb +48 -40
  52. data/lib/charty/plotters.rb +11 -0
  53. data/lib/charty/plotters/abstract_plotter.rb +183 -0
  54. data/lib/charty/plotters/bar_plotter.rb +201 -0
  55. data/lib/charty/plotters/box_plotter.rb +79 -0
  56. data/lib/charty/plotters/categorical_plotter.rb +380 -0
  57. data/lib/charty/plotters/count_plotter.rb +7 -0
  58. data/lib/charty/plotters/estimation_support.rb +84 -0
  59. data/lib/charty/plotters/random_support.rb +25 -0
  60. data/lib/charty/plotters/relational_plotter.rb +518 -0
  61. data/lib/charty/plotters/scatter_plotter.rb +104 -0
  62. data/lib/charty/plotters/vector_plotter.rb +6 -0
  63. data/lib/charty/statistics.rb +114 -0
  64. data/lib/charty/table.rb +80 -3
  65. data/lib/charty/table_adapters.rb +25 -0
  66. data/lib/charty/table_adapters/active_record_adapter.rb +63 -0
  67. data/lib/charty/table_adapters/base_adapter.rb +69 -0
  68. data/lib/charty/table_adapters/daru_adapter.rb +70 -0
  69. data/lib/charty/table_adapters/datasets_adapter.rb +49 -0
  70. data/lib/charty/table_adapters/hash_adapter.rb +224 -0
  71. data/lib/charty/table_adapters/narray_adapter.rb +76 -0
  72. data/lib/charty/table_adapters/nmatrix_adapter.rb +67 -0
  73. data/lib/charty/table_adapters/pandas_adapter.rb +81 -0
  74. data/lib/charty/util.rb +20 -0
  75. data/lib/charty/vector.rb +69 -0
  76. data/lib/charty/vector_adapters.rb +183 -0
  77. data/lib/charty/vector_adapters/array_adapter.rb +109 -0
  78. data/lib/charty/vector_adapters/daru_adapter.rb +171 -0
  79. data/lib/charty/vector_adapters/narray_adapter.rb +187 -0
  80. data/lib/charty/vector_adapters/nmatrix_adapter.rb +37 -0
  81. data/lib/charty/vector_adapters/numpy_adapter.rb +168 -0
  82. data/lib/charty/vector_adapters/pandas_adapter.rb +200 -0
  83. data/lib/charty/version.rb +1 -1
  84. metadata +179 -10
  85. data/.travis.yml +0 -11
  86. data/lib/charty/backends/google_chart.rb +0 -167
  87. data/lib/charty/plotter_adapter.rb +0 -17
@@ -0,0 +1,201 @@
1
+ module Charty
2
+ module Plotters
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
+
45
+ private def render_plot(backend, **)
46
+ draw_bars(backend)
47
+ annotate_axes(backend)
48
+ backend.invert_yaxis if orient == :h
49
+ end
50
+
51
+ private def draw_bars(backend)
52
+ setup_estimations
53
+
54
+ if @plot_colors.nil?
55
+ bar_pos = (0 ... @estimations.length).to_a
56
+ error_colors = bar_pos.map { error_color }
57
+ if @conf_int.empty?
58
+ ci_params = {}
59
+ else
60
+ ci_params = {conf_int: @conf_int, error_colors: error_colors,
61
+ error_width: error_width, cap_size: cap_size}
62
+ end
63
+ backend.bar(bar_pos, nil, @estimations, @colors, orient, **ci_params)
64
+ else
65
+ bar_pos = (0 ... @estimations[0].length).to_a
66
+ error_colors = bar_pos.map { error_color }
67
+ offsets = color_offsets
68
+ width = nested_width
69
+ @color_names.each_with_index do |color_name, i|
70
+ pos = bar_pos.map {|x| x + offsets[i] }
71
+ colors = Array.new(@estimations[i].length) { @colors[i] }
72
+ if @conf_int[i].empty?
73
+ ci_params = {}
74
+ else
75
+ ci_params = {conf_int: @conf_int[i], error_colors: error_colors,
76
+ error_width: error_width, cap_size: cap_size}
77
+ end
78
+ backend.bar(pos, @group_names, @estimations[i], colors, orient,
79
+ label: color_name, width: width, **ci_params)
80
+ end
81
+ end
82
+ end
83
+
84
+ private def setup_estimations
85
+ if @color_names.nil?
86
+ setup_estimations_with_single_color_group
87
+ else
88
+ setup_estimations_with_multiple_color_groups
89
+ end
90
+ end
91
+
92
+ private def setup_estimations_with_single_color_group
93
+ estimations = []
94
+ conf_int = []
95
+
96
+ @plot_data.each do |group_data|
97
+ # Single color group
98
+ if @plot_units.nil?
99
+ stat_data = group_data.drop_na
100
+ unit_data = nil
101
+ else
102
+ # TODO: Support units
103
+ end
104
+
105
+ estimation = if stat_data.size == 0
106
+ Float::NAN
107
+ else
108
+ estimate(estimator, stat_data)
109
+ end
110
+ estimations << estimation
111
+
112
+ unless ci.nil?
113
+ if stat_data.size < 2
114
+ conf_int << [Float::NAN, Float::NAN]
115
+ next
116
+ end
117
+
118
+ if ci == :sd
119
+ sd = stat_data.stdev
120
+ conf_int << [estimation - sd, estimation + sd]
121
+ else
122
+ conf_int << Statistics.bootstrap_ci(stat_data, ci, func: estimator, n_boot: n_boot,
123
+ units: unit_data, random: random)
124
+ end
125
+ end
126
+ end
127
+
128
+ @estimations = estimations
129
+ @conf_int = conf_int
130
+ end
131
+
132
+ private def setup_estimations_with_multiple_color_groups
133
+ estimations = Array.new(@color_names.length) { [] }
134
+ conf_int = Array.new(@color_names.length) { [] }
135
+
136
+ @plot_data.each_with_index do |group_data, i|
137
+ @color_names.each_with_index do |color_name, j|
138
+ if @plot_colors[i].length == 0
139
+ estimations[j] << Float::NAN
140
+ unless ci.nil?
141
+ conf_int[j] << [Float::NAN, Float::NAN]
142
+ end
143
+ next
144
+ end
145
+
146
+ color_mask = @plot_colors[i].eq(color_name)
147
+ if @plot_units.nil?
148
+ begin
149
+ stat_data = group_data[color_mask].drop_na
150
+ rescue
151
+ @plot_data.each_with_index {|pd, k| p k => pd }
152
+ @plot_colors.each_with_index {|pc, k| p k => pc }
153
+ raise
154
+ end
155
+ unit_data = nil
156
+ else
157
+ # TODO: Support units
158
+ end
159
+
160
+ estimation = if stat_data.size == 0
161
+ Float::NAN
162
+ else
163
+ estimate(estimator, stat_data)
164
+ end
165
+ estimations[j] << estimation
166
+
167
+ unless ci.nil?
168
+ if stat_data.size < 2
169
+ conf_int[j] << [Float::NAN, Float::NAN]
170
+ next
171
+ end
172
+
173
+ if ci == :sd
174
+ sd = stat_data.stdev
175
+ conf_int[j] << [estimation - sd, estimation + sd]
176
+ else
177
+ conf_int[j] << Statistics.bootstrap_ci(stat_data, ci, func: estimator, n_boot: n_boot,
178
+ units: unit_data, random: random)
179
+ end
180
+ end
181
+ end
182
+ end
183
+
184
+ @estimations = estimations
185
+ @conf_int = conf_int
186
+ end
187
+
188
+ private def estimate(estimator, data)
189
+ case estimator
190
+ when :count
191
+ data.length
192
+ when :mean
193
+ data.mean
194
+ else
195
+ # TODO: Support other estimations
196
+ raise NotImplementedError, "#{estimator} estimator is not supported yet"
197
+ end
198
+ end
199
+ end
200
+ end
201
+ end
@@ -0,0 +1,79 @@
1
+ module Charty
2
+ module Plotters
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
+
30
+ private def render_plot(backend, **)
31
+ draw_box_plot(backend)
32
+ annotate_axes(backend)
33
+ backend.invert_yaxis if orient == :h
34
+ end
35
+
36
+ private def draw_box_plot(backend)
37
+ if @plot_colors.nil?
38
+ plot_data = @plot_data.map do |group_data|
39
+ unless group_data.empty?
40
+ group_data = group_data.drop_na
41
+ group_data unless group_data.empty?
42
+ end
43
+ end
44
+
45
+ backend.box_plot(plot_data,
46
+ @group_names,
47
+ orient: orient,
48
+ colors: @colors,
49
+ gray: @gray,
50
+ dodge: dodge,
51
+ width: @width,
52
+ flier_size: flier_size,
53
+ whisker: whisker)
54
+ else
55
+ grouped_box_data = @color_names.map.with_index do |color_name, i|
56
+ @plot_data.map.with_index do |group_data, j|
57
+ unless group_data.empty?
58
+ color_mask = @plot_colors[j].eq(color_name)
59
+ group_data = group_data[color_mask].drop_na
60
+ group_data unless group_data.empty?
61
+ end
62
+ end
63
+ end
64
+
65
+ backend.grouped_box_plot(grouped_box_data,
66
+ @group_names,
67
+ @color_names,
68
+ orient: orient,
69
+ colors: @colors,
70
+ gray: @gray,
71
+ dodge: dodge,
72
+ width: @width,
73
+ flier_size: flier_size,
74
+ whisker: whisker)
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,380 @@
1
+ module Charty
2
+ module Plotters
3
+ class CategoricalPlotter < AbstractPlotter
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)
31
+ super
32
+
33
+ setup_variables
34
+ setup_colors
35
+ end
36
+
37
+ attr_reader :order
38
+
39
+ def order=(order)
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)
65
+ end
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
+
94
+ private def normalize_name(value)
95
+ case value
96
+ when String, Symbol
97
+ value
98
+ else
99
+ value.to_str
100
+ end
101
+ end
102
+
103
+ attr_reader :group_names, :plot_data, :group_label
104
+
105
+ attr_accessor :value_label
106
+
107
+ private def setup_variables
108
+ if x.nil? && y.nil?
109
+ @input_format = :wide
110
+ setup_variables_with_wide_form_dataset
111
+ else
112
+ @input_format = :long
113
+ setup_variables_with_long_form_dataset
114
+ end
115
+ end
116
+
117
+ private def setup_variables_with_wide_form_dataset
118
+ if @color
119
+ raise ArgumentError,
120
+ "Cannot use `color` without `x` or `y`"
121
+ end
122
+
123
+ # No color grouping with wide inputs
124
+ @plot_colors = nil
125
+ @color_title = nil
126
+ @color_names = nil
127
+
128
+ # No statistical units with wide inputs
129
+ @plot_units = nil
130
+
131
+ @value_label = nil
132
+ @group_label = nil
133
+
134
+ order = @order # TODO: supply order via parameter
135
+ unless order
136
+ order = @data.column_names.select do |cn|
137
+ @data[cn].all? {|x| Float(x, exception: false) }
138
+ end
139
+ end
140
+ order ||= @data.column_names
141
+ @plot_data = order.map {|cn| @data[cn] }
142
+ @group_names = order
143
+ end
144
+
145
+ private def setup_variables_with_long_form_dataset
146
+ x = self.x
147
+ y = self.y
148
+ color = self.color
149
+ if @data
150
+ x &&= @data[x] || x
151
+ y &&= @data[y] || y
152
+ color &&= @data[color] || color
153
+ end
154
+
155
+ # Validate inputs
156
+ [x, y, color].each do |input|
157
+ next if input.nil? || array?(input)
158
+ raise RuntimeError,
159
+ "Could not interpret input `#{input.inspect}`"
160
+ end
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
+
168
+ if x.nil? || y.nil?
169
+ setup_single_data
170
+ else
171
+ if orient == :v
172
+ groups, vals = x, y
173
+ else
174
+ groups, vals = y, x
175
+ end
176
+
177
+ if groups.respond_to?(:name)
178
+ @group_label = groups.name
179
+ end
180
+
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)
195
+ end
196
+
197
+ # TODO: Handle units
198
+ end
199
+ end
200
+
201
+ private def setup_single_data
202
+ raise NotImplementedError,
203
+ "Single data plot is not supported yet"
204
+ end
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
+
275
+ private def setup_colors
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
284
+ current_palette = Palette.default
285
+ if n_colors <= current_palette.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 }
294
+ else
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
305
+ end
306
+ else
307
+ case self.palette
308
+ when Hash
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] }
315
+ end
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
321
+ end
322
+
323
+ @colors = colors.map {|c| c.to_rgb }
324
+ lightness_values = @colors.map {|c| c.to_hsl.l }
325
+ lum = lightness_values.min * 0.6r
326
+ @gray = Colors::RGB.new(lum, lum, lum) # TODO: Use Charty::Gray
327
+ end
328
+
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
339
+ end
340
+
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)
376
+ end
377
+ end
378
+ end
379
+ end
380
+ end