plotrb 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,459 @@
1
+ module Plotrb
2
+
3
+ # Marks are the basic visual building block of a visualization.
4
+ # See {https://github.com/trifacta/vega/wiki/Marks}
5
+ class Mark
6
+
7
+ include ::Plotrb::Base
8
+
9
+ # all available types of marks defined by Vega
10
+ TYPES = %i(rect symbol path arc area line image text group)
11
+
12
+ TYPES.each do |t|
13
+ define_singleton_method(t) do |&block|
14
+ ::Plotrb::Mark.new(t, &block)
15
+ end
16
+ end
17
+
18
+ # Top level mark properties
19
+
20
+ # @!attributes type
21
+ # @return [Symbol] the mark type
22
+ # @!attributes name
23
+ # @return [String] the name of the mark
24
+ # @!attributes description
25
+ # @return [String] optional description of the mark
26
+ # @!attributes from
27
+ # @return [Hash] the data this mark set should visualize
28
+ # @!attributes properties
29
+ # @return [MarkProperty] the property set definitions
30
+ # @!attributes key
31
+ # @return [String] the data field to use an unique key for data binding
32
+ # @!attributes delay
33
+ # @return [ValueRef] the transition delay for mark updates
34
+ # @!attributes ease
35
+ # @return [String] the transition easing function for mark updates
36
+ MARK_PROPERTIES = [:type, :name, :description, :from, :properties, :key,
37
+ :delay, :ease, :group]
38
+
39
+ add_attributes *MARK_PROPERTIES
40
+
41
+ def initialize(type, &block)
42
+ @type = type
43
+ @properties = {}
44
+ define_single_val_attributes(:name, :description, :key, :delay, :ease,
45
+ :group)
46
+ define_multi_val_attributes(:from)
47
+ if @type == :group
48
+ add_attributes(:scales, :axes, :marks)
49
+ define_multi_val_attributes(:scales, :axes, :marks)
50
+ end
51
+ ::Plotrb::Kernel.marks << self
52
+ self.instance_eval(&block) if block_given?
53
+ self
54
+ end
55
+
56
+ def type
57
+ @type
58
+ end
59
+
60
+ def properties
61
+ @properties
62
+ end
63
+
64
+ def enter(&block)
65
+ process_from
66
+ data = @from[:data] if @from
67
+ @properties.merge!(
68
+ { enter: ::Plotrb::Mark::MarkProperty.
69
+ new(@type, data, &block) }
70
+ )
71
+ self
72
+ end
73
+
74
+ def exit(&block)
75
+ process_from
76
+ data = @from[:data] if @from
77
+ @properties.merge!(
78
+ { exit: ::Plotrb::Mark::MarkProperty.
79
+ new(@type, data, &block) }
80
+ )
81
+ self
82
+ end
83
+
84
+ def update(&block)
85
+ process_from
86
+ data = @from[:data] if @from
87
+ @properties.merge!(
88
+ { update: ::Plotrb::Mark::MarkProperty.
89
+ new(@type, data, &block) }
90
+ )
91
+ self
92
+ end
93
+
94
+ def hover(&block)
95
+ process_from
96
+ data = @from[:data] if @from
97
+ @properties.merge!(
98
+ { hover: ::Plotrb::Mark::MarkProperty.
99
+ new(@type, data, &block) }
100
+ )
101
+ self
102
+ end
103
+
104
+ private
105
+
106
+ def attribute_post_processing
107
+ process_name
108
+ process_from
109
+ process_group
110
+ end
111
+
112
+ def process_name
113
+ return unless @name
114
+ if ::Plotrb::Kernel.duplicate_mark?(@name)
115
+ raise ArgumentError, 'Duplicate Mark name'
116
+ end
117
+ end
118
+
119
+ def process_from
120
+ return unless @from && !@from_processed
121
+ from = {}
122
+ @from.each do |f|
123
+ case f
124
+ when String
125
+ if ::Plotrb::Kernel.find_data(f)
126
+ from[:data] = f
127
+ else
128
+ raise ArgumentError, 'Invalid data for Mark from'
129
+ end
130
+ when ::Plotrb::Data
131
+ from[:data] = f.name
132
+ when ::Plotrb::Transform
133
+ from[:transform] ||= []
134
+ from[:transform] << f
135
+ else
136
+ raise ArgumentError, 'Invalid Mark from'
137
+ end
138
+ end
139
+ @from = from
140
+ @from_processed = true
141
+ end
142
+
143
+ def process_group
144
+ return unless @scales
145
+ unless @scales.all? { |s| s.is_a?(::Plotrb::Scale) }
146
+ raise ArgumentError, 'Invalid scales for group mark'
147
+ end
148
+
149
+ return unless @axes
150
+ unless @axes.all? { |s| s.is_a?(::Plotrb::Axis) }
151
+ raise ArgumentError, 'Invalid axes for group mark'
152
+ end
153
+
154
+ return unless @marks
155
+ unless @marks.all? { |s| s.is_a?(::Plotrb::Mark) }
156
+ raise ArgumentError, 'Invalid marks for group mark'
157
+ end
158
+ end
159
+
160
+ class MarkProperty
161
+
162
+ include ::Plotrb::Base
163
+
164
+ # Shared visual properties
165
+
166
+ # @!attributes x
167
+ # @return [ValueRef] the first (left-most) x-coordinate
168
+ # @!attributes x2
169
+ # @return [ValueRef] the second (right-most) x-coordinate
170
+ # @!attributes width
171
+ # @return [ValueRef] the width of the mark
172
+ # @!attributes y
173
+ # @return [ValueRef] the first (top-most) y-coordinate
174
+ # @!attributes y2
175
+ # @return [ValueRef] the second (bottom-most) y-coordinate
176
+ # @!attributes height
177
+ # @return [ValueRef] the height of the mark
178
+ # @!attributes opacity
179
+ # @return [ValueRef] the overall opacity
180
+ # @!attributes fill
181
+ # @return [ValueRef] the fill color
182
+ # @!attributes fill_opacity
183
+ # @return [ValueRef] the fill opacity
184
+ # @!attributes stroke
185
+ # @return [ValueRef] the stroke color
186
+ # @!attributes stroke_width
187
+ # @return [ValueRef] the stroke width in pixels
188
+ # @!attributes stroke_opacity
189
+ # @return [ValueRef] the stroke opacity
190
+ # @!attributes stroke_dash
191
+ # @return [ValueRef] alternating stroke, space lengths for creating dashed
192
+ # or dotted lines
193
+ # @!attributes stroke_dash_offset
194
+ # @return [ValueRef] the offset into which to begin the stroke dash
195
+ VISUAL_PROPERTIES = [:x, :x2, :width, :y, :y2, :height, :opacity, :fill,
196
+ :fill_opacity, :stroke, :stroke_width,
197
+ :stroke_opacity, :stroke_dash, :stroke_dash_offset]
198
+
199
+ add_attributes *VISUAL_PROPERTIES
200
+ attr_reader :data
201
+
202
+ def initialize(type, data=nil, &block)
203
+ define_single_val_attributes *VISUAL_PROPERTIES
204
+ self.singleton_class.class_eval {
205
+ alias_method :x_start, :x
206
+ alias_method :left, :x
207
+ alias_method :x_end, :x2
208
+ alias_method :right, :x2
209
+ alias_method :y_start, :y
210
+ alias_method :top, :y
211
+ alias_method :y_end, :y2
212
+ alias_method :bottom, :y2
213
+ }
214
+ @data = data
215
+ self.send(type)
216
+ self.instance_eval(&block) if block_given?
217
+ end
218
+
219
+ private
220
+
221
+ def attribute_post_processing
222
+
223
+ end
224
+
225
+ def rect
226
+ # no additional attributes
227
+ end
228
+
229
+ def group
230
+ # no additional attributes
231
+ end
232
+
233
+ def symbol
234
+ # @!attribute size
235
+ # @return [ValueRef] the pixel area of the symbol
236
+ # @!attribute shape
237
+ # @return [ValueRef] the symbol shape
238
+ attrs = [:size, :shape]
239
+ add_attributes *attrs
240
+ define_single_val_attributes *attrs
241
+ end
242
+
243
+ def path
244
+ # @!attribute path
245
+ # @return [ValueRef] the path definition in SVG path string
246
+ attrs = [:path]
247
+ add_attributes *attrs
248
+ define_single_val_attribute *attrs
249
+ end
250
+
251
+ def arc
252
+ # @!attribute inner_radius
253
+ # @return [ValueRef] the inner radius of the arc in pixels
254
+ # @!attribute outer_radius
255
+ # @return [ValueRef] the outer radius of the arc in pixels
256
+ # @!attribute start_angle
257
+ # @return [ValueRef] the start angle of the arc in radians
258
+ # @!attribute end_angle
259
+ # @return [ValueRef] the end angle of the arc in radians
260
+ attrs = [:inner_radius, :outer_radius, :start_angle, :end_angle]
261
+ add_attributes *attrs
262
+ define_single_val_attributes *attrs
263
+ end
264
+
265
+ def area
266
+ # @!attribute interpolate
267
+ # @return [ValueRef] the line interpolation method to use
268
+ # @!attribute tension
269
+ # @return [ValueRef] the tension parameter for the interpolation
270
+ attrs = [:interpolate, :tension]
271
+ add_attributes *attrs
272
+ define_single_val_attributes *attrs
273
+ end
274
+
275
+ def line
276
+ # @!attribute interpolate
277
+ # @return [ValueRef] the line interpolation method to use
278
+ # @!attribute tension
279
+ # @return [ValueRef] the tension parameter for the interpolation
280
+ attrs = [:interpolate, :tension]
281
+ add_attributes *attrs
282
+ define_single_val_attributes *attrs
283
+ end
284
+
285
+ def image
286
+ # @!attribute url
287
+ # @return [ValueRef] the url from which to retrieve the image
288
+ # @!attribute align
289
+ # @return [ValueRef] the horizontal alignment of the image
290
+ # @!attribute baseline
291
+ # @return [ValueRef] the vertical alignment of the image
292
+ attrs = [:url, :align, :baseline]
293
+ add_attributes *attrs
294
+ define_single_val_attributes *attrs
295
+ end
296
+
297
+ def text
298
+ # @!attribute text
299
+ # @return [ValueRef] the text to display
300
+ # @!attribute align
301
+ # @return [ValueRef] the horizontal alignment of the text
302
+ # @!attribute baseline
303
+ # @return [ValueRef] the vertical alignment of the text
304
+ # @!attribute dx
305
+ # @return [ValueRef] the horizontal margin between text label and its
306
+ # anchor point
307
+ # @!attribute dy
308
+ # @return [ValueRef] the vertical margin between text label and its
309
+ # anchor point
310
+ # @!attribute angle
311
+ # @return [ValueRef] the rotation angle of the text in degrees
312
+ # @!attribute font
313
+ # @return [ValueRef] the font of the text
314
+ # @!attribute font_size
315
+ # @return [ValueRef] the font size
316
+ # @!attribute font_weight
317
+ # @return [ValueRef] the font weight
318
+ # @!attribute font_style
319
+ # @return [ValueRef] the font style
320
+ attrs = [:text, :align, :baseline, :dx, :dy, :angle, :font, :font_size,
321
+ :font_weight, :font_style]
322
+ add_attributes *attrs
323
+ define_single_val_attributes *attrs
324
+ end
325
+
326
+ def define_single_val_attribute(method)
327
+ define_singleton_method(method) do |*args, &block|
328
+ if block
329
+ val = ::Plotrb::Mark::MarkProperty::ValueRef.
330
+ new(@data, *args, &block)
331
+ self.instance_variable_set("@#{method}", val)
332
+ else
333
+ case args.size
334
+ when 0
335
+ self.instance_variable_get("@#{method}")
336
+ when 1
337
+ val = ::Plotrb::Mark::MarkProperty::ValueRef.new(@data, args[0])
338
+ self.instance_variable_set("@#{method}", val)
339
+ else
340
+ raise ArgumentError
341
+ end
342
+ end
343
+ self
344
+ end
345
+ end
346
+
347
+ def define_single_val_attributes(*method)
348
+ method.each { |m| define_single_val_attribute(m) }
349
+ end
350
+
351
+ # A value reference specifies the value for a given mark property
352
+ class ValueRef
353
+
354
+ include ::Plotrb::Base
355
+
356
+ # @!attributes value
357
+ # @return A constant value
358
+ # @!attributes field
359
+ # @return [String] A field from which to pull a data value
360
+ # @!attributes scale
361
+ # @return [String] the name of a scale transform to apply
362
+ # @!attributes mult
363
+ # @return [Numeric] a multiplier for the value
364
+ # @!attributes offset
365
+ # @return [Numeric] an additive offset to bias the final value
366
+ # @!attributes band
367
+ # @return [Boolean] whether to use range band of the scale as the
368
+ # retrieved value
369
+ VALUE_REF_PROPERTIES = [:value, :field, :scale, :mult, :offset, :band,
370
+ :group]
371
+
372
+ add_attributes *VALUE_REF_PROPERTIES
373
+ attr_reader :data
374
+
375
+ def initialize(data, value=nil, &block)
376
+ @data = data
377
+ define_single_val_attributes(:value, :mult, :offset, :group, :field,
378
+ :scale)
379
+ define_boolean_attribute(:band)
380
+ self.singleton_class.class_eval {
381
+ alias_method :from, :field
382
+ alias_method :use_band, :band
383
+ alias_method :use_band?, :band?
384
+ alias_method :times, :mult
385
+ }
386
+ if value
387
+ @value = value
388
+ end
389
+ self.instance_eval(&block) if block
390
+ self
391
+ end
392
+
393
+ private
394
+
395
+ def attribute_post_processing
396
+ process_scale
397
+ process_field
398
+ end
399
+
400
+ def process_field
401
+ return unless @field
402
+ case @field
403
+ when String, Symbol
404
+ @field = get_full_field_ref(@field)
405
+ when Hash
406
+ if @field[:group]
407
+ @field[:group] = get_full_field_ref(@field[:group])
408
+ else
409
+ raise ArgumentError, 'Missing field group'
410
+ end
411
+ else
412
+ raise ArgumentError, 'Invalid value field'
413
+ end
414
+ end
415
+
416
+ def process_scale
417
+ return unless @scale
418
+ case @scale
419
+ when String
420
+ unless ::Plotrb::Kernel.find_scale(@scale)
421
+ raise ArgumentError, 'Invalid value scale'
422
+ end
423
+ when ::Plotrb::Scale
424
+ @scale = @scale.name
425
+ when Hash
426
+ if @scale[:field]
427
+ @scale[:field] = get_full_field_ref(@scale[:field])
428
+ end
429
+ if @scale[:group]
430
+ @scale[:group] = get_full_field_ref(@scale[:group])
431
+ end
432
+ else
433
+ raise ArgumentError, 'Invalid value scale'
434
+ end
435
+ end
436
+
437
+ def get_full_field_ref(field)
438
+ data = if @data.is_a?(::Plotrb::Data)
439
+ @data
440
+ else
441
+ ::Plotrb::Kernel.find_data(@data)
442
+ end
443
+ extra_fields = (data.extra_fields if data) || []
444
+ if field.to_s.start_with?('data.')
445
+ field
446
+ elsif extra_fields.include?(field.to_s.split('.')[0].to_sym)
447
+ classify(field, :json)
448
+ else
449
+ "data.#{field}"
450
+ end
451
+ end
452
+
453
+ end
454
+
455
+ end
456
+
457
+ end
458
+
459
+ end