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