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