charty 0.2.5 → 0.2.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,57 @@
1
+ module Charty
2
+ module DashPatternGenerator
3
+ NAMED_PATTERNS = {
4
+ solid: "",
5
+ dash: [4, 1.5],
6
+ dot: [1, 1],
7
+ dashdot: [3, 1.25, 1.5, 1.25],
8
+ longdashdot: [5, 1, 1, 1],
9
+ }.freeze
10
+
11
+ def self.valid_name?(name)
12
+ name = case name
13
+ when Symbol, String
14
+ name.to_sym
15
+ else
16
+ name.to_str.to_sym
17
+ end
18
+ NAMED_PATTERNS.key?(name)
19
+ end
20
+
21
+ def self.pattern_to_name(pattern)
22
+ NAMED_PATTERNS.each do |key, val|
23
+ return key if pattern == val
24
+ end
25
+ nil
26
+ end
27
+
28
+ def self.each
29
+ return enum_for(__method__) unless block_given?
30
+
31
+ NAMED_PATTERNS.each_value do |pattern|
32
+ yield pattern
33
+ end
34
+
35
+ m = 3
36
+ while true
37
+ # Long and short dash combinations
38
+ a = [3, 1.25].repeated_combination(m).to_a[1..-2].reverse
39
+ b = [4, 1].repeated_combination(m).to_a[1..-2]
40
+
41
+ # Interleave these combinations
42
+ segment_list = a.zip(b).flatten(1)
43
+
44
+ # Insert the gaps
45
+ segment_list.each do |segment|
46
+ gap = segment.min
47
+ pattern = segment.map {|seg| [seg, gap] }.flatten
48
+ yield pattern
49
+ end
50
+
51
+ m += 1
52
+ end
53
+ end
54
+
55
+ extend Enumerable
56
+ end
57
+ end
data/lib/charty/index.rb CHANGED
@@ -49,7 +49,7 @@ module Charty
49
49
  return index.union(other) if index
50
50
  end
51
51
 
52
- Index.new(to_a.union(other.to_a))
52
+ Index.new(to_a.union(other.to_a), name: name)
53
53
  end
54
54
  end
55
55
 
@@ -130,6 +130,76 @@ module Charty
130
130
  )
131
131
  end
132
132
 
133
+ # Line plot
134
+ #
135
+ # @param x [vector-like object, key in data]
136
+ # @param y [vector-like object, key in data]
137
+ # @param color [vector-like object, key in data]
138
+ # @param style [vector-like object, key in data]
139
+ # @param size [vector-like object, key in data]
140
+ # @param data [table-like object]
141
+ # @param key_color [color object]
142
+ # @param palette [String,Array<Numeric>,Palette]
143
+ # @param color_order [Array<String>,Array<Symbol>]
144
+ # @param color_norm
145
+ # @param sizes [Array, Hash]
146
+ # @param size_order [Array]
147
+ # @param size_norm
148
+ # @param dashes [true, false, Array, Hash]
149
+ # @param markers [true, false, Array, Hash]
150
+ # @param style_order [Array]
151
+ # @param units [vector-like object, key in data]
152
+ # @param estimator [:mean]
153
+ # @param n_boot [Integer]
154
+ # @param random [Integer, Random, nil]
155
+ # @param sort [true, false]
156
+ # @param err_style [:band, :bars]
157
+ # @param err_params [Hash]
158
+ # @param error_bar
159
+ # @param x_scale [:linear, :log10]
160
+ # @param y_scale [:linear, :log10]
161
+ # @param legend [:auto, :brief, :full, false]
162
+ # How to draw legend. If :brief, numeric color and size variables
163
+ # will be represented with a sample of evenly spaced values. If
164
+ # :full, every group will get an entry in the legend. If :auto,
165
+ # choose between brief or full representation based on number of
166
+ # levels. If false, no legend data is added and no legend is drawn.
167
+ def line_plot(x: nil, y: nil, color: nil, style: nil, size: nil,
168
+ data: nil, key_color: nil, palette: nil, color_order: nil,
169
+ color_norm: nil, sizes: nil, size_order: nil, size_norm: nil,
170
+ markers: nil, dashes: true, style_order: nil,
171
+ units: nil, estimator: :mean, n_boot: 1000, random: nil,
172
+ sort: true, err_style: :band, err_params: nil, error_bar: [:ci, 95],
173
+ x_scale: :linear, y_scale: :linear, legend: :auto, **options, &block)
174
+ Plotters::LinePlotter.new(
175
+ data: data,
176
+ variables: { x: x, y: y, color: color, style: style, size: size },
177
+ key_color: key_color,
178
+ palette: palette,
179
+ color_order: color_order,
180
+ color_norm: color_norm,
181
+ sizes: sizes,
182
+ size_order: size_order,
183
+ size_norm: size_norm,
184
+ markers: markers,
185
+ dashes: dashes,
186
+ style_order: style_order,
187
+ units: units,
188
+ estimator: estimator,
189
+ n_boot: n_boot,
190
+ random: random,
191
+ sort: sort,
192
+ err_style: err_style,
193
+ err_params: err_params,
194
+ error_bar: error_bar,
195
+ x_scale: x_scale,
196
+ y_scale: y_scale,
197
+ legend: legend,
198
+ **options,
199
+ &block
200
+ )
201
+ end
202
+
133
203
  # Scatter plot
134
204
  #
135
205
  # @param x [vector-like object, key in data]
