plotrb 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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/data.rb
ADDED
@@ -0,0 +1,232 @@
|
|
1
|
+
module Plotrb
|
2
|
+
|
3
|
+
# The basic tabular data model used by Vega.
|
4
|
+
# See {https://github.com/trifacta/vega/wiki/Data}
|
5
|
+
class Data
|
6
|
+
|
7
|
+
include ::Plotrb::Base
|
8
|
+
|
9
|
+
# @!attributes name
|
10
|
+
# @return [String] the name of the data set
|
11
|
+
# @!attributes format
|
12
|
+
# @return [Format] the format of the data file
|
13
|
+
# @!attributes values
|
14
|
+
# @return [Hash, Array, String] the actual data set
|
15
|
+
# @!attributes source
|
16
|
+
# @return [String, Data] the name of another data set to use as source
|
17
|
+
# @!attributes url
|
18
|
+
# @return [String] the url from which to load the data set
|
19
|
+
# @!attributes transform
|
20
|
+
# @return [Array<Transform>] an array of transform definitions
|
21
|
+
add_attributes :name, :format, :values, :source, :url, :transform
|
22
|
+
|
23
|
+
def initialize(&block)
|
24
|
+
define_single_val_attributes(:name, :values, :source, :url)
|
25
|
+
define_multi_val_attribute(:transform)
|
26
|
+
self.singleton_class.class_eval {
|
27
|
+
alias_method :file, :url
|
28
|
+
}
|
29
|
+
self.instance_eval(&block) if block_given?
|
30
|
+
::Plotrb::Kernel.data << self
|
31
|
+
self
|
32
|
+
end
|
33
|
+
|
34
|
+
def format(*args, &block)
|
35
|
+
case args.size
|
36
|
+
when 0
|
37
|
+
@format
|
38
|
+
when 1
|
39
|
+
@format = ::Plotrb::Data::Format.new(args[0].to_sym, &block)
|
40
|
+
self
|
41
|
+
else
|
42
|
+
raise ArgumentError, 'Invalid Data format'
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def extra_fields
|
47
|
+
@extra_fields ||= [:data, :index]
|
48
|
+
if @transform
|
49
|
+
@extra_fields.concat(@transform.collect { |t| t.extra_fields }).
|
50
|
+
flatten!.uniq!
|
51
|
+
end
|
52
|
+
@extra_fields
|
53
|
+
end
|
54
|
+
|
55
|
+
def method_missing(method, *args, &block)
|
56
|
+
case method.to_s
|
57
|
+
# set format of the data
|
58
|
+
when /^as_(csv|tsv|json|topojson|treejson)$/
|
59
|
+
self.format($1.to_sym, &block)
|
60
|
+
else
|
61
|
+
super
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def attribute_post_processing
|
68
|
+
process_name
|
69
|
+
process_values
|
70
|
+
process_source
|
71
|
+
process_url
|
72
|
+
process_transform
|
73
|
+
end
|
74
|
+
|
75
|
+
def process_name
|
76
|
+
if @name.nil? || @name.strip.empty?
|
77
|
+
raise ArgumentError, 'Name missing for Data object'
|
78
|
+
end
|
79
|
+
if ::Plotrb::Kernel.duplicate_data?(@name)
|
80
|
+
raise ArgumentError, 'Duplicate names for Data object'
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def process_values
|
85
|
+
return unless @values
|
86
|
+
case @values
|
87
|
+
when String
|
88
|
+
begin
|
89
|
+
Yajl::Parser.parse(@values)
|
90
|
+
rescue Yajl::ParseError
|
91
|
+
raise ArgumentError, 'Invalid JSON values in Data'
|
92
|
+
end
|
93
|
+
when Array, Hash
|
94
|
+
# leave as it is
|
95
|
+
else
|
96
|
+
raise ArgumentError, 'Unsupported value type in Data'
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def process_source
|
101
|
+
return unless @source
|
102
|
+
case source
|
103
|
+
when String
|
104
|
+
unless ::Plotrb::Kernel.find_data(@source)
|
105
|
+
raise ArgumentError, 'Source Data not found'
|
106
|
+
end
|
107
|
+
when ::Plotrb::Data
|
108
|
+
@source = @source.name
|
109
|
+
else
|
110
|
+
raise ArgumentError, 'Unknown Data source'
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def process_url
|
115
|
+
return unless @url
|
116
|
+
begin
|
117
|
+
URI.parse(@url)
|
118
|
+
rescue URI::InvalidURIError
|
119
|
+
raise ArgumentError, 'Invalid URL for Data'
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def process_transform
|
124
|
+
return unless @transform
|
125
|
+
if @transform.any? { |t| not t.is_a?(::Plotrb::Transform) }
|
126
|
+
raise ArgumentError, 'Invalid Data Transform'
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
class Format
|
131
|
+
|
132
|
+
include ::Plotrb::Base
|
133
|
+
|
134
|
+
add_attributes :type
|
135
|
+
|
136
|
+
def initialize(type, &block)
|
137
|
+
case type
|
138
|
+
when :json
|
139
|
+
add_attributes(:parse, :property)
|
140
|
+
define_single_val_attributes(:parse, :property)
|
141
|
+
when :csv, :tsv
|
142
|
+
add_attributes(:parse)
|
143
|
+
define_single_val_attribute(:parse)
|
144
|
+
when :topojson
|
145
|
+
add_attributes(:feature, :mesh)
|
146
|
+
define_single_val_attributes(:feature, :mesh)
|
147
|
+
when :treejson
|
148
|
+
add_attributes(:parse, :children)
|
149
|
+
define_single_val_attributes(:parse, :children)
|
150
|
+
else
|
151
|
+
raise ArgumentError, 'Invalid Data format'
|
152
|
+
end
|
153
|
+
@type = type
|
154
|
+
self.instance_eval(&block) if block_given?
|
155
|
+
self
|
156
|
+
end
|
157
|
+
|
158
|
+
def date(*field, &block)
|
159
|
+
@parse ||= {}
|
160
|
+
field.flatten.each { |f| @parse.merge!(f => :date) }
|
161
|
+
self.instance_eval(&block) if block_given?
|
162
|
+
self
|
163
|
+
end
|
164
|
+
alias_method :as_date, :date
|
165
|
+
|
166
|
+
def number(*field, &block)
|
167
|
+
@parse ||= {}
|
168
|
+
field.flatten.each { |f| @parse.merge!(f => :number) }
|
169
|
+
self.instance_eval(&block) if block_given?
|
170
|
+
self
|
171
|
+
end
|
172
|
+
alias_method :as_number, :number
|
173
|
+
|
174
|
+
def boolean(*field, &block)
|
175
|
+
@parse ||= {}
|
176
|
+
field.flatten.each { |f| @parse.merge!(f => :boolean) }
|
177
|
+
self.instance_eval(&block) if block_given?
|
178
|
+
self
|
179
|
+
end
|
180
|
+
alias_method :as_boolean, :boolean
|
181
|
+
|
182
|
+
private
|
183
|
+
|
184
|
+
def attribute_post_processing
|
185
|
+
process_parse
|
186
|
+
process_property
|
187
|
+
process_feature
|
188
|
+
process_mesh
|
189
|
+
process_children
|
190
|
+
end
|
191
|
+
|
192
|
+
def process_parse
|
193
|
+
return unless @parse
|
194
|
+
valid_type = %i(number boolean date)
|
195
|
+
unless @parse.is_a?(Hash) && (@parse.values - valid_type).empty?
|
196
|
+
raise ArgumentError, 'Invalid parse options for Data format'
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def process_property
|
201
|
+
return unless @property
|
202
|
+
unless @property.is_a?(String)
|
203
|
+
raise ArgumentError, 'Invalid JSON property'
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
def process_feature
|
208
|
+
return unless @feature
|
209
|
+
unless @feature.is_a?(String)
|
210
|
+
raise ArgumentError, 'Invalid TopoJSON feature'
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
def process_mesh
|
215
|
+
return unless @mesh
|
216
|
+
unless @mesh.is_a?(String)
|
217
|
+
raise ArgumentError, 'Invalid TopoJSON mesh'
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
def process_children
|
222
|
+
return unless @children
|
223
|
+
unless @children.is_a?(String)
|
224
|
+
raise ArgumentError, 'Invalid TreeJSON children'
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
end
|
229
|
+
|
230
|
+
end
|
231
|
+
|
232
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
module Plotrb
|
2
|
+
|
3
|
+
# Kernel module includes most of the shortcuts used in Plotrb
|
4
|
+
module Kernel
|
5
|
+
|
6
|
+
# a global space keeping track of all Data objects defined
|
7
|
+
def self.data
|
8
|
+
@data ||= []
|
9
|
+
end
|
10
|
+
|
11
|
+
# @return [Data] find Data object by name
|
12
|
+
def self.find_data(name)
|
13
|
+
@data.find { |d| d.name == name.to_s }
|
14
|
+
end
|
15
|
+
|
16
|
+
# @return [Boolean] if a Data object with same name already exists
|
17
|
+
def self.duplicate_data?(name)
|
18
|
+
@data.select { |d| d.name == name.to_s }.size > 1
|
19
|
+
end
|
20
|
+
|
21
|
+
# a global space keeping track of all Axis objects defined
|
22
|
+
def self.axes
|
23
|
+
@axes ||= []
|
24
|
+
end
|
25
|
+
|
26
|
+
# a global space keeping track of all Scale objects defined
|
27
|
+
def self.scales
|
28
|
+
@scales ||= []
|
29
|
+
end
|
30
|
+
|
31
|
+
# @return [Scale] find Scale object by name
|
32
|
+
def self.find_scale(name)
|
33
|
+
@scales.find { |s| s.name == name.to_s }
|
34
|
+
end
|
35
|
+
|
36
|
+
# @return [Boolean] if a Scale object with same name already exists
|
37
|
+
def self.duplicate_scale?(name)
|
38
|
+
@scales.select { |s| s.name == name.to_s }.size > 1
|
39
|
+
end
|
40
|
+
|
41
|
+
# a global space keeping track of all Mark objects defined
|
42
|
+
def self.marks
|
43
|
+
@marks ||= []
|
44
|
+
end
|
45
|
+
|
46
|
+
# @return [Mark] find Mark object by name
|
47
|
+
def self.find_mark(name)
|
48
|
+
@marks.find { |m| m.name == name.to_s }
|
49
|
+
end
|
50
|
+
|
51
|
+
# @return [Boolean] if a Mark object with same name already exists
|
52
|
+
def self.duplicate_mark?(name)
|
53
|
+
@marks.select { |m| m.name == name.to_s }.size > 1
|
54
|
+
end
|
55
|
+
|
56
|
+
# a global space keeping track of all Transform objects defined
|
57
|
+
def self.transforms
|
58
|
+
@transforms ||= []
|
59
|
+
end
|
60
|
+
|
61
|
+
# a global space keeping track of all Transform objects defined
|
62
|
+
def self.legends
|
63
|
+
@legends ||= []
|
64
|
+
end
|
65
|
+
|
66
|
+
# Initialize ::Plotrb::Visualization object
|
67
|
+
|
68
|
+
def visualization(&block)
|
69
|
+
::Plotrb::Visualization.new(&block)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Initialize ::Plotrb::Data objects
|
73
|
+
|
74
|
+
def pdata(&block)
|
75
|
+
::Plotrb::Data.new(&block)
|
76
|
+
end
|
77
|
+
|
78
|
+
def legend(&block)
|
79
|
+
::Plotrb::Legend.new(&block)
|
80
|
+
end
|
81
|
+
|
82
|
+
def method_missing(method, *args, &block)
|
83
|
+
case method.to_s
|
84
|
+
when /^(\w)_axis$/
|
85
|
+
# Initialize ::Plotrb::Axis objects
|
86
|
+
if ::Plotrb::Axis::TYPES.include?($1.to_sym)
|
87
|
+
cache_method($1, 'axis')
|
88
|
+
self.send(method)
|
89
|
+
else
|
90
|
+
super
|
91
|
+
end
|
92
|
+
when /^(\w+)_scale$/
|
93
|
+
# Initialize ::Plotrb::Scale objects
|
94
|
+
if ::Plotrb::Scale::TYPES.include?($1.to_sym)
|
95
|
+
cache_method($1, 'scale')
|
96
|
+
self.send(method)
|
97
|
+
else
|
98
|
+
super
|
99
|
+
end
|
100
|
+
when /^(\w+)_transform$/
|
101
|
+
# Initialize ::Plotrb::Transform objects
|
102
|
+
if ::Plotrb::Transform::TYPES.include?($1.to_sym)
|
103
|
+
cache_method($1, 'transform')
|
104
|
+
self.send(method)
|
105
|
+
else
|
106
|
+
super
|
107
|
+
end
|
108
|
+
when /^(\w+)_mark$/
|
109
|
+
# Initialize ::Plotrb::Mark objects
|
110
|
+
if ::Plotrb::Mark::TYPES.include?($1.to_sym)
|
111
|
+
cache_method($1, 'mark')
|
112
|
+
self.send(method)
|
113
|
+
else
|
114
|
+
super
|
115
|
+
end
|
116
|
+
else
|
117
|
+
super
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
protected
|
122
|
+
|
123
|
+
def cache_method(type, klass)
|
124
|
+
self.class.class_eval {
|
125
|
+
define_method("#{type}_#{klass}") do |&block|
|
126
|
+
# class names are constants
|
127
|
+
# create shortcut methods to initialize Plotrb objects
|
128
|
+
::Kernel::const_get("::Plotrb::#{klass.capitalize}").
|
129
|
+
new(type.to_sym, &block)
|
130
|
+
end
|
131
|
+
}
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
135
|
+
|
136
|
+
end
|
@@ -0,0 +1,168 @@
|
|
1
|
+
module Plotrb
|
2
|
+
|
3
|
+
# Legends visualize scales. Legends aid interpretation of scales with ranges
|
4
|
+
# such as colors, shapes and sizes.
|
5
|
+
# See {https://github.com/trifacta/vega/wiki/Legends}
|
6
|
+
class Legend
|
7
|
+
|
8
|
+
include ::Plotrb::Base
|
9
|
+
|
10
|
+
# @!attribute size
|
11
|
+
# @return [Symbol] the name of the scale that determines an item's size
|
12
|
+
# @!attribute shape
|
13
|
+
# @return [Symbol] the name of the scale that determines an item's shape
|
14
|
+
# @!attribute fill
|
15
|
+
# @return [Symbol] the name of the scale that determines an item's fill color
|
16
|
+
# @!attribute stroke
|
17
|
+
# @return [Symbol] the name of the scale that determines an item's stroke color
|
18
|
+
# @!attribute orient
|
19
|
+
# @return [Symbol] the orientation of the legend
|
20
|
+
# @!attribute title
|
21
|
+
# @return [Symbol] the title for the legend
|
22
|
+
# @!attribute format
|
23
|
+
# @return [String] an optional formatting pattern for legend labels
|
24
|
+
# @!attribute offset
|
25
|
+
# @return [Integer] the offset of the legend
|
26
|
+
# @!attribute values
|
27
|
+
# @return [Array] explicitly set the visible legend values
|
28
|
+
# @!attributes properties
|
29
|
+
# @return [MarkProperty] the property set definitions
|
30
|
+
LEGEND_PROPERTIES = [:size, :shape, :fill, :stroke, :orient, :title,
|
31
|
+
:format, :offset, :values, :properties]
|
32
|
+
|
33
|
+
add_attributes *LEGEND_PROPERTIES
|
34
|
+
|
35
|
+
def initialize(&block)
|
36
|
+
define_single_val_attributes(:size, :shape, :fill, :stroke, :orient,
|
37
|
+
:title, :format, :offset)
|
38
|
+
define_multi_val_attributes(:values)
|
39
|
+
self.singleton_class.class_eval {
|
40
|
+
alias_method :name, :title
|
41
|
+
alias_method :offset_by, :offset
|
42
|
+
}
|
43
|
+
self.instance_eval(&block) if block_given?
|
44
|
+
::Plotrb::Kernel.legends << self
|
45
|
+
self
|
46
|
+
end
|
47
|
+
|
48
|
+
def properties(element=nil, &block)
|
49
|
+
@properties ||= {}
|
50
|
+
return @properties unless element
|
51
|
+
@properties.merge!(
|
52
|
+
element.to_sym => ::Plotrb::Mark::MarkProperty.new(:text, &block)
|
53
|
+
)
|
54
|
+
self
|
55
|
+
end
|
56
|
+
|
57
|
+
def method_missing(method, *args, &block)
|
58
|
+
case method.to_s
|
59
|
+
when /^at_(left|right)$/ # set orient of the legend
|
60
|
+
self.orient($1.to_sym, &block)
|
61
|
+
when /^with_(\d+)_name/ # set the title of the legend
|
62
|
+
self.title($1.to_s, &block)
|
63
|
+
else
|
64
|
+
super
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def attribute_post_processing
|
71
|
+
process_orient
|
72
|
+
process_format
|
73
|
+
process_properties
|
74
|
+
process_size
|
75
|
+
process_shape
|
76
|
+
process_fill
|
77
|
+
process_stroke
|
78
|
+
end
|
79
|
+
|
80
|
+
def process_orient
|
81
|
+
return unless @orient
|
82
|
+
unless %i(left right).include?(@orient.to_sym)
|
83
|
+
raise ArgumentError, 'Invalid Axis orient'
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def process_format
|
88
|
+
return unless @format
|
89
|
+
# D3's format specifier has general form:
|
90
|
+
# [[fill]align][sign][symbol][0][width][,][.precision][type]
|
91
|
+
# the regex is taken from d3/src/format/format.js
|
92
|
+
re =
|
93
|
+
/(?:([^{])?([<>=^]))?([+\- ])?([$#])?(0)?(\d+)?(,)?(\.-?\d+)?([a-z%])?/i
|
94
|
+
@format = @format.to_s
|
95
|
+
if @format =~ re
|
96
|
+
if "#{$1}#{$2}#{$3}#{$4}#{$5}#{$6}#{$7}#{$8}#{$9}" != @format
|
97
|
+
raise ArgumentError, 'Invalid format specifier'
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def process_size
|
103
|
+
return unless @size
|
104
|
+
case @size
|
105
|
+
when String
|
106
|
+
unless ::Plotrb::Kernel.find_scale(@size)
|
107
|
+
raise ArgumentError, 'Scale not found'
|
108
|
+
end
|
109
|
+
when ::Plotrb::Scale
|
110
|
+
@size = @size.name
|
111
|
+
else
|
112
|
+
raise ArgumentError, 'Unknown Scale'
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def process_shape
|
117
|
+
return unless @shape
|
118
|
+
case @shape
|
119
|
+
when String
|
120
|
+
unless ::Plotrb::Kernel.find_scale(@shape)
|
121
|
+
raise ArgumentError, 'Scale not found'
|
122
|
+
end
|
123
|
+
when ::Plotrb::Scale
|
124
|
+
@shape = @shape.name
|
125
|
+
else
|
126
|
+
raise ArgumentError, 'Unknown Scale'
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def process_fill
|
131
|
+
return unless @fill
|
132
|
+
case @fill
|
133
|
+
when String
|
134
|
+
unless ::Plotrb::Kernel.find_scale(@fill)
|
135
|
+
raise ArgumentError, 'Scale not found'
|
136
|
+
end
|
137
|
+
when ::Plotrb::Scale
|
138
|
+
@fill = @fill.name
|
139
|
+
else
|
140
|
+
raise ArgumentError, 'Unknown Scale'
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def process_stroke
|
145
|
+
return unless @stroke
|
146
|
+
case @stroke
|
147
|
+
when String
|
148
|
+
unless ::Plotrb::Kernel.find_scale(@stroke)
|
149
|
+
raise ArgumentError, 'Scale not found'
|
150
|
+
end
|
151
|
+
when ::Plotrb::Scale
|
152
|
+
@stroke = @stroke.name
|
153
|
+
else
|
154
|
+
raise ArgumentError, 'Unknown Scale'
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
|
159
|
+
def process_properties
|
160
|
+
return unless @properties
|
161
|
+
valid_elements = %i(title labels symbols gradient legend)
|
162
|
+
unless (@properties.keys - valid_elements).empty?
|
163
|
+
raise ArgumentError, 'Invalid property element'
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
end
|
168
|
+
end
|