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