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