charty 0.2.5 → 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.
@@ -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