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.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/.rspec +1 -0
- data/.travis.yml +3 -0
- data/Gemfile +2 -0
- data/LICENSE.txt +38 -0
- data/README.rdoc +77 -0
- data/Rakefile +7 -0
- data/examples/arc.rb +31 -0
- data/examples/area.rb +48 -0
- data/examples/bar.rb +44 -0
- data/examples/barley.rb +66 -0
- data/examples/choropleth.rb +48 -0
- data/examples/lifelines.rb +106 -0
- data/examples/scatter.rb +43 -0
- data/lib/plotrb.rb +25 -0
- data/lib/plotrb/axes.rb +208 -0
- data/lib/plotrb/base.rb +193 -0
- data/lib/plotrb/data.rb +232 -0
- data/lib/plotrb/kernel.rb +136 -0
- data/lib/plotrb/legends.rb +168 -0
- data/lib/plotrb/marks.rb +459 -0
- data/lib/plotrb/scales.rb +346 -0
- data/lib/plotrb/simple.rb +197 -0
- data/lib/plotrb/transforms.rb +592 -0
- data/lib/plotrb/version.rb +3 -0
- data/lib/plotrb/visualization.rb +55 -0
- data/plotrb.gemspec +27 -0
- data/spec/plotrb/axes_spec.rb +227 -0
- data/spec/plotrb/base_spec.rb +321 -0
- data/spec/plotrb/data_spec.rb +258 -0
- data/spec/plotrb/kernel_spec.rb +54 -0
- data/spec/plotrb/legends_spec.rb +157 -0
- data/spec/plotrb/marks_spec.rb +46 -0
- data/spec/plotrb/scales_spec.rb +187 -0
- data/spec/plotrb/simple_spec.rb +61 -0
- data/spec/plotrb/transforms_spec.rb +248 -0
- data/spec/plotrb/visualization_spec.rb +93 -0
- data/spec/plotrb_spec.rb +5 -0
- data/spec/spec_helper.rb +12 -0
- metadata +180 -0
data/lib/plotrb/marks.rb
ADDED
@@ -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
|