plotrb 0.0.1

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,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
+