plotrb 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,346 @@
1
+ module Plotrb
2
+
3
+ # Scales are functions that transform a domain of data values to a range of
4
+ # visual values.
5
+ # See {https://github.com/trifacta/vega/wiki/Scales}
6
+ class Scale
7
+
8
+ include ::Plotrb::Base
9
+
10
+ TYPES = %i(linear log pow sqrt quantile quantize threshold ordinal time utc)
11
+
12
+ TYPES.each do |t|
13
+ define_singleton_method(t) do |&block|
14
+ ::Plotrb::Scale.new(t, &block)
15
+ end
16
+ end
17
+
18
+ # @!attributes type
19
+ # @return [Symbol] the type of the scale
20
+ SCALE_PROPERTIES = [:name, :type, :domain, :domain_min, :domain_max, :range,
21
+ :range_min, :range_max, :reverse, :round]
22
+
23
+ add_attributes *SCALE_PROPERTIES
24
+
25
+ RANGE_LITERALS = %i(width height shapes colors more_colors)
26
+ TIME_SCALE_NICE = %i(second minute hour day week month year)
27
+
28
+ def initialize(type=:linear, &block)
29
+ @type = type
30
+ case @type
31
+ when :ordinal
32
+ set_ordinal_scale_attributes
33
+ when :time, :utc
34
+ set_time_scale_attributes
35
+ else
36
+ set_quantitative_scale_attributes
37
+ end
38
+ set_common_scale_attributes
39
+ ::Plotrb::Kernel.scales << self
40
+ self.instance_eval(&block) if block_given?
41
+ self
42
+ end
43
+
44
+ def type
45
+ @type
46
+ end
47
+
48
+ def method_missing(method, *args, &block)
49
+ case method.to_s
50
+ when /in_(\w+)s$/ # set @nice for time and utc type, eg. in_seconds
51
+ if TIME_SCALE_NICE.include?($1.to_sym)
52
+ self.nice($1.to_sym, &block)
53
+ else
54
+ super
55
+ end
56
+ when /to_(\w+)$/ # set range literals, eg. to_more_colors
57
+ if RANGE_LITERALS.include?($1.to_sym)
58
+ self.range($1.to_sym, &block)
59
+ else
60
+ super
61
+ end
62
+ else
63
+ super
64
+ end
65
+ end
66
+
67
+ private
68
+
69
+ def set_common_scale_attributes
70
+ # @!attributes name
71
+ # @return [String] the name of the scale
72
+ # @!attributes domain
73
+ # @return [Array(Numeric, Numeric), Array, String] the domain of the
74
+ # scale, representing the set of data values
75
+ # @!attributes domain_min
76
+ # @return [Numeric, String] the minimum value in the scale domain
77
+ # @!attributes domain_max
78
+ # @return [Numeric, String] the maximum value in the scale domain
79
+ # @!attributes range
80
+ # @return [Array(Numeric, Numeric), Array, String] the range of the
81
+ # scale, representing the set of visual values
82
+ # @!attributes range_min
83
+ # @return [Numeric, String] the minimum value in the scale range
84
+ # @!attributes range_max
85
+ # @return [Numeric, String] the maximum value in the scale range
86
+ # @!attributes reverse
87
+ # @return [Boolean] whether flips the scale range
88
+ # @!attributes round
89
+ # @return [Boolean] whether rounds numeric output values to integers
90
+ define_single_val_attributes(:name, :domain, :domain_max, :domain_min,
91
+ :range, :range_max, :range_min)
92
+ define_boolean_attributes(:reverse, :round)
93
+ self.singleton_class.class_eval {
94
+ alias_method :from, :domain
95
+ alias_method :to, :range
96
+ }
97
+ end
98
+
99
+ def set_ordinal_scale_attributes
100
+ # @!attributes points
101
+ # @return [Boolean] whether distributes the ordinal values over a
102
+ # quantitative range at uniformly spaced points or bands
103
+ # @!attributes padding
104
+ # @return [Numeric] the spacing among ordinal elements in the scale range
105
+ # @!attributes sort
106
+ # @return [Boolean] whether values in the scale domain will be sorted
107
+ # according to their natural order
108
+ add_attributes(:points, :padding, :sort)
109
+ define_boolean_attributes(:points, :sort)
110
+ define_single_val_attribute(:padding)
111
+ define_singleton_method(:bands) do |&block|
112
+ @points = false
113
+ self.instance_eval(&block) if block
114
+ self
115
+ end
116
+ define_singleton_method(:bands?) do
117
+ !@points
118
+ end
119
+ self.singleton_class.class_eval {
120
+ alias_method :as_bands, :bands
121
+ alias_method :as_bands?, :bands?
122
+ alias_method :as_points, :points
123
+ alias_method :as_points?, :points?
124
+ }
125
+ end
126
+
127
+ def set_time_scale_attributes
128
+ # @!attributes clamp
129
+ # @return [Boolean] whether clamps values that exceed the data domain
130
+ # to either to minimum or maximum range value
131
+ # @!attributes nice
132
+ # @return [Symbol, Boolean, nil] scale domain in a more human-friendly
133
+ # value range
134
+ add_attributes(:clamp, :nice)
135
+ define_boolean_attribute(:clamp)
136
+ define_single_val_attribute(:nice)
137
+ end
138
+
139
+ def set_quantitative_scale_attributes
140
+ # @!attributes clamp
141
+ # @return [Boolean] whether clamps values that exceed the data domain
142
+ # to either to minimum or maximum range value
143
+ # @!attributes nice
144
+ # @return [Boolean] scale domain in a more human-friendly
145
+ # value range
146
+ # @!attributes exponent
147
+ # @return [Numeric] the exponent of the scale transformation
148
+ # @!attributes zero
149
+ # @return [Boolean] whether zero baseline value is included
150
+ add_attributes(:clamp, :exponent, :nice, :zero)
151
+ define_boolean_attributes(:clamp, :nice, :zero)
152
+ define_single_val_attribute(:exponent)
153
+ define_singleton_method(:exclude_zero) do |&block|
154
+ @zero = false
155
+ self.instance_eval(&block) if block
156
+ self
157
+ end
158
+ define_singleton_method(:exclude_zero?) do
159
+ !@zero
160
+ end
161
+ self.singleton_class.class_eval {
162
+ alias_method :nicely, :nice
163
+ alias_method :nicely?, :nice?
164
+ alias_method :include_zero, :zero
165
+ alias_method :include_zero?, :zero?
166
+ alias_method :in_exponent, :exponent
167
+ }
168
+ end
169
+
170
+ def attribute_post_processing
171
+ process_name
172
+ process_type
173
+ process_domain
174
+ process_domain_min
175
+ process_domain_max
176
+ process_range
177
+ end
178
+
179
+ def process_name
180
+ if @name.nil? || @name.strip.empty?
181
+ raise ArgumentError, 'Name missing for Scale object'
182
+ end
183
+ if ::Plotrb::Kernel.duplicate_scale?(@name)
184
+ raise ArgumentError, 'Duplicate names for Scale object'
185
+ end
186
+ end
187
+
188
+ def process_type
189
+ unless TYPES.include?(@type)
190
+ raise ArgumentError, 'Invalid Scale type'
191
+ end
192
+ end
193
+
194
+ def process_domain
195
+ return unless @domain
196
+ case @domain
197
+ when String
198
+ @domain = get_data_ref_from_string(@domain)
199
+ when ::Plotrb::Data
200
+ @domain = get_data_ref_from_data(@domain)
201
+ when Array
202
+ if @domain.all? { |d| is_data_ref?(d) }
203
+ fields = @domain.collect { |d| get_data_ref_from_string(d) }
204
+ @domain = {:fields => fields}
205
+ else
206
+ # leave as it is
207
+ end
208
+ when ::Plotrb::Scale::DataRef
209
+ # leave as it is
210
+ else
211
+ raise ArgumentError, 'Unsupported Scale domain type'
212
+ end
213
+ end
214
+
215
+ def process_domain_min
216
+ # only for quantitative domain
217
+ return unless @domain_min && !%i(ordinal time utc).include?(@type)
218
+ case @domain_min
219
+ when String
220
+ @domain_min = get_data_ref_from_string(@domain_min)
221
+ when ::Plotrb::Data
222
+ @domain_min = get_data_ref_from_data(@domain_min)
223
+ when Array
224
+ if @domain_min.all? { |d| is_data_ref?(d) }
225
+ fields = @domain_min.collect { |d| get_data_ref_from_string(d) }
226
+ @domain_min = {:fields => fields}
227
+ else
228
+ raise ArgumentError, 'Unsupported Scale domain_min type'
229
+ end
230
+ when Numeric
231
+ # leave as it is
232
+ else
233
+ raise ArgumentError, 'Unsupported Scale domain_min type'
234
+ end
235
+ end
236
+
237
+ def process_domain_max
238
+ # only for quantitative domain
239
+ return unless @domain_max && !%i(ordinal time utc).include?(@type)
240
+ case @domain_max
241
+ when String
242
+ @domain_max = get_data_ref_from_string(@domain_max)
243
+ when ::Plotrb::Data
244
+ @domain_max = get_data_ref_from_data(@domain_max)
245
+ when Array
246
+ if @domain_max.all? { |d| is_data_ref?(d) }
247
+ fields = @domain_max.collect { |d| get_data_ref_from_string(d) }
248
+ @domain_max = {:fields => fields}
249
+ else
250
+ raise ArgumentError, 'Unsupported Scale domain_max type'
251
+ end
252
+ when Numeric
253
+ # leave as it is
254
+ else
255
+ raise ArgumentError, 'Unsupported Scale domain_max type'
256
+ end
257
+ end
258
+
259
+ def get_data_ref_from_string(ref)
260
+ source, field = ref.split('.', 2)
261
+ data = ::Plotrb::Kernel.find_data(source)
262
+ if field.nil?
263
+ if data && data.values.is_a?(Array)
264
+ ::Plotrb::Scale::DataRef.new.data(source).field('data')
265
+ else
266
+ ::Plotrb::Scale::DataRef.new.data(source).field('index')
267
+ end
268
+ elsif field == 'index'
269
+ ::Plotrb::Scale::DataRef.new.data(source).field('index')
270
+ else
271
+ if data.extra_fields.include?(field.to_sym)
272
+ ::Plotrb::Scale::DataRef.new.data(source).field(field)
273
+ else
274
+ ::Plotrb::Scale::DataRef.new.data(source).field("data.#{field}")
275
+ end
276
+ end
277
+ end
278
+
279
+ def get_data_ref_from_data(data)
280
+ if data.values.is_a?(Array)
281
+ ::Plotrb::Scale::DataRef.new.data(data.name).field('data')
282
+ else
283
+ ::Plotrb::Scale::DataRef.new.data(data.name).field('index')
284
+ end
285
+ end
286
+
287
+ def is_data_ref?(ref)
288
+ return false unless ref.is_a?(String)
289
+ source, _ = ref.split('.', 2)
290
+ not ::Plotrb::Kernel.find_data(source).nil?
291
+ end
292
+
293
+ def process_range
294
+ return unless @range
295
+ case @range
296
+ when String, Symbol
297
+ @range = range_literal(@range)
298
+ when Array
299
+ #leave as it is
300
+ else
301
+ raise ArgumentError, 'Unsupported Scale range type'
302
+ end
303
+ end
304
+
305
+ def range_literal(literal)
306
+ case literal
307
+ when :colors
308
+ :category10
309
+ when :more_colors
310
+ :category20
311
+ when :width, :height, :shapes, :category10, :category20
312
+ literal
313
+ else
314
+ raise ArgumentError, 'Invalid Scale range'
315
+ end
316
+ end
317
+
318
+ # A data reference specifies the field for a given scale property
319
+ class DataRef
320
+
321
+ include ::Plotrb::Base
322
+
323
+ # @!attributes data
324
+ # @return [String] the name of a data set
325
+ # @!attributes field
326
+ # @return [String] A field from which to pull a data values
327
+ add_attributes :data, :field
328
+
329
+ # TODO: Support group
330
+ def initialize(&block)
331
+ define_single_val_attributes(:data, :field)
332
+ self.instance_eval(&block) if block
333
+ self
334
+ end
335
+
336
+ private
337
+
338
+ def attribute_post_processing
339
+
340
+ end
341
+
342
+ end
343
+
344
+ end
345
+
346
+ end
@@ -0,0 +1,197 @@
1
+ #--
2
+ # simple.rb: Shortcuts for making some simple plots.
3
+ # Copyright (c) 2013 Colin J. Fuller and the Ruby Science Foundation
4
+ # All rights reserved.
5
+ #
6
+ # Redistribution and use in source and binary forms, with or without
7
+ # modification, are permitted provided that the following conditions
8
+ # are met:
9
+ # - Redistributions of source code must retain the above copyright
10
+ # notice, this list of conditions and the following disclaimer.
11
+ #
12
+ # - Redistributions in binary form must reproduce the above copyright
13
+ # notice, this list of conditions and the following disclaimer in the
14
+ # documentation and/or other materials provided with the
15
+ # distribution.
16
+ #
17
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19
+ # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20
+ # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21
+ # HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
22
+ # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
23
+ # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
24
+ # OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
25
+ # AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
26
+ # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
27
+ # WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28
+ # POSSIBILITY OF SUCH DAMAGE.
29
+ #++
30
+
31
+ require 'plotrb'
32
+
33
+ module Plotrb
34
+ module Simple
35
+
36
+ SCATTER_DATA_NAME = 'scatter'
37
+ SCATTER_X_SCALE_NAME = 'scatter_x'
38
+ SCATTER_Y_SCALE_NAME = 'scatter_y'
39
+
40
+ #
41
+ # Data name used by scatter plot
42
+ #
43
+ def self.scatter_data_name
44
+ SCATTER_DATA_NAME
45
+ end
46
+
47
+ #
48
+ # Scale name used by scatter plot for x axis
49
+ #
50
+ def self.scatter_x_scale_name
51
+ SCATTER_X_SCALE_NAME
52
+ end
53
+
54
+ #
55
+ # Scale name used by scatter plot for y axis
56
+ #
57
+ def self.scatter_y_scale_name
58
+ SCATTER_Y_SCALE_NAME
59
+ end
60
+
61
+ #
62
+ # Generate a simple 2d scatter plot.
63
+ #
64
+ # @param [NMatrix, Array] x the x datapoints; if a single row, will be used
65
+ # for all y dataseries, if multiple rows, each row of x will be used for
66
+ # the corresponding row of y
67
+ # @param [NMatrix, Array] y the y datapoints. Can be a single dimensional array,
68
+ # or 2D with multiple series in rows; column dimension should match the
69
+ # number of elements in x.
70
+ # @param [String, Array<String>] symbol the type of symbol to be used to
71
+ # plot the points. Can be any symbol Vega understands: circle, square,
72
+ # cross, diamond, triangle-up, triangle-down. If a single String is
73
+ # provided, this will be used for all the points. If an Array of Strings
74
+ # is provided, each symbol in the array will be used for the corresponding
75
+ # data series (row) in y. Default: 'circle'
76
+ # @param [String, Array<String>] color the color to be used to plot the
77
+ # points. If a single String is provided, this will be used for all the
78
+ # points. If an Array of Strings is provided, each color in the array will
79
+ # be used for the corresponding data series (row) in y. Default: 'blue'
80
+ # @param [Numeric] markersize the size of the marker in pixels. Default: 20
81
+ # @param [Numeric] width the visualization width in pixels. Default: 640
82
+ # @param [Numeric] height the visualization height in pixels. Default: 480
83
+ # @param [Array, String] domain the domain for the plot (limits on the
84
+ # x-axis). This can be a 2-element array of bounds or any other object
85
+ # that Plotrb::Scale::from understands. Default: scale to x data
86
+ # @param [Array, String] range the range for the plot (limits on the
87
+ # y-axis). This can be a 2-element array of bounds or any other object
88
+ # that Plotrb::Scale::from understands. Default: scale to first row of
89
+ # y.
90
+ #
91
+ # @return [Plotrb::Visualization] A visualization object. (This can be
92
+ # written to a json string for Vega with #generate_spec.)
93
+ #
94
+ # method signature for ruby 2.0 kwargs:
95
+ # def scatter(x, y, symbol: 'circle', color: 'blue', markersize: 20,
96
+ # width: 640, height: 480, domain: nil, range: nil)
97
+ def self.scatter(x, y, kwargs={})
98
+ kwargs = {symbol: 'circle', color: 'blue', markersize: 20, width: 640,
99
+ height: 480, domain: nil, range: nil}.merge(kwargs)
100
+ symbol = kwargs[:symbol]
101
+ color = kwargs[:color]
102
+ markersize = kwargs[:markersize]
103
+ width = kwargs[:width]
104
+ height = kwargs[:height]
105
+ domain = kwargs[:domain]
106
+ range = kwargs[:range]
107
+
108
+ datapoints = []
109
+ n_sets = 1
110
+ x_n_sets = 1
111
+ x_size = x.size
112
+
113
+ if x.respond_to?(:shape) and x.shape.length > 1 then # x is 2D NMatrix
114
+ x_n_sets = x.shape[0]
115
+ x_size = x.shape[1]
116
+ elsif x.instance_of? Array and x[0].instance_of? Array then # x is nested Array
117
+ x_n_sets = x.size
118
+ x_size = x[0].size
119
+ end
120
+
121
+ if y.respond_to?(:shape) and y.shape.length > 1 then # y is 2D NMatrix
122
+ n_sets = y.shape[0]
123
+ elsif y.instance_of? Array and y[0].instance_of? Array then # y is nested array
124
+ n_sets = y.size
125
+ end
126
+
127
+ x_size.times do |i|
128
+ dp = {}
129
+ n_sets.times do |j|
130
+
131
+ xj = j.modulo(x_n_sets)
132
+ if x.respond_to?(:shape) and x.shape.length > 1 then
133
+ dp["x#{xj}".to_sym] = x[xj, i]
134
+ elsif x.instance_of? Array and x[0].instance_of? Array then
135
+ dp["x#{xj}".to_sym] = x[xj][i]
136
+ else
137
+ dp["x#{xj}".to_sym] = x[i]
138
+ end
139
+
140
+ indices = [i]
141
+ if y.respond_to?(:shape) and y.shape.length > 1 then
142
+ indices = [j,i]
143
+ end
144
+ if y.instance_of? Array and y[0].instance_of? Array then
145
+ dp["y#{j}".to_sym] = y[j][*indices]
146
+ else
147
+ dp["y#{j}".to_sym] = y[*indices]
148
+ end
149
+ end
150
+
151
+ datapoints << dp
152
+ end
153
+
154
+ Plotrb::Kernel.data.delete_if { |d| d.name == scatter_data_name }
155
+ dataset= Plotrb::Data.new.name(scatter_data_name)
156
+ dataset.values(datapoints)
157
+
158
+ domain_in = "#{scatter_data_name}.x0"
159
+ if domain then
160
+ domain_in = domain
161
+ end
162
+ range_in = "#{scatter_data_name}.y0"
163
+ if range then
164
+ range_in = range
165
+ end
166
+
167
+ Plotrb::Kernel.scales.delete_if { |d| d.name == scatter_x_scale_name or d.name == scatter_y_scale_name }
168
+
169
+ xs = linear_scale.name(scatter_x_scale_name).from(domain_in).to_width
170
+ ys = linear_scale.name(scatter_y_scale_name).from(range_in).to_height
171
+
172
+ marks = []
173
+ n_sets.times do |j|
174
+ marks << symbol_mark.from(dataset) do
175
+ c_j = color.instance_of?(Array) ? color[j] : color
176
+ s_j = symbol.instance_of?(Array) ? symbol[j] : symbol
177
+ x_j = j.modulo(x_n_sets)
178
+ enter do
179
+ x_start { scale(xs).from("x#{x_j}") }
180
+ y_start { scale(ys).from("y#{j}") }
181
+ size markersize
182
+ shape s_j
183
+ fill c_j
184
+ end
185
+ end
186
+ end
187
+
188
+ visualization.width(width).height(height) do
189
+ data dataset
190
+ scales xs, ys
191
+ marks marks
192
+ axes x_axis.scale(xs), y_axis.scale(ys)
193
+ end
194
+ end
195
+ end
196
+ end
197
+