plotrb 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,592 @@
1
+ module Plotrb
2
+
3
+ # Data transform performs operations on a data set prior to
4
+ # visualization.
5
+ # See {https://github.com/trifacta/vega/wiki/Data-Transforms}
6
+ class Transform
7
+
8
+ include ::Plotrb::Base
9
+
10
+ # all available types of transforms defined by Vega
11
+ TYPES = %i(array copy cross facet filter flatten fold formula slice sort
12
+ stats truncate unique window zip force geo geopath link pie stack
13
+ treemap wordcloud)
14
+
15
+ TYPES.each do |t|
16
+ define_singleton_method(t) do |&block|
17
+ ::Plotrb::Transform.new(t, &block)
18
+ end
19
+ end
20
+
21
+ # @!attributes type
22
+ # @return [Symbol] the transform type
23
+ add_attributes :type
24
+
25
+ def initialize(type, &block)
26
+ @type = type
27
+ @extra_fields = [:index, :data]
28
+ self.send(@type)
29
+ self.instance_eval(&block) if block_given?
30
+ ::Plotrb::Kernel.transforms << self
31
+ self
32
+ end
33
+
34
+ def type
35
+ @type
36
+ end
37
+
38
+ def extra_fields
39
+ @extra_fields
40
+ end
41
+
42
+ private
43
+
44
+ # Data Manipulation Transforms
45
+
46
+ def array
47
+ # @!attributes fields
48
+ # @return [Array<String>] array of field references to copy
49
+ add_attributes(:fields)
50
+ define_multi_val_attribute(:fields)
51
+ self.singleton_class.class_eval { alias_method :take, :fields }
52
+ end
53
+
54
+ def copy
55
+ # @!attributes from
56
+ # @return [String] the name of the object to copy values from
57
+ # @!attributes fields
58
+ # @return [Array<String>] the fields to copy
59
+ # @!attributes as
60
+ # @return [Array<String>] the field names to copy the values to
61
+ add_attributes(:from, :fields, :as)
62
+ define_single_val_attribute(:from)
63
+ define_multi_val_attributes(:fields, :as)
64
+ self.singleton_class.class_eval { alias_method :take, :fields }
65
+ end
66
+
67
+ def cross
68
+ # @!attributes with
69
+ # @return [String] the name of the secondary data to cross with
70
+ # @!attributes diagonal
71
+ # @return [Boolean] whether diagonal of cross-product will be included
72
+ add_attributes(:with, :diagonal)
73
+ define_single_val_attribute(:with)
74
+ define_boolean_attribute(:diagonal)
75
+ self.singleton_class.class_eval {
76
+ alias_method :include_diagonal, :diagonal
77
+ alias_method :include_diagonal?, :diagonal?
78
+ }
79
+ end
80
+
81
+ def facet
82
+ # @!attributes keys
83
+ # @return [Array<String>] the fields to use as keys
84
+ # @!attributes sort
85
+ # @return [String, Array<String>] sort criteria
86
+ add_attributes(:keys, :sort)
87
+ define_multi_val_attributes(:keys, :sort)
88
+ self.singleton_class.class_eval { alias_method :group_by, :keys }
89
+ @extra_fields.concat([:key])
90
+ end
91
+
92
+ def filter
93
+ # @!attributes test
94
+ # @return [String] the expression for the filter predicate, which
95
+ # includes the variable `d`, corresponding to the current data object
96
+ add_attributes(:test)
97
+ define_single_val_attribute(:test)
98
+ end
99
+
100
+ def flatten
101
+ # no parameter needed
102
+ end
103
+
104
+ def fold
105
+ # @!attributes fields
106
+ # @return [Array<String>] the field references indicating the data
107
+ # properties to fold
108
+ add_attributes(:fields)
109
+ define_multi_val_attribute(:fields)
110
+ self.singleton_class.class_eval { alias_method :into, :fields }
111
+ @extra_fields.concat([:key, :value])
112
+ end
113
+
114
+ def formula
115
+ # @!attributes field
116
+ # @return [String] the property name in which to store the value
117
+ # @!attributes
118
+ # @return expr [String] the expression for the formula
119
+ add_attributes(:field, :expr)
120
+ define_single_val_attributes(:field, :expr)
121
+ self.singleton_class.class_eval {
122
+ alias_method :into, :field
123
+ alias_method :apply, :expr
124
+ }
125
+ end
126
+
127
+ def slice
128
+ # @!attributes by
129
+ # @return [Integer, Array<Integer>, Symbol] the sub-array to copy
130
+ # @!attributes field
131
+ # @return [String] the data field to copy the max, min or median value
132
+ add_attributes(:by, :field)
133
+ define_single_val_attributes(:by, :field)
134
+ end
135
+
136
+ # TODO: allow reverse sort
137
+ def sort
138
+ # @!attributes by
139
+ # @return [String, Array<String>] a list of fields to use as sort
140
+ # criteria
141
+ add_attributes(:by)
142
+ define_multi_val_attribute(:by)
143
+ end
144
+
145
+ def stats
146
+ # @!attributes value
147
+ # @return [String] the field for which to computer the statistics
148
+ # @!attributes median
149
+ # @return [Boolean] whether median will be computed
150
+ # @!attributes assign
151
+ # @return [Boolean] whether add stat property to each data element
152
+ add_attributes(:value, :median, :assign)
153
+ define_single_val_attribute(:value)
154
+ define_boolean_attributes(:median, :assign)
155
+ self.singleton_class.class_eval {
156
+ alias_method :from, :value
157
+ alias_method :include_median, :median
158
+ alias_method :include_median?, :median?
159
+ alias_method :store_stats, :assign
160
+ alias_method :store_stats?, :assign?
161
+ }
162
+ @extra_fields.concat([:count, :min, :max, :sum, :mean, :variance, :stdev,
163
+ :median])
164
+ end
165
+
166
+ def truncate
167
+ # @!attributes value
168
+ # @return [String] the field containing values to truncate
169
+ # @!attributes output
170
+ # @return [String] the field to store the truncated values
171
+ # @!attributes limit
172
+ # @return [Integer] maximum length of truncated string
173
+ # @!attributes position
174
+ # @return [Symbol] the position from which to remove text
175
+ # @!attributes ellipsis
176
+ # @return [String] the ellipsis for truncated text
177
+ # @!attributes wordbreak
178
+ # @return [Boolean] whether to truncate along word boundaries
179
+ add_attributes(:value, :output, :limit, :position, :ellipsis, :wordbreak)
180
+ define_single_val_attributes(:value, :output, :limit, :position,
181
+ :ellipsis)
182
+ define_boolean_attribute(:wordbreak)
183
+ self.singleton_class.class_eval {
184
+ alias_method :from, :value
185
+ alias_method :to, :output
186
+ alias_method :max_length, :limit
187
+ }
188
+ define_singleton_method :method_missing do |method, *args, &block|
189
+ if method.to_s =~ /^in_(front|back|middle)$/
190
+ self.position($1.to_sym, &block)
191
+ else
192
+ super
193
+ end
194
+ end
195
+ end
196
+
197
+ def unique
198
+ # @!attributes field
199
+ # @return [String] the data field for which to compute unique value
200
+ # @!attributes as
201
+ # @return [String] the field name to store the unique values
202
+ add_attributes(:field, :as)
203
+ define_single_val_attributes(:field, :as)
204
+ self.singleton_class.class_eval {
205
+ alias_method :from, :field
206
+ alias_method :to, :as
207
+ }
208
+ end
209
+
210
+ def window
211
+ # @!attributes size
212
+ # @return [Integer] the size of the sliding window
213
+ # @!attributes step
214
+ # @return [Integer] the step size to advance the window per frame
215
+ add_attributes(:size, :step)
216
+ define_single_val_attributes(:size, :step)
217
+ end
218
+
219
+ def zip
220
+ # @!attributes with
221
+ # @return [String] the name of the secondary data set to zip with the
222
+ # primary data set
223
+ # @!attributes as
224
+ # @return [String] the name of the field to store the secondary data set
225
+ # values
226
+ # @!attributes key
227
+ # @return [String] the field in the primary data set to match against
228
+ # the secondary data set
229
+ # @!attributes with_key
230
+ # @return [String] the field in the secondary data set to match
231
+ # against the primary data set
232
+ # @!attributes default
233
+ # @return [] a default value to use if no matching key value is found
234
+ add_attributes(:with, :as, :key, :with_key, :default)
235
+ define_single_val_attributes(:with, :as, :default, :key, :with_key)
236
+ self.singleton_class.class_eval {
237
+ alias_method :match, :key
238
+ alias_method :against, :with_key
239
+ }
240
+ end
241
+
242
+ # Visual Encoding Transforms
243
+
244
+ def force
245
+ # @!attributes links
246
+ # @return [String] the name of the link (edge) data set, must have
247
+ # `source` and `target` attributes
248
+ # @!attributes size
249
+ # @return [Array(Integer, Integer)] the dimensions of the layout
250
+ # @!attributes iterations
251
+ # @return [Integer] the number of iterations to run
252
+ # @!attributes charge
253
+ # @return [Numeric, String] the strength of the charge each node exerts
254
+ # @!attributes link_distance
255
+ # @return [Integer, String] the length of edges
256
+ # @!attributes link_strength
257
+ # @return [Numeric, String] the tension of edges
258
+ # @!attributes friction
259
+ # @return [Numeric] the strength of the friction force used to
260
+ # stabilize the layout
261
+ # @!attributes theta
262
+ # @return [Numeric] the theta parameter for the Barnes-Hut algorithm
263
+ # used to compute charge forces between nodes
264
+ # @!attributes gravity
265
+ # @return [Numeric] the strength of the pseudo-gravity force that pulls
266
+ # nodes towards the center of the layout area
267
+ # @!attributes alpha
268
+ # @return [Numeric] a "temperature" parameter that determines how much
269
+ # node positions are adjusted at each step
270
+ attr = [:links, :size, :iterations, :charge, :link_distance,
271
+ :link_strength, :friction, :theta, :gravity, :alpha]
272
+ add_attributes(*attr)
273
+ define_single_val_attributes(*attr)
274
+ end
275
+
276
+ def geo
277
+ # @!attributes projection
278
+ # @return [String] the type of cartographic projection to use
279
+ # @!attributes lon
280
+ # @return [String] the input longitude values
281
+ # @!attributes lat
282
+ # @return [String] the input latitude values
283
+ # @!attributes center
284
+ # @return [Array(Integer, Integer)] the center of the projection
285
+ # @!attributes translate
286
+ # @return [Array(Integer, Integer)] the translation of the projection
287
+ # @!attributes scale
288
+ # @return [Numeric] the scale of the projection
289
+ # @!attributes rotate
290
+ # @return [Numeric] the rotation of the projection
291
+ # @!attributes precision
292
+ # @return [Numeric] the desired precision of the projection
293
+ # @!attributes clip_angle
294
+ # @return [Numeric] the clip angle of the projection
295
+ attr = [:projection, :lon, :lat, :center, :translate, :scale,
296
+ :rotate, :precision, :clip_angle]
297
+ add_attributes(*attr)
298
+ define_single_val_attributes(*attr)
299
+ end
300
+
301
+ def geopath
302
+ # @!attributes value
303
+ # @return [String] the data field containing the GeoJSON feature data
304
+ # @!attributes (see #geo)
305
+ attr = [:value, :projection, :center, :translate, :scale, :rotate,
306
+ :precision, :clip_angle]
307
+ add_attributes(*attr)
308
+ define_single_val_attributes(*attr)
309
+ @value ||= 'data'
310
+ @extra_fields.concat([:path])
311
+ end
312
+
313
+ def link
314
+ # @!attributes source
315
+ # @return [String] the data field that references the source node for
316
+ # this link
317
+ # @!attributes target
318
+ # @return [String] the data field that references the target node for
319
+ # this link
320
+ # @!attributes shape
321
+ # @return [Symbol] the path shape to use
322
+ # @!attributes tension
323
+ # @return [Numeric] the tension in the range [0,1] for the "tightness"
324
+ # of 'curve'-shaped links
325
+ attr = [:source, :target, :shape, :tension]
326
+ add_attributes(*attr)
327
+ define_single_val_attributes(*attr)
328
+ @extra_fields.concat([:path])
329
+ end
330
+
331
+ def pie
332
+ # @!attributes sort
333
+ # @return [Boolean] whether to sort the data prior to computing angles
334
+ # @!attributes value
335
+ # @return [String] the data values to encode as angular spans
336
+ add_attributes(:sort, :value)
337
+ define_boolean_attribute(:sort)
338
+ define_single_val_attribute(:value)
339
+ @extra_fields.concat([:start_angle, :end_angle])
340
+ end
341
+
342
+ def stack
343
+ # @!attributes point
344
+ # @return [String] the data field determining the points at which to
345
+ # stack
346
+ # @!attributes height
347
+ # @return [String] the data field determining the height of a stack
348
+ # @!attributes offset
349
+ # @return [Symbol] the baseline offset style
350
+ # @!attributes order
351
+ # @return [Symbol] the sort order for stack layers
352
+ attr = [:point, :height, :offset, :order]
353
+ add_attributes(*attr)
354
+ define_single_val_attributes(*attr)
355
+ @extra_fields.concat([:y, :y2])
356
+ end
357
+
358
+ def treemap
359
+ # @!attributes padding
360
+ # @return [Integer, Array(Integer, Integer, Integer, Integer)] the
361
+ # padding to provide around the internal nodes in the treemap
362
+ # @!attributes ratio
363
+ # @return [Numeric] the target aspect ratio for the layout to optimize
364
+ # @!attributes round
365
+ # @return [Boolean] whether cell dimensions will be rounded to integer
366
+ # pixels
367
+ # @!attributes size
368
+ # @return [Array(Integer, Integer)] the dimensions of the layout
369
+ # @!attributes sticky
370
+ # @return [Boolean] whether repeated runs of the treemap will use cached
371
+ # partition boundaries
372
+ # @!attributes value
373
+ # @return [String] the values to use to determine the area of each
374
+ # leaf-level treemap cell
375
+ add_attributes(:padding, :ratio, :round, :size, :sticky, :value)
376
+ define_single_val_attributes(:padding, :ratio, :size, :value)
377
+ define_boolean_attributes(:round, :sticky)
378
+ @extra_fields.concat([:x, :y, :width, :height])
379
+ end
380
+
381
+ def wordcloud
382
+ # @!attributes font
383
+ # @return [String] the font face to use within the word cloud
384
+ # @!attributes font_size
385
+ # @return [String] the font size for a word
386
+ # @!attributes font_style
387
+ # @return [String] the font style to use
388
+ # @!attributes font_weight
389
+ # @return [String] the font weight to use
390
+ # @!attributes padding
391
+ # @return [Integer, Array(Integer, Integer, Integer, Integer)] the
392
+ # padding to provide around text in the word cloud
393
+ # @!attributes rotate
394
+ # @return [String, Hash] the rotation angle for a word
395
+ # @!attributes size
396
+ # @return [Array(Integer, Integer)] the dimensions of the layout
397
+ # @!attributes text
398
+ # @return [String] the data field containing the text to visualize
399
+ attr = [:font, :font_size, :font_style, :font_weight, :padding,
400
+ :rotate, :size, :text]
401
+ add_attributes(*attr)
402
+ define_single_val_attribute(*attr)
403
+ @extra_fields.concat([:x, :y, :font_size, :font, :angle])
404
+ end
405
+
406
+ def attribute_post_processing
407
+ process_array_fields
408
+ process_copy_as
409
+ process_facet_keys
410
+ process_filter_test
411
+ process_fold_fields
412
+ process_slice_field
413
+ process_stats_value
414
+ process_unique_field
415
+ process_truncate_value
416
+ process_zip_key
417
+ process_zip_with_key
418
+ process_zip_as
419
+ process_geo_lon
420
+ process_geo_lat
421
+ process_link_source
422
+ process_link_target
423
+ process_pie_value
424
+ process_stack_order
425
+ process_stack_point
426
+ process_stack_height
427
+ process_treemap_value
428
+ process_wordcloud_text
429
+ process_wordcloud_font_size
430
+ end
431
+
432
+ def process_array_fields
433
+ return unless @type == :array && @fields
434
+ @fields.collect! { |f| get_full_field_ref(f) }
435
+ end
436
+
437
+ def process_copy_as
438
+ return unless @type == :copy && @as && @fields
439
+ if @as.is_a?(Array) && @as.size != @fields.size
440
+ raise ArgumentError, 'Unmatched number of fields for copy transform'
441
+ end
442
+ end
443
+
444
+ def process_cross_with
445
+ return unless @type == :cross && @with
446
+ case @with
447
+ when String
448
+ unless ::Plotrb::Kernel.find_data(@with)
449
+ raise ArgumentError, 'Invalid data for cross transform'
450
+ end
451
+ when ::Plotrb::Data
452
+ @with = @with.name
453
+ else
454
+ raise ArgumentError, 'Invalid data for cross transform'
455
+ end
456
+ end
457
+
458
+ def process_facet_keys
459
+ return unless @type == :facet && @keys
460
+ @keys.collect! { |k| get_full_field_ref(k) }
461
+ end
462
+
463
+ def process_filter_test
464
+ return unless @type == :filter && @test
465
+ unless @test =~ /d\./
466
+ raise ArgumentError, 'Invalid filter test string, prefix with \'d.\''
467
+ end
468
+ end
469
+
470
+ def process_fold_fields
471
+ return unless @type == :fold && @fields
472
+ @fields.collect! { |f| get_full_field_ref(f) }
473
+ end
474
+
475
+ def process_slice_field
476
+ return unless @type == :slice && @field
477
+ @field = get_full_field_ref(@field)
478
+ end
479
+
480
+ def process_stats_value
481
+ return unless @type == :stats && @value
482
+ @value = get_full_field_ref(@value)
483
+ end
484
+
485
+ def process_unique_field
486
+ return unless @type == :unique && @field
487
+ @field = get_full_field_ref(@field)
488
+ end
489
+
490
+ def process_truncate_value
491
+ return unless @type == :truncate && @value
492
+ @value = get_full_field_ref(@value)
493
+ end
494
+
495
+ def process_zip_key
496
+ return unless @type == :zip && @key
497
+ @key = get_full_field_ref(@key)
498
+ end
499
+
500
+ def process_zip_with_key
501
+ return unless @type == :zip && @with_key
502
+ @with_key = get_full_field_ref(@with_key)
503
+ end
504
+
505
+ def process_zip_as
506
+ return unless @type == :zip && @as
507
+ @extra_fields.concat([@as.to_sym])
508
+ end
509
+
510
+ def process_geo_lon
511
+ return unless @type == :geo && @lon
512
+ @lon = get_full_field_ref(@lon)
513
+ end
514
+
515
+ def process_geo_lat
516
+ return unless @type == :geo && @lat
517
+ @lat = get_full_field_ref(@lat)
518
+ end
519
+
520
+ def process_link_source
521
+ return unless @type == :link && @source
522
+ @source = get_full_field_ref(@source)
523
+ end
524
+
525
+ def process_link_target
526
+ return unless @type == :link && @target
527
+ @target = get_full_field_ref(@target)
528
+ end
529
+
530
+ def process_pie_value
531
+ return unless @type == :pie
532
+ if @value
533
+ @value = get_full_field_ref(@value)
534
+ else
535
+ @value = 'data'
536
+ end
537
+ end
538
+
539
+ def process_stack_order
540
+ return unless @order
541
+ case @order
542
+ when :default, 'default', :reverse, 'reverse'
543
+ when :inside_out, 'inside-out', 'inside_out'
544
+ @order = 'inside-out'
545
+ else
546
+ raise ArgumentError, 'Unsupported stack order'
547
+ end
548
+ end
549
+
550
+ def process_stack_point
551
+ return unless @type == :stack && @point
552
+ @point = get_full_field_ref(@point)
553
+ end
554
+
555
+ def process_stack_height
556
+ return unless @type == :stack && @height
557
+ @height = get_full_field_ref(@height)
558
+ end
559
+
560
+ def process_treemap_value
561
+ return unless @type == :treemap && @value
562
+ @value = get_full_field_ref(@value)
563
+ end
564
+
565
+ def process_wordcloud_text
566
+ return unless @type == :wordcloud && @text
567
+ @text = get_full_field_ref(@text)
568
+ end
569
+
570
+ def process_wordcloud_font_size
571
+ return unless @type == :wordcloud && @font_size
572
+ @font_size = get_full_field_ref(@font_size)
573
+ end
574
+
575
+ def get_full_field_ref(field)
576
+ case field
577
+ when String
578
+ if field.start_with?('data.') || extra_fields.include?(field.to_sym)
579
+ field
580
+ else
581
+ "data.#{field}"
582
+ end
583
+ when ::Plotrb::Data
584
+ 'data'
585
+ else
586
+ raise ArgumentError, 'Invalid data field'
587
+ end
588
+ end
589
+
590
+ end
591
+
592
+ end