charty 0.2.0 → 0.2.6

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 (78) 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 +177 -9
  8. data/Rakefile +4 -5
  9. data/charty.gemspec +10 -4
  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_images/hist_gruff.png +0 -0
  19. data/examples/sample_pyplot.ipynb +40 -38
  20. data/images/penguins_body_mass_g_flipper_length_mm_scatter_plot.png +0 -0
  21. data/images/penguins_body_mass_g_flipper_length_mm_species_scatter_plot.png +0 -0
  22. data/images/penguins_body_mass_g_flipper_length_mm_species_sex_scatter_plot.png +0 -0
  23. data/images/penguins_species_body_mass_g_bar_plot_h.png +0 -0
  24. data/images/penguins_species_body_mass_g_bar_plot_v.png +0 -0
  25. data/images/penguins_species_body_mass_g_box_plot_h.png +0 -0
  26. data/images/penguins_species_body_mass_g_box_plot_v.png +0 -0
  27. data/images/penguins_species_body_mass_g_sex_bar_plot_v.png +0 -0
  28. data/images/penguins_species_body_mass_g_sex_box_plot_v.png +0 -0
  29. data/lib/charty.rb +13 -1
  30. data/lib/charty/backend_methods.rb +8 -0
  31. data/lib/charty/backends.rb +26 -1
  32. data/lib/charty/backends/bokeh.rb +31 -31
  33. data/lib/charty/backends/{google_chart.rb → google_charts.rb} +75 -33
  34. data/lib/charty/backends/gruff.rb +14 -3
  35. data/lib/charty/backends/plotly.rb +774 -9
  36. data/lib/charty/backends/pyplot.rb +611 -34
  37. data/lib/charty/backends/rubyplot.rb +2 -2
  38. data/lib/charty/backends/unicode_plot.rb +79 -0
  39. data/lib/charty/dash_pattern_generator.rb +57 -0
  40. data/lib/charty/index.rb +213 -0
  41. data/lib/charty/linspace.rb +1 -1
  42. data/lib/charty/plot_methods.rb +254 -0
  43. data/lib/charty/plotter.rb +10 -10
  44. data/lib/charty/plotters.rb +12 -0
  45. data/lib/charty/plotters/abstract_plotter.rb +243 -0
  46. data/lib/charty/plotters/bar_plotter.rb +201 -0
  47. data/lib/charty/plotters/box_plotter.rb +79 -0
  48. data/lib/charty/plotters/categorical_plotter.rb +380 -0
  49. data/lib/charty/plotters/count_plotter.rb +7 -0
  50. data/lib/charty/plotters/estimation_support.rb +84 -0
  51. data/lib/charty/plotters/line_plotter.rb +300 -0
  52. data/lib/charty/plotters/random_support.rb +25 -0
  53. data/lib/charty/plotters/relational_plotter.rb +635 -0
  54. data/lib/charty/plotters/scatter_plotter.rb +80 -0
  55. data/lib/charty/plotters/vector_plotter.rb +6 -0
  56. data/lib/charty/statistics.rb +114 -0
  57. data/lib/charty/table.rb +161 -15
  58. data/lib/charty/table_adapters.rb +2 -0
  59. data/lib/charty/table_adapters/active_record_adapter.rb +17 -9
  60. data/lib/charty/table_adapters/base_adapter.rb +166 -0
  61. data/lib/charty/table_adapters/daru_adapter.rb +41 -3
  62. data/lib/charty/table_adapters/datasets_adapter.rb +17 -2
  63. data/lib/charty/table_adapters/hash_adapter.rb +143 -16
  64. data/lib/charty/table_adapters/narray_adapter.rb +25 -6
  65. data/lib/charty/table_adapters/nmatrix_adapter.rb +15 -5
  66. data/lib/charty/table_adapters/pandas_adapter.rb +163 -0
  67. data/lib/charty/util.rb +28 -0
  68. data/lib/charty/vector.rb +69 -0
  69. data/lib/charty/vector_adapters.rb +187 -0
  70. data/lib/charty/vector_adapters/array_adapter.rb +101 -0
  71. data/lib/charty/vector_adapters/daru_adapter.rb +163 -0
  72. data/lib/charty/vector_adapters/narray_adapter.rb +182 -0
  73. data/lib/charty/vector_adapters/nmatrix_adapter.rb +37 -0
  74. data/lib/charty/vector_adapters/numpy_adapter.rb +168 -0
  75. data/lib/charty/vector_adapters/pandas_adapter.rb +199 -0
  76. data/lib/charty/version.rb +1 -1
  77. metadata +121 -22
  78. data/.travis.yml +0 -10