@@ -146,7 +216,7 @@ module Charty
146
216
  # @param size_order [Array]
147
217
  # @param size_norm
148
218
  # @param markers [true, false, Array, Hash]
149
- # @param marker_order [Array]
219
+ # @param style_order [Array]
150
220
  # @param alpha [scalar number]
151
221
  # Propotional opacity of the points.
152
222
  # @param legend [:auto, :brief, :full, false]
@@ -158,7 +228,7 @@ module Charty
158
228
  def scatter_plot(x: nil, y: nil, color: nil, style: nil, size: nil,
159
229
  data: nil, key_color: nil, palette: nil, color_order: nil,
160
230
  color_norm: nil, sizes: nil, size_order: nil, size_norm: nil,
161
- markers: true, marker_order: nil, alpha: nil, legend: :auto,
231
+ markers: true, style_order: nil, alpha: nil, legend: :auto,
162
232
  **options, &block)
163
233
  Plotters::ScatterPlotter.new(
164
234
  data: data,
@@ -171,7 +241,7 @@ module Charty
171
241
  size_order: size_order,
172
242
  size_norm: size_norm,
173
243
  markers: markers,
174
- marker_order: marker_order,
244
+ style_order: style_order,
175
245
  alpha: alpha,
176
246
  legend: legend,
177
247
  **options,
@@ -9,3 +9,4 @@ require_relative "plotters/count_plotter"
9
9
  require_relative "plotters/vector_plotter"
10
10
  require_relative "plotters/relational_plotter"
11
11
  require_relative "plotters/scatter_plotter"
12
+ require_relative "plotters/line_plotter"
@@ -8,12 +8,28 @@ module Charty
8
8
  self.data = data
9
9
  self.palette = palette
10
10
  substitute_options(options)
11
+
12
+ @var_levels = {}
13
+ @var_ordered = {x: false, y: false}
14
+
11
15
  yield self if block_given?
12
16
  end
13
17
 
14
18
  attr_reader :data, :x, :y, :color
15
19
  attr_reader :color_order, :key_color, :palette
16
20
 
21
+ def var_levels
22
+ variables.each_key do |var|
23
+ # TODO: Move mappers from RelationalPlotter to here,
24
+ # and remove the use of instance_variable_get
25
+ if instance_variable_defined?(:"@#{var}_mapper")
26
+ mapper = instance_variable_get(:"@#{var}_mapper")
27
+ @var_levels[var] = mapper.levels
28
+ end
29
+ end
30
+ @var_levels
31
+ end
32
+
17
33
  def inspect
18
34
  "#<#{self.class}:0x%016x>" % self.object_id
19
35
  end
@@ -22,6 +38,8 @@ module Charty
22
38
  @data = case data
23
39
  when nil, Charty::Table
24
40
  data
41
+ when method(:array?)
42
+ Charty::Vector.new(data)
25
43
  else
26
44
  Charty::Table.new(data)
27
45
  end
@@ -40,11 +58,7 @@ module Charty
40
58
  end
41
59
 
42
60
  def color_order=(color_order)
43
- #@color_order = XXX
44
- unless color_order.nil?
45
- raise NotImplementedError,
46
- "Specifying color_order is not supported yet"
47
- end
61
+ @color_order = color_order
48
62
  end
49
63
 
50
64
  # TODO: move to categorical_plotter
@@ -144,13 +158,59 @@ module Charty
144
158
  end
145
159
 
146
160
  private def remove_na!(ary)
147
- ary.reject! do |x|
148
- next true if x.nil?
149
- x.respond_to?(:nan?) && x.nan?
150
- end
161
+ ary.reject! {|x| Util.missing?(x) }
151
162
  ary
152
163
  end
153
164
 
165
+ private def each_subset(grouping_vars, reverse: false, processed: false, by_facet: true, allow_empty: false, drop_na: true)
166
+ case grouping_vars
167
+ when nil
168
+ grouping_vars = []
169
+ when String, Symbol
170
+ grouping_vars = [grouping_vars.to_sym]
171
+ end
172
+
173
+ if by_facet
174
+ [:col, :row].each do |facet_var|
175
+ grouping_vars << facet_var if variables.key?(facet_var)
176
+ end
177
+ end
178
+
179
+ grouping_vars = grouping_vars.select {|var| variables.key?(var) }
180
+
181
+ data = processed ? processed_data : plot_data
182
+ data = data.drop_na if drop_na
183
+
184
+ levels = var_levels.dup
185
+
186
+ [:x, :y].each do |axis|
187
+ levels[axis] = plot_data[axis].categorical_order()
188
+ if processed
189
+ # TODO: perform inverse conversion of axis scaling here
190
+ end
191
+ end
192
+
193
+ if not grouping_vars.empty?
194
+ grouped = data.group_by(grouping_vars, sort: false)
195
+ grouped.each_group do |group_key, group_data|
196
+ next if group_data.empty? && !allow_empty
197
+
198
+ yield(grouping_vars.zip(group_key).to_h, group_data)
199
+ end
200
+ else
201
+ yield({}, data.dup)
202
+ end
203
+ end
204
+
205
+ def processed_data
206
+ @processed_data ||= calculate_processed_data
207
+ end
208
+
209
+ private def calculate_processed_data
210
+ # TODO: axis scaling support
211
+ plot_data
212
+ end
213
+
154
214
  def save(filename, **kwargs)
155
215
  backend = Backends.current
156
216
  backend.begin_figure
@@ -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