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/examples/scatter.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'plotrb'
|
2
|
+
|
3
|
+
raw_data = pdata.name('iris').url('iris_data.json')
|
4
|
+
xs = linear_scale.name('x').from('iris.sepalWidth').to_width.nicely
|
5
|
+
ys = linear_scale.name('y').from('iris.petalLength').to_height.nicely
|
6
|
+
cs = ordinal_scale.name('c').from('iris.species').range(["#800", "#080", "#008"])
|
7
|
+
|
8
|
+
xaxis = x_axis.scale(xs).offset(5).ticks(5).title('Sepal Width')
|
9
|
+
yaxis = y_axis.scale(ys).offset(5).ticks(5).title('Petal Length')
|
10
|
+
|
11
|
+
lgnd = legend.fill(cs).title('Species') do
|
12
|
+
properties(:symbols) do
|
13
|
+
fill_opacity 0.5
|
14
|
+
stroke :transparent
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
mark = symbol_mark.from(raw_data) do
|
19
|
+
enter do
|
20
|
+
x { scale(xs).field('sepalWidth') }
|
21
|
+
y { scale(ys).field('petalLength') }
|
22
|
+
fill { scale(cs).field('species') }
|
23
|
+
fill_opacity 0.5
|
24
|
+
end
|
25
|
+
update do
|
26
|
+
size 100
|
27
|
+
stroke 'transparent'
|
28
|
+
end
|
29
|
+
hover do
|
30
|
+
size 300
|
31
|
+
stroke 'white'
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
vis = visualization.name('arc').width(200).height(200) do
|
36
|
+
data raw_data
|
37
|
+
scales xs, ys, cs
|
38
|
+
axes xaxis, yaxis
|
39
|
+
legends lgnd
|
40
|
+
marks mark
|
41
|
+
end
|
42
|
+
|
43
|
+
puts vis.generate_spec(:pretty)
|
data/lib/plotrb.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'yajl'
|
2
|
+
require 'json'
|
3
|
+
require 'uri'
|
4
|
+
|
5
|
+
require_relative 'plotrb/base'
|
6
|
+
|
7
|
+
require_relative 'plotrb/data'
|
8
|
+
require_relative 'plotrb/transforms'
|
9
|
+
require_relative 'plotrb/scales'
|
10
|
+
require_relative 'plotrb/marks'
|
11
|
+
require_relative 'plotrb/axes'
|
12
|
+
require_relative 'plotrb/kernel'
|
13
|
+
require_relative 'plotrb/visualization'
|
14
|
+
require_relative 'plotrb/legends'
|
15
|
+
require_relative 'plotrb/simple'
|
16
|
+
|
17
|
+
module Plotrb
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
class Object
|
22
|
+
|
23
|
+
include ::Plotrb::Kernel
|
24
|
+
|
25
|
+
end
|
data/lib/plotrb/axes.rb
ADDED
@@ -0,0 +1,208 @@
|
|
1
|
+
module Plotrb
|
2
|
+
|
3
|
+
# Axes provide axis lines, ticks, and labels to convey how a spatial range
|
4
|
+
# represents a data range.
|
5
|
+
# See {https://github.com/trifacta/vega/wiki/Axes}
|
6
|
+
class Axis
|
7
|
+
|
8
|
+
include ::Plotrb::Base
|
9
|
+
|
10
|
+
TYPES = %i(x y)
|
11
|
+
|
12
|
+
TYPES.each do |t|
|
13
|
+
define_singleton_method(t) do |&block|
|
14
|
+
::Plotrb::Axis.new(t, &block)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# @!attribute type
|
19
|
+
# @return [Symbol] type of the axis, either :x or :y
|
20
|
+
# @!attribute scale
|
21
|
+
# @return [String] the name of the scale backing the axis
|
22
|
+
# @!attribute orient
|
23
|
+
# @return [Symbol] the orientation of the axis
|
24
|
+
# @!attribute format
|
25
|
+
# @return [String] the formatting pattern for axis labels
|
26
|
+
# @!attribute ticks
|
27
|
+
# @return [Integer] a desired number of ticks
|
28
|
+
# @!attribute values
|
29
|
+
# @return [Array] explicitly set the visible axis tick values
|
30
|
+
# @!attribute subdivide
|
31
|
+
# @return [Integer] the number of minor ticks between major ticks
|
32
|
+
# @!attribute tick_padding
|
33
|
+
# @return [Integer] the padding between ticks and text labels
|
34
|
+
# @!attribute tick_size
|
35
|
+
# @return [Integer] the size of major, minor, and end ticks
|
36
|
+
# @!attribute tick_size_major
|
37
|
+
# @return [Integer] the size of major ticks
|
38
|
+
# @!attribute tick_size_minor
|
39
|
+
# @return [Integer] the size of minor ticks
|
40
|
+
# @!attribute tick_size_end
|
41
|
+
# @return [Integer] the size of end ticks
|
42
|
+
# @!attribute offset
|
43
|
+
# @return [Integer] the offset by which to displace the axis from the edge
|
44
|
+
# of the enclosing group or data rectangle
|
45
|
+
# @!attribute properties
|
46
|
+
# @return [Hash] optional mark property definitions for custom styling
|
47
|
+
# @!attribute title
|
48
|
+
# @return [String] the title for the axis
|
49
|
+
# @!attribute tittle_offset
|
50
|
+
# @return [Integer] the offset from the axis at which to place the title
|
51
|
+
# @!attribute grid
|
52
|
+
# @return [Boolean] whether gridlines should be created
|
53
|
+
add_attributes :type, :scale, :orient, :format, :ticks, :values, :subdivide,
|
54
|
+
:tick_padding, :tick_size, :tick_size_major, :tick_size_minor,
|
55
|
+
:tick_size_end, :offset, :layer, :properties, :title,
|
56
|
+
:title_offset, :grid
|
57
|
+
|
58
|
+
def initialize(type, &block)
|
59
|
+
@type = type
|
60
|
+
define_single_val_attributes(:scale, :orient, :title, :title_offset,
|
61
|
+
:format, :ticks, :subdivide, :tick_padding,
|
62
|
+
:tick_size, :tick_size_major, :tick_size_end,
|
63
|
+
:tick_size_minor, :offset, :layer)
|
64
|
+
define_boolean_attribute(:grid)
|
65
|
+
define_multi_val_attributes(:values)
|
66
|
+
self.singleton_class.class_eval {
|
67
|
+
alias_method :from, :scale
|
68
|
+
alias_method :offset_title_by, :title_offset
|
69
|
+
alias_method :subdivide_by, :subdivide
|
70
|
+
alias_method :major_tick_size, :tick_size_major
|
71
|
+
alias_method :minor_tick_size, :tick_size_minor
|
72
|
+
alias_method :end_tick_size, :tick_size_end
|
73
|
+
alias_method :offset_by, :offset
|
74
|
+
alias_method :show_grid, :grid
|
75
|
+
alias_method :with_grid, :grid
|
76
|
+
alias_method :show_grid?, :grid?
|
77
|
+
alias_method :with_grid?, :grid?
|
78
|
+
}
|
79
|
+
self.instance_eval(&block) if block_given?
|
80
|
+
::Plotrb::Kernel.axes << self
|
81
|
+
self
|
82
|
+
end
|
83
|
+
|
84
|
+
def type
|
85
|
+
@type
|
86
|
+
end
|
87
|
+
|
88
|
+
def above(&block)
|
89
|
+
@layer = :front
|
90
|
+
self.instance_eval(&block) if block_given?
|
91
|
+
self
|
92
|
+
end
|
93
|
+
|
94
|
+
def above?
|
95
|
+
@layer == :front
|
96
|
+
end
|
97
|
+
|
98
|
+
def below(&block)
|
99
|
+
@layer = :back
|
100
|
+
self.instance_eval(&block) if block_given?
|
101
|
+
self
|
102
|
+
end
|
103
|
+
|
104
|
+
def below?
|
105
|
+
@layer == :back
|
106
|
+
end
|
107
|
+
|
108
|
+
def no_grid(&block)
|
109
|
+
@grid = false
|
110
|
+
self.instance_eval(&block) if block
|
111
|
+
self
|
112
|
+
end
|
113
|
+
|
114
|
+
def properties(element=nil, &block)
|
115
|
+
@properties ||= {}
|
116
|
+
return @properties unless element
|
117
|
+
@properties.merge!(
|
118
|
+
element.to_sym => ::Plotrb::Mark::MarkProperty.new(:text, &block)
|
119
|
+
)
|
120
|
+
self
|
121
|
+
end
|
122
|
+
|
123
|
+
def method_missing(method, *args, &block)
|
124
|
+
case method.to_s
|
125
|
+
when /^with_(\d+)_ticks$/ # set number of ticks. eg. in_20_ticks
|
126
|
+
self.ticks($1.to_i, &block)
|
127
|
+
when /^subdivide_by_(\d+)$/ # set subdivide of ticks
|
128
|
+
self.subdivide($1.to_i, &block)
|
129
|
+
when /^at_(top|bottom|left|right)$/ # set orient of the axis
|
130
|
+
self.orient($1.to_sym, &block)
|
131
|
+
when /^in_(front|back)$/ # set layer of the axis
|
132
|
+
self.layer($1.to_sym, &block)
|
133
|
+
else
|
134
|
+
super
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
private
|
139
|
+
|
140
|
+
def attribute_post_processing
|
141
|
+
process_type
|
142
|
+
process_scale
|
143
|
+
process_orient
|
144
|
+
process_format
|
145
|
+
process_layer
|
146
|
+
process_properties
|
147
|
+
end
|
148
|
+
|
149
|
+
def process_type
|
150
|
+
unless TYPES.include?(@type)
|
151
|
+
raise ArgumentError, 'Invalid Axis type'
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def process_scale
|
156
|
+
return unless @scale
|
157
|
+
case @scale
|
158
|
+
when String
|
159
|
+
unless ::Plotrb::Kernel.find_scale(@scale)
|
160
|
+
raise ArgumentError, 'Scale not found'
|
161
|
+
end
|
162
|
+
when ::Plotrb::Scale
|
163
|
+
@scale = @scale.name
|
164
|
+
else
|
165
|
+
raise ArgumentError, 'Unknown Scale'
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def process_orient
|
170
|
+
return unless @orient
|
171
|
+
unless %i(top bottom left right).include?(@orient.to_sym)
|
172
|
+
raise ArgumentError, 'Invalid Axis orient'
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def process_format
|
177
|
+
return unless @format
|
178
|
+
# D3's format specifier has general form:
|
179
|
+
# [[fill]align][sign][symbol][0][width][,][.precision][type]
|
180
|
+
# the regex is taken from d3/src/format/format.js
|
181
|
+
re =
|
182
|
+
/(?:([^{])?([<>=^]))?([+\- ])?([$#])?(0)?(\d+)?(,)?(\.-?\d+)?([a-z%])?/i
|
183
|
+
@format = @format.to_s
|
184
|
+
if @format =~ re
|
185
|
+
if "#{$1}#{$2}#{$3}#{$4}#{$5}#{$6}#{$7}#{$8}#{$9}" != @format
|
186
|
+
raise ArgumentError, 'Invalid format specifier'
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def process_layer
|
192
|
+
return unless @layer
|
193
|
+
unless %i(front back).include?(@layer.to_sym)
|
194
|
+
raise ArgumentError, 'Invalid Axis layer'
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def process_properties
|
199
|
+
return unless @properties
|
200
|
+
valid_elements = %i(ticks major_ticks minor_ticks grid labels axis)
|
201
|
+
unless (@properties.keys - valid_elements).empty?
|
202
|
+
raise ArgumentError, 'Invalid property element'
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
end
|
207
|
+
|
208
|
+
end
|
data/lib/plotrb/base.rb
ADDED
@@ -0,0 +1,193 @@
|
|
1
|
+
module Plotrb
|
2
|
+
|
3
|
+
# Some internal methods for mixin
|
4
|
+
module Base
|
5
|
+
|
6
|
+
def self.included(base)
|
7
|
+
base.extend(ClassMethods)
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
|
12
|
+
# add setter methods to attributes
|
13
|
+
def add_attributes(*vars)
|
14
|
+
@attributes ||= []
|
15
|
+
@attributes.concat(vars)
|
16
|
+
vars.each do |var|
|
17
|
+
define_method("#{var}=") { |value|
|
18
|
+
instance_variable_set("@#{var}", value)
|
19
|
+
}
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def attributes
|
24
|
+
@attributes
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
# @return [Array<Symbol>] attributes of the particular instance combined
|
30
|
+
# with attributes of the class
|
31
|
+
def attributes
|
32
|
+
singleton_attr = self.singleton_class.attributes || []
|
33
|
+
class_attr = self.class.attributes || []
|
34
|
+
singleton_attr.concat(class_attr).uniq
|
35
|
+
end
|
36
|
+
|
37
|
+
# add and set new attributes and values to the instance
|
38
|
+
# @param args [Hash] attributes in the form of a Hash
|
39
|
+
def set_attributes(args)
|
40
|
+
args.each do |k, v|
|
41
|
+
# use singleton_class as attributes are instance-specific
|
42
|
+
self.singleton_class.add_attributes(k)
|
43
|
+
self.instance_variable_set("@#{k}", v) unless v.nil?
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# add new attributes to the instance
|
48
|
+
# @param args [Array<Symbol>] the attributes to add to the instance
|
49
|
+
def add_attributes(*args)
|
50
|
+
self.singleton_class.add_attributes(*args)
|
51
|
+
end
|
52
|
+
|
53
|
+
# @return [Array<Symbol>] attributes that have values
|
54
|
+
def defined_attributes
|
55
|
+
attributes.reject { |attr| self.instance_variable_get("@#{attr}").nil? }
|
56
|
+
end
|
57
|
+
|
58
|
+
# to be implemented in each Plotrb class
|
59
|
+
def attribute_post_processing
|
60
|
+
raise NotImplementedError
|
61
|
+
end
|
62
|
+
|
63
|
+
# @return [Hash] recursively construct a massive hash
|
64
|
+
def collect_attributes
|
65
|
+
attribute_post_processing
|
66
|
+
collected = {}
|
67
|
+
defined_attributes.each do |attr|
|
68
|
+
value = self.instance_variable_get("@#{attr}")
|
69
|
+
# change snake_case attributes to camelCase used in Vega's JSON spec
|
70
|
+
json_attr = classify(attr, :json)
|
71
|
+
if value.respond_to?(:collect_attributes)
|
72
|
+
collected[json_attr] = value.collect_attributes
|
73
|
+
elsif value.is_a?(Array)
|
74
|
+
collected[json_attr] = [].concat(value.collect{ |v|
|
75
|
+
v.respond_to?(:collect_attributes) ? v.collect_attributes : v
|
76
|
+
})
|
77
|
+
else
|
78
|
+
collected[json_attr] = value
|
79
|
+
end
|
80
|
+
end
|
81
|
+
collected
|
82
|
+
end
|
83
|
+
|
84
|
+
def define_boolean_attribute(method)
|
85
|
+
# when setting boolean values, eg. foo.bar sets bar attribute to true,
|
86
|
+
# foo.bar? returns state of bar attribute
|
87
|
+
define_singleton_method(method) do |&block|
|
88
|
+
self.instance_variable_set("@#{method}", true)
|
89
|
+
self.instance_eval(&block) if block
|
90
|
+
self
|
91
|
+
end
|
92
|
+
define_singleton_method("#{method}?") do
|
93
|
+
self.instance_variable_get("@#{method}")
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def define_boolean_attributes(*methods)
|
98
|
+
methods.each { |m| define_boolean_attribute(m) }
|
99
|
+
end
|
100
|
+
|
101
|
+
def define_single_val_attribute(method, proc=nil)
|
102
|
+
# when only single value is allowed, eg. foo.bar(1)
|
103
|
+
# proc is passed in to process value before assigning to attributes
|
104
|
+
define_singleton_method(method) do |*args, &block|
|
105
|
+
case args.size
|
106
|
+
when 0
|
107
|
+
self.instance_variable_get("@#{method}")
|
108
|
+
when 1
|
109
|
+
val = proc.is_a?(Proc) ? proc.call(args[0]) : args[0]
|
110
|
+
self.instance_variable_set("@#{method}", val)
|
111
|
+
self.instance_eval(&block) if block
|
112
|
+
self
|
113
|
+
else
|
114
|
+
raise ArgumentError
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def define_single_val_attributes(*methods)
|
120
|
+
methods.each { |m| define_single_val_attribute(m) }
|
121
|
+
end
|
122
|
+
|
123
|
+
def define_multi_val_attribute(method, proc=nil)
|
124
|
+
# when multiple values are allowed, eg. foo.bar(1,2) or foo.bar([1,2])
|
125
|
+
# proc is passed in to process values before assigning to attributes
|
126
|
+
define_singleton_method(method) do |*args, &block|
|
127
|
+
case args.size
|
128
|
+
when 0
|
129
|
+
self.instance_variable_get("@#{method}")
|
130
|
+
else
|
131
|
+
vals = proc.is_a?(Proc) ? proc.call(*args) : [args].flatten
|
132
|
+
self.instance_variable_set("@#{method}", vals)
|
133
|
+
self.instance_eval(&block) if block
|
134
|
+
self
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def define_multi_val_attributes(*methods)
|
140
|
+
methods.each { |m| define_multi_val_attribute(m) }
|
141
|
+
end
|
142
|
+
|
143
|
+
def classify(name, format=nil)
|
144
|
+
klass = name.to_s.split('_').collect(&:capitalize).join
|
145
|
+
if format == :json
|
146
|
+
klass[0].downcase + klass[1..-1]
|
147
|
+
else
|
148
|
+
klass
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# monkey patch Hash class to support reverse_merge and collect_attributes
|
153
|
+
class ::Hash
|
154
|
+
|
155
|
+
def attribute_post_processing
|
156
|
+
# nothing to do for Hash
|
157
|
+
end
|
158
|
+
|
159
|
+
def reverse_merge(other_hash)
|
160
|
+
other_hash.merge(self)
|
161
|
+
end
|
162
|
+
|
163
|
+
def collect_attributes
|
164
|
+
collected = {}
|
165
|
+
self.each do |k, v|
|
166
|
+
json_attr = classify(k, :json)
|
167
|
+
if v.respond_to?(:collect_attributes)
|
168
|
+
collected[json_attr] = v.collect_attributes
|
169
|
+
elsif v.is_a?(Array)
|
170
|
+
collected[json_attr] = [].concat(v.collect{ |va|
|
171
|
+
va.respond_to?(:collect_attributes) ? va.collect_attributes : va
|
172
|
+
})
|
173
|
+
else
|
174
|
+
collected[json_attr] = v
|
175
|
+
end
|
176
|
+
end
|
177
|
+
collected
|
178
|
+
end
|
179
|
+
|
180
|
+
def classify(name, format=nil)
|
181
|
+
klass = name.to_s.split('_').collect(&:capitalize).join
|
182
|
+
if format == :json
|
183
|
+
klass[0].downcase + klass[1..-1]
|
184
|
+
else
|
185
|
+
klass
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
end
|
190
|
+
|
191
|
+
end
|
192
|
+
|
193
|
+
end
|