@@ -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,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,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
@@ -0,0 +1,25 @@
1
+ module Charty
2
+ module Plotters
3
+ module RandomSupport
4
+ attr_reader :random
5
+
6
+ def random=(random)
7
+ @random = check_random(random)
8
+ end
9
+
10
+ module_function def check_random(random)
11
+ case random
12
+ when nil
13
+ Random.new
14
+ when Integer
15
+ Random.new(random)
16
+ when Random
17
+ random
18
+ else
19
+ raise ArgumentError,
20
+ "invalid value for random (%p for a generator or a seed value)" % value
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,635 @@
1
+ module Charty
2
+ module Plotters
3
+ class BaseMapper
4
+ def initialize(plotter, *params)
5
+ @plotter = plotter
6
+ initialize_mapping(*params)
7
+ end
8
+
9
+ attr_reader :plotter
10
+
11
+ def [](key, *args)
12
+ case key
13
+ when Array, Charty::Vector
14
+ key.map {|k| lookup_single_value(k, *args) }
15
+ else
16
+ lookup_single_value(key, *args)
17
+ end
18
+ end
19
+
20
+ private def categorical_lut_from_hash(levels, pairs, name)
21
+ missing_keys = levels - pairs.keys
22
+ unless missing_keys.empty?
23
+ raise ArgumentError,
24
+ "The `#{name}` hash is missing keys: %p" % missing_keys
25
+ end
26
+ pairs.dup
27
+ end
28
+
29
+ private def categorical_lut_from_array(levels, values, name)
30
+ if levels.length != values.length
31
+ raise ArgumentError,
32
+ "The `#{name}` array has the wrong number of values " +
33
+ "(%d for %d)." % [values.length, levels.length]
34
+ end
35
+ levels.zip(values).to_h
36
+ end
37
+ end
38
+
39
+ # TODO: This should be replaced with red-colors's Normalize feature
40
+ class SimpleNormalizer
41
+ def initialize(vmin=nil, vmax=nil)
42
+ @vmin = vmin
43
+ @vmax = vmax
44
+ end
45
+
46
+ attr_accessor :vmin, :vmax
47
+
48
+ def call(value, clip=nil)
49
+ scalar_p = false
50
+ vector_p = false
51
+ case value
52
+ when Charty::Vector
53
+ vector_p = true
54
+ value = value.to_a
55
+ when Array
56
+ # do nothing
57
+ else
58
+ scalar_p = true
59
+ value = [value]
60
+ end
61
+
62
+ @vmin = value.min if vmin.nil?
63
+ @vmax = value.max if vmax.nil?
64
+
65
+ result = value.map {|x| (x - vmin) / (vmax - vmin).to_f }
66
+
67
+ case
68
+ when scalar_p
69
+ result[0]
70
+ when vector_p
71
+ Charty::Vector.new(result, index: value.index)
72
+ else
73
+ result
74
+ end
75
+ end
76
+ end
77
+
78
+ class ColorMapper < BaseMapper
79
+ private def initialize_mapping(palette, order, norm)
80
+ @palette = palette
81
+ @order = order
82
+
83
+ if plotter.variables.key?(:color)
84
+ data = plotter.plot_data[:color]
85
+ end
86
+
87
+ if data && data.notnull.any?
88
+ @map_type = infer_map_type(palette, norm, @plotter.input_format, @plotter.var_types[:color])
89
+
90
+ case @map_type
91
+ when :numeric
92
+ @levels, @lookup_table, @norm, @cmap = numeric_mapping(data, palette, norm)
93
+ when :categorical
94
+ @cmap = nil
95
+ @norm = nil
96
+ @levels, @lookup_table = categorical_mapping(data, palette, order)
97
+ else
98
+ raise NotImplementedError,
99
+ "datetime color mapping is not supported"
100
+ end
101
+ end
102
+ end
103
+
104
+ private def numeric_mapping(data, palette, norm)
105
+ case palette
106
+ when Hash
107
+ levels = palette.keys.sort
108
+ colors = palette.values_at(*levels)
109
+ cmap = Colors::ListedColormap.new(colors)
110
+ lookup_table = palette.dup
111
+ else
112
+ levels = data.drop_na.unique_values
113
+ levels.sort!
114
+
115
+ palette ||= "ch:"
116
+ cmap = case palette
117
+ when Colors::Colormap
118
+ palette
119
+ else
120
+ Palette.new(palette, 256).to_colormap
121
+ end
122
+
123
+ case norm
124
+ when nil
125
+ norm = SimpleNormalizer.new
126
+ when Array
127
+ norm = SimpleNormalizer.new(*norm)
128
+ #when Colors::Normalizer
129
+ # TODO: Must support red-color's Normalize feature
130
+ else
131
+ raise ArgumentError,
132
+ "`color_norm` must be nil, Array, or Normalizer object"
133
+ end
134
+
135
+ # initialize norm
136
+ norm.(data.drop_na.to_a)
137
+
138
+ lookup_table = levels.map { |level|
139
+ [
140
+ level,
141
+ cmap[norm.(level)]
142
+ ]
143
+ }.to_h
144
+ end
145
+
146
+ return levels, lookup_table, norm, cmap
147
+ end
148
+
149
+ private def categorical_mapping(data, palette, order)
150
+ levels = data.categorical_order(order)
151
+ n_colors = levels.length
152
+
153
+ case palette
154
+ when Hash
155
+ lookup_table = categorical_lut_from_hash(levels, palette, :palette)
156
+ return levels, lookup_table
157
+
158
+ when nil
159
+ current_palette = Palette.default
160
+ if n_colors <= current_palette.n_colors
161
+ colors = Palette.new(current_palette.colors, n_colors).colors
162
+ else
163
+ colors = Palette.husl_colors(n_colors)
164
+ end
165
+
166
+ when Array
167
+ colors = palette
168
+
169
+ else
170
+ colors = Palette.new(palette, n_colors).colors
171
+ end
172
+
173
+ lookup_table = categorical_lut_from_array(levels, colors, :palette)
174
+ return levels, lookup_table
175
+ end
176
+
177
+ private def infer_map_type(palette, norm, input_format, var_type)
178
+ case
179
+ when false # palette is qualitative_palette
180
+ :categorical
181
+ when ! norm.nil?
182
+ :numeric
183
+ when palette.is_a?(Array),
184
+ palette.is_a?(Hash)
185
+ :categorical
186
+ when input_format == :wide
187
+ :categorical
188
+ else
189
+ var_type
190
+ end
191
+ end
192
+
193
+ attr_reader :palette, :norm, :levels, :lookup_table, :map_type
194
+
195
+ def inverse_lookup_table
196
+ lookup_table.invert
197
+ end
198
+
199
+ def lookup_single_value(key)
200
+ if @lookup_table.key?(key)
201
+ @lookup_table[key]
202
+ elsif @norm
203
+ # Use the colormap to interpolate between existing datapoints
204
+ begin
205
+ normed = @norm.(key)
206
+ @cmap[normed]
207
+ rescue ArgumentError, TypeError => err
208
+ if Util.nan?(key)
209
+ return "#000000"
210
+ else
211
+ raise err
212
+ end
213
+ end
214
+ end
215
+ end
216
+ end
217
+
218
+ class SizeMapper < BaseMapper
219
+ private def initialize_mapping(sizes, order, norm)
220
+ @sizes = sizes
221
+ @order = order
222
+ @norm = norm
223
+
224
+ return unless plotter.variables.key?(:size)
225
+
226
+ data = plotter.plot_data[:size]
227
+ return unless data.notnull.any?
228
+
229
+ @map_type = infer_map_type(sizes, norm, @plotter.var_types[:size])
230
+ case @map_type
231
+ when :numeric
232
+ @levels, @lookup_table, @norm = numeric_mapping(data, sizes, norm)
233
+ when :categorical
234
+ @levels, @lookup_table = categorical_mapping(data, sizes, order)
235
+ else
236
+ raise NotImplementedError,
237
+ "datetime color mapping is not supported"
238
+ end
239
+ end
240
+
241
+ private def infer_map_type(sizes, norm, var_type)
242
+ case
243
+ when ! norm.nil?
244
+ :numeric
245
+ when sizes.is_a?(Hash),
246
+ sizes.is_a?(Array)
247
+ :categorical
248
+ else
249
+ var_type
250
+ end
251
+ end
252
+
253
+ private def numeric_mapping(data, sizes, norm)
254
+ case sizes
255
+ when Hash
256
+ # The presence of a norm object overrides a dictionary of sizes
257
+ # in specifying a numeric mapping, so we need to process the
258
+ # dictionary here
259
+ levels = sizes.keys.sort
260
+ size_values = sizes.values
261
+ size_range = [size_values.min, size_values.max]
262
+ else
263
+ levels = Charty::Vector.new(data.unique_values).drop_na.to_a
264
+ levels.sort!
265
+
266
+ case sizes
267
+ when Range
268
+ size_range = [sizes.begin, sizes.end]
269
+ when nil
270
+ size_range = [0r, 1r]
271
+ else
272
+ raise ArgumentError,
273
+ "Unable to recognize the value for `sizes`: %p" % sizes
274
+ end
275
+ end
276
+
277
+ # Now we have the minimum and the maximum values of sizes
278
+ case norm
279
+ when nil
280
+ norm = SimpleNormalizer.new
281
+ sizes_scaled = norm.(levels)
282
+ # when Colors::Normalize
283
+ # TODO: Must support red-color's Normalize feature
284
+ else
285
+ raise ArgumentError,
286
+ "Unable to recognize the value for `norm`: %p" % norm
287
+ end
288
+
289
+ case sizes
290
+ when Hash
291
+ # do nothing
292
+ else
293
+ lo, hi = size_range
294
+ sizes = sizes_scaled.map {|x| lo + x * (hi - lo) }
295
+ lookup_table = levels.zip(sizes).to_h
296
+ end
297
+
298
+ return levels, lookup_table, norm
299
+ end
300
+
301
+ private def categorical_mapping(data, sizes, order)
302
+ levels = data.categorical_order(order)
303
+
304
+ case sizes
305
+ when Hash
306
+ lookup_table = categorical_lut_from_hash(levels, sizes, :sizes)
307
+ return levels, lookup_table
308
+
309
+ when Array
310
+ # nothing to do
311
+
312
+ when Range, nil
313
+ # Reverse values to use the largest size for the first category
314
+ size_range = sizes || (0r .. 1r)
315
+ sizes = Linspace.new(size_range, levels.length).reverse_each.to_a
316
+ else
317
+ raise ArgumentError,
318
+ "Unable to recognize the value for `sizes`: %p" % sizes
319
+ end
320
+
321
+ lookup_table = categorical_lut_from_array(levels, sizes, :sizes)
322
+ return levels, lookup_table
323
+ end
324
+
325
+ attr_reader :palette, :order, :norm, :levels, :lookup_table, :map_type
326
+
327
+ def lookup_single_value(key)
328
+ if @lookup_table.key?(key)
329
+ @lookup_table[key]
330
+ else
331
+ normed = @norm.(key) || Float::NAN
332
+ size_values = @lookup_table.values
333
+ min, max = size_values.min, size_values.max
334
+ min + normed * (max - min)
335
+ end
336
+ end
337
+ end
338
+
339
+ class StyleMapper < BaseMapper
340
+ private def initialize_mapping(markers, dashes, order)
341
+ @markers = markers
342
+ @dashes = dashes
343
+ @order = order
344
+
345
+ return unless plotter.variables.key?(:style)
346
+
347
+ data = plotter.plot_data[:style]
348
+ return unless data.notnull.any?
349
+
350
+ @levels = data.categorical_order(order)
351
+
352
+ markers = map_attributes(markers, @levels, unique_markers(@levels.length), :markers)
353
+ dashes = map_attributes(dashes, @levels, unique_dashes(@levels.length), :dashes)
354
+
355
+ @lookup_table = @levels.map {|key|
356
+ record = {
357
+ marker: markers[key],
358
+ dashes: dashes[key]
359
+ }
360
+ record.compact!
361
+ [key, record]
362
+ }.to_h
363
+ end
364
+
365
+ MARKER_NAMES = [
366
+ :circle, :x, :square, :cross, :diamond, :star_diamond,
367
+ :triangle_up, :star_square, :triangle_down, :hexagon, :star, :pentagon,
368
+ ].freeze
369
+
370
+ private def unique_markers(n)
371
+ if n > MARKER_NAMES.length
372
+ raise ArgumentError,
373
+ "Too many markers are required (%p for %p)" % [n, MARKER_NAMES.length]
374
+ end
375
+ MARKER_NAMES[0, n]
376
+ end
377
+
378
+ private def unique_dashes(n)
379
+ DashPatternGenerator.take(n)
380
+ end
381
+
382
+ private def map_attributes(vals, levels, defaults, attr)
383
+ case vals
384
+ when true
385
+ return levels.zip(defaults).to_h
386
+ when Hash
387
+ missing_keys = lavels - vals.keys
388
+ unless missing_keys.empty?
389
+ raise ArgumentError,
390
+ "The `%s` levels are missing values: %p" % [attr, missing_keys]
391
+ end
392
+ return vals
393
+ when Array, Enumerable
394
+ if levels.length != vals.length
395
+ raise ArgumentError,
396
+ "%he `%s` argument has the wrong number of values" % attr
397
+ end
398
+ return levels.zip(vals).to_h
399
+ when nil, false
400
+ return {}
401
+ else
402
+ raise ArgumentError,
403
+ "Unable to recognize the value for `%s`: %p" % [attr, vals]
404
+ end
405
+ end
406
+
407
+ attr_reader :palette, :order, :norm, :lookup_table, :levels
408
+
409
+ def inverse_lookup_table(attr)
410
+ lookup_table.map { |k, v| [v[attr], k] }.to_h
411
+ end
412
+
413
+ def lookup_single_value(key, attr=nil)
414
+ case attr
415
+ when nil
416
+ @lookup_table[key]
417
+ else
418
+ @lookup_table[key][attr]
419
+ end
420
+ end
421
+ end
422
+
423
+ class RelationalPlotter < AbstractPlotter
424
+ def flat_structure
425
+ {
426
+ x: :index,
427
+ y: :values
428
+ }
429
+ end
430
+
431
+ def initialize(x, y, color, style, size, data: nil, **options, &block)
432
+ super(x, y, color, data: data, **options, &block)
433
+
434
+ self.style = style
435
+ self.size = size
436
+
437
+ setup_variables
438
+ end
439
+
440
+ attr_reader :style, :size, :units
441
+
442
+ attr_reader :color_norm
443
+
444
+ attr_reader :sizes, :size_order, :size_norm
445
+
446
+ attr_reader :markers, :dashes, :style_order
447
+
448
+ attr_reader :legend
449
+
450
+ def units=(units)
451
+ @units = check_dimension(units, :units)
452
+ end
453
+
454
+ def style=(val)
455
+ @style = check_dimension(val, :style)
456
+ end
457
+
458
+ def size=(val)
459
+ @size = check_dimension(val, :size)
460
+ end
461
+
462
+ def color_norm=(val)
463
+ unless val.nil?
464
+ raise NotImplementedError,
465
+ "Specifying color_norm is not supported yet"
466
+ end
467
+ end
468
+
469
+ def sizes=(val)
470
+ # NOTE: the value check will be perfomed in SizeMapper
471
+ @sizes = val
472
+ end
473
+
474
+ def size_order=(val)
475
+ unless val.nil?
476
+ raise NotImplementedError,
477
+ "Specifying size_order is not supported yet"
478
+ end
479
+ end
480
+
481
+ def size_norm=(val)
482
+ unless val.nil?
483
+ raise NotImplementedError,
484
+ "Specifying size_order is not supported yet"
485
+ end
486
+ end
487
+
488
+ def markers=(val)
489
+ @markers = check_markers(val)
490
+ end
491
+
492
+ private def check_markers(val)
493
+ # TODO
494
+ val
495
+ end
496
+
497
+ def dashes=(val)
498
+ @dashes = check_dashes(val)
499
+ end
500
+
501
+ private def check_dashes(val)
502
+ # TODO
503
+ val
504
+ end
505
+
506
+ def style_order=(val)
507
+ unless val.nil?
508
+ raise NotImplementedError,
509
+ "Specifying style_order is not supported yet"
510
+ end
511
+ end
512
+
513
+ def legend=(val)
514
+ case val
515
+ when :auto, :brief, :full, false
516
+ @legend = val
517
+ when "auto", "brief", "full"
518
+ @legend = val.to_sym
519
+ else
520
+ raise ArgumentError,
521
+ "invalid value of legend (%p for :auto, :brief, :full, or false)" % val
522
+ end
523
+ end
524
+
525
+ attr_reader :input_format, :plot_data, :variables, :var_types
526
+
527
+ private def setup_variables
528
+ if x.nil? && y.nil?
529
+ @input_format = :wide
530
+ setup_variables_with_wide_form_dataset
531
+ else
532
+ @input_format = :long
533
+ setup_variables_with_long_form_dataset
534
+ end
535
+
536
+ @var_types = @plot_data.columns.map { |k|
537
+ [k, variable_type(@plot_data[k], :categorical)]
538
+ }.to_h
539
+ end
540
+
541
+ private def setup_variables_with_wide_form_dataset
542
+ unless color.nil? && style.nil? && size.nil?
543
+ vars = []
544
+ vars << "color" unless color.nil?
545
+ vars << "style" unless style.nil?
546
+ vars << "size" unless size.nil?
547
+ raise ArgumentError,
548
+ "Unable to assign the following variables in wide-form data: " +
549
+ vars.join(", ")
550
+ end
551
+
552
+ if data.nil? || data.empty?
553
+ @plot_data = Charty::Table.new({})
554
+ @variables = {}
555
+ return
556
+ end
557
+
558
+ flat = data.is_a?(Charty::Vector)
559
+ if flat
560
+ @plot_data = {}
561
+ @variables = {}
562
+
563
+ [:x, :y].each do |var|
564
+ case self.flat_structure[var]
565
+ when :index
566
+ @plot_data[var] = data.index.to_a
567
+ @variables[var] = data.index.name
568
+ when :values
569
+ @plot_data[var] = data.to_a
570
+ @variables[var] = data.name
571
+ end
572
+ end
573
+
574
+ @plot_data = Charty::Table.new(@plot_data)
575
+ else
576
+ raise NotImplementedError,
577
+ "wide-form input is not supported"
578
+ end
579
+ end
580
+
581
+ private def setup_variables_with_long_form_dataset
582
+ self.data = {} if data.nil?
583
+
584
+ plot_data = {}
585
+ variables = {}
586
+
587
+ {
588
+ x: self.x,
589
+ y: self.y,
590
+ color: self.color,
591
+ style: self.style,
592
+ size: self.size,
593
+ units: self.units
594
+ }.each do |key, val|
595
+ next if val.nil?
596
+
597
+ if data.column?(val)
598
+ plot_data[key] = data[val]
599
+ variables[key] = val
600
+ else
601
+ case val
602
+ when Charty::Vector
603
+ plot_data[key] = val
604
+ variables[key] = val.name
605
+ else
606
+ raise ArgumentError,
607
+ "Could not interpret value %p for parameter %p" % [val, key]
608
+ end
609
+ end
610
+ end
611
+
612
+ @plot_data = Charty::Table.new(plot_data)
613
+ @variables = variables.select do |var, name|
614
+ @plot_data[var].notnull.any?
615
+ end
616
+ end
617
+
618
+ private def annotate_axes(backend)
619
+ # TODO
620
+ end
621
+
622
+ private def map_color(palette: nil, order: nil, norm: nil)
623
+ @color_mapper = ColorMapper.new(self, palette, order, norm)
624
+ end
625
+
626
+ private def map_size(sizes: nil, order: nil, norm: nil)
627
+ @size_mapper = SizeMapper.new(self, sizes, order, norm)
628
+ end
629
+
630
+ private def map_style(markers: nil, dashes: nil, order: nil)
631
+ @style_mapper = StyleMapper.new(self, markers, dashes, order)
632
+ end
633
+ end
634
+ end
635
+ end