├── .gitignore
├── .rspec
├── .travis.yml
├── Gemfile
├── Gemfile.lock
├── LICENSE.txt
├── README.rdoc
├── Rakefile
├── examples
├── arc.rb
├── area.rb
├── bar.rb
├── barley.rb
├── choropleth.rb
└── lifelines.rb
├── lib
├── plotrb.rb
└── plotrb
│ ├── axes.rb
│ ├── base.rb
│ ├── data.rb
│ ├── kernel.rb
│ ├── marks.rb
│ ├── scales.rb
│ ├── transforms.rb
│ ├── version.rb
│ └── visualization.rb
├── plotrb.gemspec
└── spec
├── plotrb
├── axes_spec.rb
├── base_spec.rb
├── data_spec.rb
├── kernel_spec.rb
├── marks_spec.rb
├── scales_spec.rb
├── transforms_spec.rb
└── visualization_spec.rb
├── plotrb_spec.rb
└── spec_helper.rb
/.gitignore:
--------------------------------------------------------------------------------
1 | *.gem
2 | *.rbc
3 | .bundle
4 | .config
5 | .yardoc
6 | Gemfile.lock
7 | InstalledFiles
8 | _yardoc
9 | coverage
10 | doc/
11 | lib/bundler/man
12 | pkg
13 | rdoc
14 | spec/reports
15 | test/tmp
16 | test/version_tmp
17 | tmp
18 | .idea
19 | .DS_Store
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --colour --format documentation
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: ruby
2 | rvm:
3 | - 2.0.0
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'http://rubygems.org'
2 | gemspec
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | PATH
2 | remote: .
3 | specs:
4 | plotrb (0.0.1)
5 | activemodel
6 | yajl-ruby
7 |
8 | GEM
9 | remote: http://rubygems.org/
10 | specs:
11 | activemodel (4.0.0)
12 | activesupport (= 4.0.0)
13 | builder (~> 3.1.0)
14 | activesupport (4.0.0)
15 | i18n (~> 0.6, >= 0.6.4)
16 | minitest (~> 4.2)
17 | multi_json (~> 1.3)
18 | thread_safe (~> 0.1)
19 | tzinfo (~> 0.3.37)
20 | atomic (1.1.10)
21 | builder (3.1.4)
22 | diff-lcs (1.2.4)
23 | i18n (0.6.4)
24 | minitest (4.7.3)
25 | multi_json (1.7.7)
26 | rake (10.0.4)
27 | rspec (2.13.0)
28 | rspec-core (~> 2.13.0)
29 | rspec-expectations (~> 2.13.0)
30 | rspec-mocks (~> 2.13.0)
31 | rspec-core (2.13.1)
32 | rspec-expectations (2.13.0)
33 | diff-lcs (>= 1.1.3, < 2.0)
34 | rspec-mocks (2.13.1)
35 | thread_safe (0.1.0)
36 | atomic
37 | tzinfo (0.3.37)
38 | yajl-ruby (1.1.0)
39 | yard (0.8.6.1)
40 |
41 | PLATFORMS
42 | ruby
43 |
44 | DEPENDENCIES
45 | bundler (~> 1.3)
46 | plotrb!
47 | rake
48 | rspec
49 | yard
50 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | This version of Plotrb is licensed under the BSD 2-clause license.
2 |
3 | * http://sciruby.com
4 | * http://github.com/sciruby/sciruby/wiki/License
5 |
6 | You *must* read the Contributor Agreement before contributing code to
7 | the SciRuby Project. This is available online:
8 |
9 | * http://github.com/sciruby/sciruby/wiki/Contributor-Agreement
10 |
11 | -----
12 |
13 | Copyright (c) 2010 - 2013, Wan Zuhao and the Ruby Science Foundation
14 | All rights reserved.
15 |
16 | Redistribution and use in source and binary forms, with or without
17 | modification, are permitted provided that the following conditions are
18 | met:
19 |
20 | * Redistributions of source code must retain the above copyright notice,
21 | this list of conditions and the following disclaimer.
22 |
23 | * Redistributions in binary form must reproduce the above copyright
24 | notice, this list of conditions and the following disclaimer in the
25 | documentation and/or other materials provided with the distribution.
26 |
27 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
28 | IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
29 | TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
30 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
31 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
32 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
33 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
34 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
35 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
36 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
37 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
38 |
39 |
--------------------------------------------------------------------------------
/README.rdoc:
--------------------------------------------------------------------------------
1 | = Plotrb
2 |
3 | Vega/D3-based plotting gem for Ruby
4 |
5 | * {sciruby.com}[http://sciruby.com]
6 | * {Google+}[https://plus.google.com/109304769076178160953/posts]
7 | * {Wan Zuhao}[http://www.wanzuhao.com]
8 | * {Examples on bl.ocks.org}[http://bl.ocks.org/zuhao]
9 |
10 | {
}[https://travis-ci.org/zuhao/plotrb]
11 |
12 | == Description
13 |
14 | Plotrb is an experimental plotting library for the Ruby language. It
15 | brings {the simplicity of Vega}[http://trifacta.github.io/vega/] and
16 | {the power of D3}[http://d3js.org/] to the Ruby science community, to
17 | allow for {fast, customizable design; reusability and shareability; programmatic generation of visualizations; and improved performance and platform flexibility}[https://github.com/trifacta/vega/wiki/Vega-and-D3].
18 |
19 | Plotrb is in many ways the child of
20 | {Rubyvis}[http://github.com/SciRuby/rubyvis] (the Ruby port of
21 | Protovis). However, it improves upon Rubyvis in that it uses a DSL that
22 | will be much more familiar and natural to Ruby coders.
23 |
24 | Plotrb was created by Wan Zuhao as part of Google's Summer of Code 2013.
25 | It is part of {SciRuby}[http://sciruby.com].
26 |
27 | == Installation
28 |
29 | Add this line to your application's Gemfile:
30 |
31 | gem 'plotrb'
32 |
33 | And then execute:
34 |
35 | bundle
36 |
37 | Or install it yourself as:
38 |
39 | gem install plotrb
40 |
41 | == Usage
42 |
43 | Currently, Plotrb is intended to generate JSON specification for Vega. If you would like to see the visualization rendered in PNG or SVG, you will have to install {Vega}[https://github.com/trifacta/vega] and use its {Headless Mode}[https://github.com/trifacta/vega/wiki/Headless-Mode].
44 |
45 | Note that Plotrb is a work in progress, so we can't guarantee that every
46 | example will work exactly.
47 |
48 | * {Arc}[http://bl.ocks.org/zuhao/6663966]
49 | * {Area}[http://bl.ocks.org/zuhao/6663999]
50 | * {Bar}[http://bl.ocks.org/zuhao/6662333]
51 | * {Barley}[http://bl.ocks.org/zuhao/6664828]
52 | * {Lifelines}[http://bl.ocks.org/zuhao/6664107]
53 |
54 | Examples may also be found in the `examples/` directory.
55 |
56 | == Contributing
57 |
58 | 1. Fork it
59 | 2. Create your feature branch (`git checkout -b my-new-feature`)
60 | 3. Commit your changes (`git commit -am 'Add some feature'`)
61 | 4. Push to the branch (`git push origin my-new-feature`)
62 | 5. Create new Pull Request
63 |
64 | == License
65 |
66 | Copyright (c) 2013, Wan Zuhao and the Ruby Science Foundation.
67 |
68 | All rights reserved.
69 |
70 | Plotrb, along with SciRuby, is licensed under the BSD 2-clause license. See
71 | {LICENSE.txt}[https://github.com/SciRuby/sciruby/wiki/License] for details.
72 |
73 | == Donations
74 |
75 | Support a SciRuby Fellow:
76 |
77 | {
}[http://www.pledgie.com/campaigns/15783]
78 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'bundler/gem_tasks'
2 | require 'rspec/core/rake_task'
3 |
4 | RSpec::Core::RakeTask.new
5 |
6 | task :default => :spec
7 | task :test => :spec
--------------------------------------------------------------------------------
/examples/arc.rb:
--------------------------------------------------------------------------------
1 | require 'plotrb'
2 |
3 | data = pdata.name('table').values([12,23,47,6,52,19]).transform(pie_transform)
4 |
5 | scale = sqrt_scale.name('r').from(data).to([20,100])
6 |
7 | mark = arc_mark.from(data) do
8 | enter do
9 | x_start { group(:width).times(0.5) }
10 | y_start { group(:height).times(0.5) }
11 | start_angle { from :start_angle }
12 | end_angle { from :end_angle }
13 | inner_radius 20
14 | outer_radius { scale(scale) }
15 | stroke '#fff'
16 | end
17 | update do
18 | fill '#ccc'
19 | end
20 | hover do
21 | fill 'pink'
22 | end
23 | end
24 |
25 | vis = visualization.name('arc').width(400).height(400) do
26 | data data
27 | scales scale
28 | marks mark
29 | end
30 |
31 | puts vis.generate_spec(:pretty)
32 |
--------------------------------------------------------------------------------
/examples/area.rb:
--------------------------------------------------------------------------------
1 | require './lib/plotrb.rb'
2 |
3 | data = pdata.name('table').values(
4 | [
5 | {x: 1, y: 28}, {x: 2, y: 55},
6 | {x: 3, y: 43}, {x: 4, y: 91},
7 | {x: 5, y: 81}, {x: 6, y: 53},
8 | {x: 7, y: 19}, {x: 8, y: 87},
9 | {x: 9, y: 52}, {x: 10, y: 48},
10 | {x: 11, y: 24}, {x: 12, y: 49},
11 | {x: 13, y: 87}, {x: 14, y: 66},
12 | {x: 15, y: 17}, {x: 16, y: 27},
13 | {x: 17, y: 68}, {x: 18, y: 16},
14 | {x: 19, y: 49}, {x: 20, y: 15}
15 | ]
16 | )
17 |
18 | xs = linear_scale.name('x').from('table.x').to_width.exclude_zero
19 | ys = linear_scale.name('y').from('table.y').to_height.nicely
20 |
21 | xa = x_axis.scale(xs).ticks(20)
22 | ya = y_axis.scale(ys)
23 |
24 | mark = area_mark.from(data) do
25 | enter do
26 | interpolate :monotone
27 | x_start {from('x').scale(xs)}
28 | y_start {from('y').scale(ys)}
29 | y_end {value(0).scale(ys)}
30 | fill 'steelblue'
31 | end
32 | update do
33 | fill_opacity 1
34 | end
35 | hover do
36 | fill_opacity 0.5
37 | end
38 | end
39 |
40 | vis = visualization.name('area').width(500).height(200) do
41 | padding :top => 10, :left => 30, :bottom => 30, :right => 10
42 | data data
43 | scales xs, ys
44 | axes xa, ya
45 | marks mark
46 | end
47 |
48 | puts vis.generate_spec(:pretty)
49 |
--------------------------------------------------------------------------------
/examples/bar.rb:
--------------------------------------------------------------------------------
1 | require 'plotrb'
2 |
3 | data = pdata.name('table').values(
4 | [
5 | {x: 1, y: 28}, {x: 2, y: 55},
6 | {x: 3, y: 43}, {x: 4, y: 91},
7 | {x: 5, y: 81}, {x: 6, y: 53},
8 | {x: 7, y: 19}, {x: 8, y: 87},
9 | {x: 9, y: 52}, {x: 10, y: 48},
10 | {x: 11, y: 24}, {x: 12, y: 49},
11 | {x: 13, y: 87}, {x: 14, y: 66},
12 | {x: 15, y: 17}, {x: 16, y: 27},
13 | {x: 17, y: 68}, {x: 18, y: 16},
14 | {x: 19, y: 49}, {x: 20, y: 15}
15 | ]
16 | )
17 |
18 | xs = ordinal_scale.name('x').from('table.x').to_width
19 | ys = linear_scale.name('y').from('table.y').nicely.to_height
20 |
21 | mark = rect_mark.from(data) do
22 | enter do
23 | x_start { scale(xs).from('x') }
24 | width { scale(xs).offset(-1).use_band }
25 | y_start { scale(ys).from('y') }
26 | y_end { scale(ys).value(0) }
27 | end
28 | update do
29 | fill 'steelblue'
30 | end
31 | hover do
32 | fill 'red'
33 | end
34 | end
35 |
36 | vis = visualization.width(400).height(200) do
37 | padding top: 10, left: 30, bottom: 30, right: 10
38 | data data
39 | scales xs, ys
40 | marks mark
41 | axes x_axis.scale(xs), y_axis.scale(ys)
42 | end
43 |
44 | puts vis.generate_spec(:pretty)
45 |
--------------------------------------------------------------------------------
/examples/barley.rb:
--------------------------------------------------------------------------------
1 | require './lib/plotrb.rb'
2 |
3 | raw_data = pdata.name('barley').url('barley_data.json')
4 | variety = pdata.name('variety').source('barley').transform [
5 | facet_transform.keys('variety'),
6 | stats_transform.value('yield').median,
7 | sort_transform.by('-median')
8 | ]
9 | site = pdata.name('site').source('barley').transform [
10 | facet_transform.keys('site'),
11 | stats_transform.value('yield').median,
12 | sort_transform.by('-median')
13 | ]
14 |
15 | gs = ordinal_scale.name('g').padding(0.15).from('site.key').to_height
16 | xs = linear_scale.name('x').from('barley.yield').to_width.nicely
17 | cs = ordinal_scale.name('c').to_colors
18 | ys = ordinal_scale.name('y').from('variety.key').to_height.as_points.padding(1.2)
19 | xaxis = x_axis.scale(xs).offset(-12)
20 | yaxis = y_axis.scale(ys).tick_size(0) do
21 | properties(:axis) { stroke :transparent }
22 | end
23 |
24 |
25 | tm = text_mark.from(site) do
26 | enter do
27 | x { group(:width).times(0.5) }
28 | y { scale(gs).field(:key).offset(-2) }
29 | font_weight :bold
30 | text { field(:key) }
31 | align :center
32 | baseline :bottom
33 | fill '#000'
34 | end
35 | end
36 |
37 | sm = symbol_mark.enter do
38 | x { scale(xs).field('yield') }
39 | y { scale(ys).field('variety') }
40 | size 50
41 | stroke { scale(cs).field('year') }
42 | stroke_width 2
43 | end
44 |
45 | gm = group_mark.from(site) do
46 | scales ys
47 | axes yaxis
48 | marks sm
49 | enter do
50 | x 0.5
51 | y { scale(gs).field(:key) }
52 | height { scale(gs).use_band }
53 | width { group(:width) }
54 | stroke '#ccc'
55 | end
56 | end
57 |
58 | vis = visualization.width(200).height(720) do
59 | data raw_data, variety, site
60 | scales gs, cs, xs, ys
61 | axes xaxis
62 | marks tm, gm
63 | end
64 |
65 | puts vis.generate_spec(:pretty)
66 |
67 |
--------------------------------------------------------------------------------
/examples/choropleth.rb:
--------------------------------------------------------------------------------
1 | require './lib/plotrb.rb'
2 |
3 | ump_data = pdata.name('unemp') do
4 | url('unemployment.tsv')
5 | format(:tsv) { parse :rate => :number }
6 | end
7 | cty_data = pdata.name('counties') do
8 | url('us-10m.json')
9 | format(:topojson) { feature 'counties' }
10 | transform [
11 | geopath_transform.projection(:albersUsa),
12 | zip_transform.with('unemp').match('id').against('id').as('value').default(nil),
13 | filter_transform.test('d.path!=null && d.value!=null')
14 | ]
15 | end
16 |
17 | cs = quantize_scale.name('color').from([0, 0.15]).to(
18 | [
19 | "#f7fbff",
20 | "#deebf7",
21 | "#c6dbef",
22 | "#9ecae1",
23 | "#6baed6",
24 | "#4292c6",
25 | "#2171b5",
26 | "#08519c",
27 | "#08306b"
28 | ])
29 |
30 | mark = path_mark.from(cty_data) do
31 | enter do
32 | path { from 'path' }
33 | end
34 | update do
35 | fill { scale(cs).from('value.data.rate') }
36 | end
37 | hover do
38 | fill 'red'
39 | end
40 | end
41 |
42 | vis = visualization.width(960).height(500) do
43 | data ump_data, cty_data
44 | scales cs
45 | marks mark
46 | end
47 |
48 | puts vis.generate_spec(:pretty)
49 |
--------------------------------------------------------------------------------
/examples/lifelines.rb:
--------------------------------------------------------------------------------
1 | require './lib/plotrb.rb'
2 |
3 | people = pdata.name('people').values(
4 | [
5 | {"label" => "Washington",
6 | "born" => -7506057600000,
7 | "died" => -5366196000000,
8 | "enter" => -5701424400000,
9 | "leave" => -5453884800000},
10 | {"label" => "Adams",
11 | "born" => -7389766800000,
12 | "died" => -4528285200000,
13 | "enter" => -5453884800000,
14 | "leave" => -5327740800000},
15 | {"label" => "Jefferson",
16 | "born" => -7154586000000,
17 | "died" => -4528285200000,
18 | "enter" => -5327740800000,
19 | "leave" => -5075280000000},
20 | {"label" => "Madison",
21 | "born" => -6904544400000,
22 | "died" => -4213184400000,
23 | "enter" => -5075280000000,
24 | "leave" => -4822819200000},
25 | {"label" => "Monroe",
26 | "born" => -6679904400000,
27 | "died" => -4370518800000,
28 | "enter" => -4822819200000,
29 | "leave" => -4570358400000}
30 | ]
31 | )
32 |
33 | events = pdata.name('events') do
34 | format(:json) { parse 'when' => :date }
35 | values [
36 | {"name" => "Decl. of Independence", "when" => "July 4, 1776"},
37 | {"name" => "U.S. Constitution", "when" => "3/4/1789"},
38 | {"name" => "Louisiana Purchase", "when" => "April 30, 1803"},
39 | {"name" => "Monroe Doctrine", "when" => "Dec 2, 1823"}
40 | ]
41 | end
42 |
43 | y_scale = ordinal_scale.name('y').from('people.label').to_height
44 | x_scale = time_scale.name('x').from(['people.born', 'people.died']).to_width.round.in_years
45 |
46 | events_mark_t = text_mark.from(events) do
47 | enter do
48 | x_start { scale(x_scale).from('when') }
49 | y_start -10
50 | angle -25
51 | fill '#000'
52 | text { from 'name' }
53 | font 'Helvetica Neue'
54 | font_size 10
55 | end
56 | end
57 |
58 | events_mark_r = rect_mark.from(events) do
59 | enter do
60 | x_start { scale(x_scale).from('when') }
61 | y_start -8
62 | width 1
63 | height { group(:height).offset(8) }
64 | fill '#888'
65 | end
66 | end
67 |
68 | people_mark_t = text_mark.from(people) do
69 | enter do
70 | x_start { scale(x_scale).from('born') }
71 | y_start { scale(y_scale).from('label').offset(-3) }
72 | fill '#000'
73 | text { from('label') }
74 | font 'Helvetica Neue'
75 | font_size 10
76 | end
77 | end
78 |
79 | people_mark_r = rect_mark.from(people) do
80 | enter do
81 | x_start { scale(x_scale).from('born') }
82 | x_end { scale(x_scale).from('died') }
83 | y_start { scale(y_scale).from('label') }
84 | height 2
85 | fill '#557'
86 | end
87 | end
88 |
89 | people_mark_r2 = rect_mark.from(people) do
90 | enter do
91 | x_start { scale(x_scale).from('enter') }
92 | x_end { scale(x_scale).from('leave') }
93 | y_start { scale(y_scale).from('label').offset(-1) }
94 | height 4
95 | fill '#e44'
96 | end
97 | end
98 |
99 | vis = visualization.name('lifelines').width(400).height(100) do
100 | data people, events
101 | scales x_scale, y_scale
102 | axes x_axis.scale('x')
103 | marks events_mark_t, events_mark_r, people_mark_t, people_mark_r, people_mark_r2
104 | end
105 |
106 | puts vis.generate_spec(:pretty)
107 |
--------------------------------------------------------------------------------
/lib/plotrb.rb:
--------------------------------------------------------------------------------
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 |
15 | module Plotrb
16 |
17 | end
18 |
19 | class Object
20 |
21 | include ::Plotrb::Kernel
22 |
23 | end
24 |
--------------------------------------------------------------------------------
/lib/plotrb/axes.rb:
--------------------------------------------------------------------------------
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
209 |
--------------------------------------------------------------------------------
/lib/plotrb/base.rb:
--------------------------------------------------------------------------------
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] 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] 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] 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
194 |
--------------------------------------------------------------------------------
/lib/plotrb/data.rb:
--------------------------------------------------------------------------------
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] 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
233 |
--------------------------------------------------------------------------------
/lib/plotrb/kernel.rb:
--------------------------------------------------------------------------------
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 | # Initialize ::Plotrb::Visualization object
62 |
63 | def visualization(&block)
64 | ::Plotrb::Visualization.new(&block)
65 | end
66 |
67 | # Initialize ::Plotrb::Data objects
68 |
69 | def pdata(&block)
70 | ::Plotrb::Data.new(&block)
71 | end
72 |
73 | def method_missing(method, *args, &block)
74 | case method.to_s
75 | when /^(\w)_axis$/
76 | # Initialize ::Plotrb::Axis objects
77 | if ::Plotrb::Axis::TYPES.include?($1.to_sym)
78 | cache_method($1, 'axis')
79 | self.send(method)
80 | else
81 | super
82 | end
83 | when /^(\w+)_scale$/
84 | # Initialize ::Plotrb::Scale objects
85 | if ::Plotrb::Scale::TYPES.include?($1.to_sym)
86 | cache_method($1, 'scale')
87 | self.send(method)
88 | else
89 | super
90 | end
91 | when /^(\w+)_transform$/
92 | # Initialize ::Plotrb::Transform objects
93 | if ::Plotrb::Transform::TYPES.include?($1.to_sym)
94 | cache_method($1, 'transform')
95 | self.send(method)
96 | else
97 | super
98 | end
99 | when /^(\w+)_mark$/
100 | # Initialize ::Plotrb::Mark objects
101 | if ::Plotrb::Mark::TYPES.include?($1.to_sym)
102 | cache_method($1, 'mark')
103 | self.send(method)
104 | else
105 | super
106 | end
107 | else
108 | super
109 | end
110 | end
111 |
112 | protected
113 |
114 | def cache_method(type, klass)
115 | self.class.class_eval {
116 | define_method("#{type}_#{klass}") do |&block|
117 | # class names are constants
118 | # create shortcut methods to initialize Plotrb objects
119 | ::Kernel::const_get("::Plotrb::#{klass.capitalize}").
120 | new(type.to_sym, &block)
121 | end
122 | }
123 | end
124 |
125 | end
126 |
127 | end
128 |
--------------------------------------------------------------------------------
/lib/plotrb/marks.rb:
--------------------------------------------------------------------------------
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.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
460 |
--------------------------------------------------------------------------------
/lib/plotrb/scales.rb:
--------------------------------------------------------------------------------
1 | module Plotrb
2 |
3 | # Scales are functions that transform a domain of data values to a range of
4 | # visual values.
5 | # See {https://github.com/trifacta/vega/wiki/Scales}
6 | class Scale
7 |
8 | include ::Plotrb::Base
9 |
10 | TYPES = %i(linear log pow sqrt quantile quantize threshold ordinal time utc)
11 |
12 | TYPES.each do |t|
13 | define_singleton_method(t) do |&block|
14 | ::Plotrb::Scale.new(t, &block)
15 | end
16 | end
17 |
18 | # @!attributes type
19 | # @return [Symbol] the type of the scale
20 | SCALE_PROPERTIES = [:name, :type, :domain, :domain_min, :domain_max, :range,
21 | :range_min, :range_max, :reverse, :round]
22 |
23 | add_attributes *SCALE_PROPERTIES
24 |
25 | RANGE_LITERALS = %i(width height shapes colors more_colors)
26 | TIME_SCALE_NICE = %i(second minute hour day week month year)
27 |
28 | def initialize(type=:linear, &block)
29 | @type = type
30 | case @type
31 | when :ordinal
32 | set_ordinal_scale_attributes
33 | when :time, :utc
34 | set_time_scale_attributes
35 | else
36 | set_quantitative_scale_attributes
37 | end
38 | set_common_scale_attributes
39 | ::Plotrb::Kernel.scales << self
40 | self.instance_eval(&block) if block_given?
41 | self
42 | end
43 |
44 | def type
45 | @type
46 | end
47 |
48 | def method_missing(method, *args, &block)
49 | case method.to_s
50 | when /in_(\w+)s$/ # set @nice for time and utc type, eg. in_seconds
51 | if TIME_SCALE_NICE.include?($1.to_sym)
52 | self.nice($1.to_sym, &block)
53 | else
54 | super
55 | end
56 | when /to_(\w+)$/ # set range literals, eg. to_more_colors
57 | if RANGE_LITERALS.include?($1.to_sym)
58 | self.range($1.to_sym, &block)
59 | else
60 | super
61 | end
62 | else
63 | super
64 | end
65 | end
66 |
67 | private
68 |
69 | def set_common_scale_attributes
70 | # @!attributes name
71 | # @return [String] the name of the scale
72 | # @!attributes domain
73 | # @return [Array(Numeric, Numeric), Array, String] the domain of the
74 | # scale, representing the set of data values
75 | # @!attributes domain_min
76 | # @return [Numeric, String] the minimum value in the scale domain
77 | # @!attributes domain_max
78 | # @return [Numeric, String] the maximum value in the scale domain
79 | # @!attributes range
80 | # @return [Array(Numeric, Numeric), Array, String] the range of the
81 | # scale, representing the set of visual values
82 | # @!attributes range_min
83 | # @return [Numeric, String] the minimum value in the scale range
84 | # @!attributes range_max
85 | # @return [Numeric, String] the maximum value in the scale range
86 | # @!attributes reverse
87 | # @return [Boolean] whether flips the scale range
88 | # @!attributes round
89 | # @return [Boolean] whether rounds numeric output values to integers
90 | define_single_val_attributes(:name, :domain, :domain_max, :domain_min,
91 | :range, :range_max, :range_min)
92 | define_boolean_attributes(:reverse, :round)
93 | self.singleton_class.class_eval {
94 | alias_method :from, :domain
95 | alias_method :to, :range
96 | }
97 | end
98 |
99 | def set_ordinal_scale_attributes
100 | # @!attributes points
101 | # @return [Boolean] whether distributes the ordinal values over a
102 | # quantitative range at uniformly spaced points or bands
103 | # @!attributes padding
104 | # @return [Numeric] the spacing among ordinal elements in the scale range
105 | # @!attributes sort
106 | # @return [Boolean] whether values in the scale domain will be sorted
107 | # according to their natural order
108 | add_attributes(:points, :padding, :sort)
109 | define_boolean_attributes(:points, :sort)
110 | define_single_val_attribute(:padding)
111 | define_singleton_method(:bands) do |&block|
112 | @points = false
113 | self.instance_eval(&block) if block
114 | self
115 | end
116 | define_singleton_method(:bands?) do
117 | !@points
118 | end
119 | self.singleton_class.class_eval {
120 | alias_method :as_bands, :bands
121 | alias_method :as_bands?, :bands?
122 | alias_method :as_points, :points
123 | alias_method :as_points?, :points?
124 | }
125 | end
126 |
127 | def set_time_scale_attributes
128 | # @!attributes clamp
129 | # @return [Boolean] whether clamps values that exceed the data domain
130 | # to either to minimum or maximum range value
131 | # @!attributes nice
132 | # @return [Symbol, Boolean, nil] scale domain in a more human-friendly
133 | # value range
134 | add_attributes(:clamp, :nice)
135 | define_boolean_attribute(:clamp)
136 | define_single_val_attribute(:nice)
137 | end
138 |
139 | def set_quantitative_scale_attributes
140 | # @!attributes clamp
141 | # @return [Boolean] whether clamps values that exceed the data domain
142 | # to either to minimum or maximum range value
143 | # @!attributes nice
144 | # @return [Boolean] scale domain in a more human-friendly
145 | # value range
146 | # @!attributes exponent
147 | # @return [Numeric] the exponent of the scale transformation
148 | # @!attributes zero
149 | # @return [Boolean] whether zero baseline value is included
150 | add_attributes(:clamp, :exponent, :nice, :zero)
151 | define_boolean_attributes(:clamp, :nice, :zero)
152 | define_single_val_attribute(:exponent)
153 | define_singleton_method(:exclude_zero) do |&block|
154 | @zero = false
155 | self.instance_eval(&block) if block
156 | self
157 | end
158 | define_singleton_method(:exclude_zero?) do
159 | !@zero
160 | end
161 | self.singleton_class.class_eval {
162 | alias_method :nicely, :nice
163 | alias_method :nicely?, :nice?
164 | alias_method :include_zero, :zero
165 | alias_method :include_zero?, :zero?
166 | alias_method :in_exponent, :exponent
167 | }
168 | end
169 |
170 | def attribute_post_processing
171 | process_name
172 | process_type
173 | process_domain
174 | process_domain_min
175 | process_domain_max
176 | process_range
177 | end
178 |
179 | def process_name
180 | if @name.nil? || @name.strip.empty?
181 | raise ArgumentError, 'Name missing for Scale object'
182 | end
183 | if ::Plotrb::Kernel.duplicate_scale?(@name)
184 | raise ArgumentError, 'Duplicate names for Scale object'
185 | end
186 | end
187 |
188 | def process_type
189 | unless TYPES.include?(@type)
190 | raise ArgumentError, 'Invalid Scale type'
191 | end
192 | end
193 |
194 | def process_domain
195 | return unless @domain
196 | case @domain
197 | when String
198 | @domain = get_data_ref_from_string(@domain)
199 | when ::Plotrb::Data
200 | @domain = get_data_ref_from_data(@domain)
201 | when Array
202 | if @domain.all? { |d| is_data_ref?(d) }
203 | fields = @domain.collect { |d| get_data_ref_from_string(d) }
204 | @domain = {:fields => fields}
205 | else
206 | # leave as it is
207 | end
208 | when ::Plotrb::Scale::DataRef
209 | # leave as it is
210 | else
211 | raise ArgumentError, 'Unsupported Scale domain type'
212 | end
213 | end
214 |
215 | def process_domain_min
216 | # only for quantitative domain
217 | return unless @domain_min && !%i(ordinal time utc).include?(@type)
218 | case @domain_min
219 | when String
220 | @domain_min = get_data_ref_from_string(@domain_min)
221 | when ::Plotrb::Data
222 | @domain_min = get_data_ref_from_data(@domain_min)
223 | when Array
224 | if @domain_min.all? { |d| is_data_ref?(d) }
225 | fields = @domain_min.collect { |d| get_data_ref_from_string(d) }
226 | @domain_min = {:fields => fields}
227 | else
228 | raise ArgumentError, 'Unsupported Scale domain_min type'
229 | end
230 | when Numeric
231 | # leave as it is
232 | else
233 | raise ArgumentError, 'Unsupported Scale domain_min type'
234 | end
235 | end
236 |
237 | def process_domain_max
238 | # only for quantitative domain
239 | return unless @domain_max && !%i(ordinal time utc).include?(@type)
240 | case @domain_max
241 | when String
242 | @domain_max = get_data_ref_from_string(@domain_max)
243 | when ::Plotrb::Data
244 | @domain_max = get_data_ref_from_data(@domain_max)
245 | when Array
246 | if @domain_max.all? { |d| is_data_ref?(d) }
247 | fields = @domain_max.collect { |d| get_data_ref_from_string(d) }
248 | @domain_max = {:fields => fields}
249 | else
250 | raise ArgumentError, 'Unsupported Scale domain_max type'
251 | end
252 | when Numeric
253 | # leave as it is
254 | else
255 | raise ArgumentError, 'Unsupported Scale domain_max type'
256 | end
257 | end
258 |
259 | def get_data_ref_from_string(ref)
260 | source, field = ref.split('.', 2)
261 | data = ::Plotrb::Kernel.find_data(source)
262 | if field.nil?
263 | if data && data.values.is_a?(Array)
264 | ::Plotrb::Scale::DataRef.new.data(source).field('data')
265 | else
266 | ::Plotrb::Scale::DataRef.new.data(source).field('index')
267 | end
268 | elsif field == 'index'
269 | ::Plotrb::Scale::DataRef.new.data(source).field('index')
270 | else
271 | if data.extra_fields.include?(field.to_sym)
272 | ::Plotrb::Scale::DataRef.new.data(source).field(field)
273 | else
274 | ::Plotrb::Scale::DataRef.new.data(source).field("data.#{field}")
275 | end
276 | end
277 | end
278 |
279 | def get_data_ref_from_data(data)
280 | if data.values.is_a?(Array)
281 | ::Plotrb::Scale::DataRef.new.data(data.name).field('data')
282 | else
283 | ::Plotrb::Scale::DataRef.new.data(data.name).field('index')
284 | end
285 | end
286 |
287 | def is_data_ref?(ref)
288 | return false unless ref.is_a?(String)
289 | source, _ = ref.split('.', 2)
290 | not ::Plotrb::Kernel.find_data(source).nil?
291 | end
292 |
293 | def process_range
294 | return unless @range
295 | case @range
296 | when String, Symbol
297 | @range = range_literal(@range)
298 | when Array
299 | #leave as it is
300 | else
301 | raise ArgumentError, 'Unsupported Scale range type'
302 | end
303 | end
304 |
305 | def range_literal(literal)
306 | case literal
307 | when :colors
308 | :category10
309 | when :more_colors
310 | :category20
311 | when :width, :height, :shapes, :category10, :category20
312 | literal
313 | else
314 | raise ArgumentError, 'Invalid Scale range'
315 | end
316 | end
317 |
318 | # A data reference specifies the field for a given scale property
319 | class DataRef
320 |
321 | include ::Plotrb::Base
322 |
323 | # @!attributes data
324 | # @return [String] the name of a data set
325 | # @!attributes field
326 | # @return [String] A field from which to pull a data values
327 | add_attributes :data, :field
328 |
329 | # TODO: Support group
330 | def initialize(&block)
331 | define_single_val_attributes(:data, :field)
332 | self.instance_eval(&block) if block
333 | self
334 | end
335 |
336 | private
337 |
338 | def attribute_post_processing
339 |
340 | end
341 |
342 | end
343 |
344 | end
345 |
346 | end
347 |
--------------------------------------------------------------------------------
/lib/plotrb/transforms.rb:
--------------------------------------------------------------------------------
1 | module Plotrb
2 |
3 | # Data transform performs operations on a data set prior to
4 | # visualization.
5 | # See {https://github.com/trifacta/vega/wiki/Data-Transforms}
6 | class Transform
7 |
8 | include ::Plotrb::Base
9 |
10 | # all available types of transforms defined by Vega
11 | TYPES = %i(array copy cross facet filter flatten fold formula slice sort
12 | stats truncate unique window zip force geo geopath link pie stack
13 | treemap wordcloud)
14 |
15 | TYPES.each do |t|
16 | define_singleton_method(t) do |&block|
17 | ::Plotrb::Transform.new(t, &block)
18 | end
19 | end
20 |
21 | # @!attributes type
22 | # @return [Symbol] the transform type
23 | add_attributes :type
24 |
25 | def initialize(type, &block)
26 | @type = type
27 | @extra_fields = [:index, :data]
28 | self.send(@type)
29 | self.instance_eval(&block) if block_given?
30 | ::Plotrb::Kernel.transforms << self
31 | self
32 | end
33 |
34 | def type
35 | @type
36 | end
37 |
38 | def extra_fields
39 | @extra_fields
40 | end
41 |
42 | private
43 |
44 | # Data Manipulation Transforms
45 |
46 | def array
47 | # @!attributes fields
48 | # @return [Array] array of field references to copy
49 | add_attributes(:fields)
50 | define_multi_val_attribute(:fields)
51 | self.singleton_class.class_eval { alias_method :take, :fields }
52 | end
53 |
54 | def copy
55 | # @!attributes from
56 | # @return [String] the name of the object to copy values from
57 | # @!attributes fields
58 | # @return [Array] the fields to copy
59 | # @!attributes as
60 | # @return [Array] the field names to copy the values to
61 | add_attributes(:from, :fields, :as)
62 | define_single_val_attribute(:from)
63 | define_multi_val_attributes(:fields, :as)
64 | self.singleton_class.class_eval { alias_method :take, :fields }
65 | end
66 |
67 | def cross
68 | # @!attributes with
69 | # @return [String] the name of the secondary data to cross with
70 | # @!attributes diagonal
71 | # @return [Boolean] whether diagonal of cross-product will be included
72 | add_attributes(:with, :diagonal)
73 | define_single_val_attribute(:with)
74 | define_boolean_attribute(:diagonal)
75 | self.singleton_class.class_eval {
76 | alias_method :include_diagonal, :diagonal
77 | alias_method :include_diagonal?, :diagonal?
78 | }
79 | end
80 |
81 | def facet
82 | # @!attributes keys
83 | # @return [Array] the fields to use as keys
84 | # @!attributes sort
85 | # @return [String, Array] sort criteria
86 | add_attributes(:keys, :sort)
87 | define_multi_val_attributes(:keys, :sort)
88 | self.singleton_class.class_eval { alias_method :group_by, :keys }
89 | @extra_fields.concat([:key])
90 | end
91 |
92 | def filter
93 | # @!attributes test
94 | # @return [String] the expression for the filter predicate, which
95 | # includes the variable `d`, corresponding to the current data object
96 | add_attributes(:test)
97 | define_single_val_attribute(:test)
98 | end
99 |
100 | def flatten
101 | # no parameter needed
102 | end
103 |
104 | def fold
105 | # @!attributes fields
106 | # @return [Array] the field references indicating the data
107 | # properties to fold
108 | add_attributes(:fields)
109 | define_multi_val_attribute(:fields)
110 | self.singleton_class.class_eval { alias_method :into, :fields }
111 | @extra_fields.concat([:key, :value])
112 | end
113 |
114 | def formula
115 | # @!attributes field
116 | # @return [String] the property name in which to store the value
117 | # @!attributes
118 | # @return expr [String] the expression for the formula
119 | add_attributes(:field, :expr)
120 | define_single_val_attributes(:field, :expr)
121 | self.singleton_class.class_eval {
122 | alias_method :into, :field
123 | alias_method :apply, :expr
124 | }
125 | end
126 |
127 | def slice
128 | # @!attributes by
129 | # @return [Integer, Array, Symbol] the sub-array to copy
130 | # @!attributes field
131 | # @return [String] the data field to copy the max, min or median value
132 | add_attributes(:by, :field)
133 | define_single_val_attributes(:by, :field)
134 | end
135 |
136 | # TODO: allow reverse sort
137 | def sort
138 | # @!attributes by
139 | # @return [String, Array] a list of fields to use as sort
140 | # criteria
141 | add_attributes(:by)
142 | define_multi_val_attribute(:by)
143 | end
144 |
145 | def stats
146 | # @!attributes value
147 | # @return [String] the field for which to computer the statistics
148 | # @!attributes median
149 | # @return [Boolean] whether median will be computed
150 | # @!attributes assign
151 | # @return [Boolean] whether add stat property to each data element
152 | add_attributes(:value, :median, :assign)
153 | define_single_val_attribute(:value)
154 | define_boolean_attributes(:median, :assign)
155 | self.singleton_class.class_eval {
156 | alias_method :from, :value
157 | alias_method :include_median, :median
158 | alias_method :include_median?, :median?
159 | alias_method :store_stats, :assign
160 | alias_method :store_stats?, :assign?
161 | }
162 | @extra_fields.concat([:count, :min, :max, :sum, :mean, :variance, :stdev,
163 | :median])
164 | end
165 |
166 | def truncate
167 | # @!attributes value
168 | # @return [String] the field containing values to truncate
169 | # @!attributes output
170 | # @return [String] the field to store the truncated values
171 | # @!attributes limit
172 | # @return [Integer] maximum length of truncated string
173 | # @!attributes position
174 | # @return [Symbol] the position from which to remove text
175 | # @!attributes ellipsis
176 | # @return [String] the ellipsis for truncated text
177 | # @!attributes wordbreak
178 | # @return [Boolean] whether to truncate along word boundaries
179 | add_attributes(:value, :output, :limit, :position, :ellipsis, :wordbreak)
180 | define_single_val_attributes(:value, :output, :limit, :position,
181 | :ellipsis)
182 | define_boolean_attribute(:wordbreak)
183 | self.singleton_class.class_eval {
184 | alias_method :from, :value
185 | alias_method :to, :output
186 | alias_method :max_length, :limit
187 | }
188 | define_singleton_method :method_missing do |method, *args, &block|
189 | if method.to_s =~ /^in_(front|back|middle)$/
190 | self.position($1.to_sym, &block)
191 | else
192 | super
193 | end
194 | end
195 | end
196 |
197 | def unique
198 | # @!attributes field
199 | # @return [String] the data field for which to compute unique value
200 | # @!attributes as
201 | # @return [String] the field name to store the unique values
202 | add_attributes(:field, :as)
203 | define_single_val_attributes(:field, :as)
204 | self.singleton_class.class_eval {
205 | alias_method :from, :field
206 | alias_method :to, :as
207 | }
208 | end
209 |
210 | def window
211 | # @!attributes size
212 | # @return [Integer] the size of the sliding window
213 | # @!attributes step
214 | # @return [Integer] the step size to advance the window per frame
215 | add_attributes(:size, :step)
216 | define_single_val_attributes(:size, :step)
217 | end
218 |
219 | def zip
220 | # @!attributes with
221 | # @return [String] the name of the secondary data set to zip with the
222 | # primary data set
223 | # @!attributes as
224 | # @return [String] the name of the field to store the secondary data set
225 | # values
226 | # @!attributes key
227 | # @return [String] the field in the primary data set to match against
228 | # the secondary data set
229 | # @!attributes with_key
230 | # @return [String] the field in the secondary data set to match
231 | # against the primary data set
232 | # @!attributes default
233 | # @return [] a default value to use if no matching key value is found
234 | add_attributes(:with, :as, :key, :with_key, :default)
235 | define_single_val_attributes(:with, :as, :default, :key, :with_key)
236 | self.singleton_class.class_eval {
237 | alias_method :match, :key
238 | alias_method :against, :with_key
239 | }
240 | end
241 |
242 | # Visual Encoding Transforms
243 |
244 | def force
245 | # @!attributes links
246 | # @return [String] the name of the link (edge) data set, must have
247 | # `source` and `target` attributes
248 | # @!attributes size
249 | # @return [Array(Integer, Integer)] the dimensions of the layout
250 | # @!attributes iterations
251 | # @return [Integer] the number of iterations to run
252 | # @!attributes charge
253 | # @return [Numeric, String] the strength of the charge each node exerts
254 | # @!attributes link_distance
255 | # @return [Integer, String] the length of edges
256 | # @!attributes link_strength
257 | # @return [Numeric, String] the tension of edges
258 | # @!attributes friction
259 | # @return [Numeric] the strength of the friction force used to
260 | # stabilize the layout
261 | # @!attributes theta
262 | # @return [Numeric] the theta parameter for the Barnes-Hut algorithm
263 | # used to compute charge forces between nodes
264 | # @!attributes gravity
265 | # @return [Numeric] the strength of the pseudo-gravity force that pulls
266 | # nodes towards the center of the layout area
267 | # @!attributes alpha
268 | # @return [Numeric] a "temperature" parameter that determines how much
269 | # node positions are adjusted at each step
270 | attr = [:links, :size, :iterations, :charge, :link_distance,
271 | :link_strength, :friction, :theta, :gravity, :alpha]
272 | add_attributes(*attr)
273 | define_single_val_attributes(*attr)
274 | end
275 |
276 | def geo
277 | # @!attributes projection
278 | # @return [String] the type of cartographic projection to use
279 | # @!attributes lon
280 | # @return [String] the input longitude values
281 | # @!attributes lat
282 | # @return [String] the input latitude values
283 | # @!attributes center
284 | # @return [Array(Integer, Integer)] the center of the projection
285 | # @!attributes translate
286 | # @return [Array(Integer, Integer)] the translation of the projection
287 | # @!attributes scale
288 | # @return [Numeric] the scale of the projection
289 | # @!attributes rotate
290 | # @return [Numeric] the rotation of the projection
291 | # @!attributes precision
292 | # @return [Numeric] the desired precision of the projection
293 | # @!attributes clip_angle
294 | # @return [Numeric] the clip angle of the projection
295 | attr = [:projection, :lon, :lat, :center, :translate, :scale,
296 | :rotate, :precision, :clip_angle]
297 | add_attributes(*attr)
298 | define_single_val_attributes(*attr)
299 | end
300 |
301 | def geopath
302 | # @!attributes value
303 | # @return [String] the data field containing the GeoJSON feature data
304 | # @!attributes (see #geo)
305 | attr = [:value, :projection, :center, :translate, :scale, :rotate,
306 | :precision, :clip_angle]
307 | add_attributes(*attr)
308 | define_single_val_attributes(*attr)
309 | @value ||= 'data'
310 | @extra_fields.concat([:path])
311 | end
312 |
313 | def link
314 | # @!attributes source
315 | # @return [String] the data field that references the source node for
316 | # this link
317 | # @!attributes target
318 | # @return [String] the data field that references the target node for
319 | # this link
320 | # @!attributes shape
321 | # @return [Symbol] the path shape to use
322 | # @!attributes tension
323 | # @return [Numeric] the tension in the range [0,1] for the "tightness"
324 | # of 'curve'-shaped links
325 | attr = [:source, :target, :shape, :tension]
326 | add_attributes(*attr)
327 | define_single_val_attributes(*attr)
328 | @extra_fields.concat([:path])
329 | end
330 |
331 | def pie
332 | # @!attributes sort
333 | # @return [Boolean] whether to sort the data prior to computing angles
334 | # @!attributes value
335 | # @return [String] the data values to encode as angular spans
336 | add_attributes(:sort, :value)
337 | define_boolean_attribute(:sort)
338 | define_single_val_attribute(:value)
339 | @extra_fields.concat([:start_angle, :end_angle])
340 | end
341 |
342 | def stack
343 | # @!attributes point
344 | # @return [String] the data field determining the points at which to
345 | # stack
346 | # @!attributes height
347 | # @return [String] the data field determining the height of a stack
348 | # @!attributes offset
349 | # @return [Symbol] the baseline offset style
350 | # @!attributes order
351 | # @return [Symbol] the sort order for stack layers
352 | attr = [:point, :height, :offset, :order]
353 | add_attributes(*attr)
354 | define_single_val_attributes(*attr)
355 | @extra_fields.concat([:y, :y2])
356 | end
357 |
358 | def treemap
359 | # @!attributes padding
360 | # @return [Integer, Array(Integer, Integer, Integer, Integer)] the
361 | # padding to provide around the internal nodes in the treemap
362 | # @!attributes ratio
363 | # @return [Numeric] the target aspect ratio for the layout to optimize
364 | # @!attributes round
365 | # @return [Boolean] whether cell dimensions will be rounded to integer
366 | # pixels
367 | # @!attributes size
368 | # @return [Array(Integer, Integer)] the dimensions of the layout
369 | # @!attributes sticky
370 | # @return [Boolean] whether repeated runs of the treemap will use cached
371 | # partition boundaries
372 | # @!attributes value
373 | # @return [String] the values to use to determine the area of each
374 | # leaf-level treemap cell
375 | add_attributes(:padding, :ratio, :round, :size, :sticky, :value)
376 | define_single_val_attributes(:padding, :ratio, :size, :value)
377 | define_boolean_attributes(:round, :sticky)
378 | @extra_fields.concat([:x, :y, :width, :height])
379 | end
380 |
381 | def wordcloud
382 | # @!attributes font
383 | # @return [String] the font face to use within the word cloud
384 | # @!attributes font_size
385 | # @return [String] the font size for a word
386 | # @!attributes font_style
387 | # @return [String] the font style to use
388 | # @!attributes font_weight
389 | # @return [String] the font weight to use
390 | # @!attributes padding
391 | # @return [Integer, Array(Integer, Integer, Integer, Integer)] the
392 | # padding to provide around text in the word cloud
393 | # @!attributes rotate
394 | # @return [String, Hash] the rotation angle for a word
395 | # @!attributes size
396 | # @return [Array(Integer, Integer)] the dimensions of the layout
397 | # @!attributes text
398 | # @return [String] the data field containing the text to visualize
399 | attr = [:font, :font_size, :font_style, :font_weight, :padding,
400 | :rotate, :size, :text]
401 | add_attributes(*attr)
402 | define_single_val_attribute(*attr)
403 | @extra_fields.concat([:x, :y, :font_size, :font, :angle])
404 | end
405 |
406 | def attribute_post_processing
407 | process_array_fields
408 | process_copy_as
409 | process_facet_keys
410 | process_filter_test
411 | process_fold_fields
412 | process_slice_field
413 | process_stats_value
414 | process_unique_field
415 | process_truncate_value
416 | process_zip_key
417 | process_zip_with_key
418 | process_zip_as
419 | process_geo_lon
420 | process_geo_lat
421 | process_link_source
422 | process_link_target
423 | process_pie_value
424 | process_stack_order
425 | process_stack_point
426 | process_stack_height
427 | process_treemap_value
428 | process_wordcloud_text
429 | process_wordcloud_font_size
430 | end
431 |
432 | def process_array_fields
433 | return unless @type == :array && @fields
434 | @fields.collect! { |f| get_full_field_ref(f) }
435 | end
436 |
437 | def process_copy_as
438 | return unless @type == :copy && @as && @fields
439 | if @as.is_a?(Array) && @as.size != @fields.size
440 | raise ArgumentError, 'Unmatched number of fields for copy transform'
441 | end
442 | end
443 |
444 | def process_cross_with
445 | return unless @type == :cross && @with
446 | case @with
447 | when String
448 | unless ::Plotrb::Kernel.find_data(@with)
449 | raise ArgumentError, 'Invalid data for cross transform'
450 | end
451 | when ::Plotrb::Data
452 | @with = @with.name
453 | else
454 | raise ArgumentError, 'Invalid data for cross transform'
455 | end
456 | end
457 |
458 | def process_facet_keys
459 | return unless @type == :facet && @keys
460 | @keys.collect! { |k| get_full_field_ref(k) }
461 | end
462 |
463 | def process_filter_test
464 | return unless @type == :filter && @test
465 | unless @test =~ /d\./
466 | raise ArgumentError, 'Invalid filter test string, prefix with \'d.\''
467 | end
468 | end
469 |
470 | def process_fold_fields
471 | return unless @type == :fold && @fields
472 | @fields.collect! { |f| get_full_field_ref(f) }
473 | end
474 |
475 | def process_slice_field
476 | return unless @type == :slice && @field
477 | @field = get_full_field_ref(@field)
478 | end
479 |
480 | def process_stats_value
481 | return unless @type == :stats && @value
482 | @value = get_full_field_ref(@value)
483 | end
484 |
485 | def process_unique_field
486 | return unless @type == :unique && @field
487 | @field = get_full_field_ref(@field)
488 | end
489 |
490 | def process_truncate_value
491 | return unless @type == :truncate && @value
492 | @value = get_full_field_ref(@value)
493 | end
494 |
495 | def process_zip_key
496 | return unless @type == :zip && @key
497 | @key = get_full_field_ref(@key)
498 | end
499 |
500 | def process_zip_with_key
501 | return unless @type == :zip && @with_key
502 | @with_key = get_full_field_ref(@with_key)
503 | end
504 |
505 | def process_zip_as
506 | return unless @type == :zip && @as
507 | @extra_fields.concat([@as.to_sym])
508 | end
509 |
510 | def process_geo_lon
511 | return unless @type == :geo && @lon
512 | @lon = get_full_field_ref(@lon)
513 | end
514 |
515 | def process_geo_lat
516 | return unless @type == :geo && @lat
517 | @lat = get_full_field_ref(@lat)
518 | end
519 |
520 | def process_link_source
521 | return unless @type == :link && @source
522 | @source = get_full_field_ref(@source)
523 | end
524 |
525 | def process_link_target
526 | return unless @type == :link && @target
527 | @target = get_full_field_ref(@target)
528 | end
529 |
530 | def process_pie_value
531 | return unless @type == :pie
532 | if @value
533 | @value = get_full_field_ref(@value)
534 | else
535 | @value = 'data'
536 | end
537 | end
538 |
539 | def process_stack_order
540 | return unless @order
541 | case @order
542 | when :default, 'default', :reverse, 'reverse'
543 | when :inside_out, 'inside-out', 'inside_out'
544 | @order = 'inside-out'
545 | else
546 | raise ArgumentError, 'Unsupported stack order'
547 | end
548 | end
549 |
550 | def process_stack_point
551 | return unless @type == :stack && @point
552 | @point = get_full_field_ref(@point)
553 | end
554 |
555 | def process_stack_height
556 | return unless @type == :stack && @height
557 | @height = get_full_field_ref(@height)
558 | end
559 |
560 | def process_treemap_value
561 | return unless @type == :treemap && @value
562 | @value = get_full_field_ref(@value)
563 | end
564 |
565 | def process_wordcloud_text
566 | return unless @type == :wordcloud && @text
567 | @text = get_full_field_ref(@text)
568 | end
569 |
570 | def process_wordcloud_font_size
571 | return unless @type == :wordcloud && @font_size
572 | @font_size = get_full_field_ref(@font_size)
573 | end
574 |
575 | def get_full_field_ref(field)
576 | case field
577 | when String
578 | if field.start_with?('data.') || extra_fields.include?(field.to_sym)
579 | field
580 | else
581 | "data.#{field}"
582 | end
583 | when ::Plotrb::Data
584 | 'data'
585 | else
586 | raise ArgumentError, 'Invalid data field'
587 | end
588 | end
589 |
590 | end
591 |
592 | end
593 |
--------------------------------------------------------------------------------
/lib/plotrb/version.rb:
--------------------------------------------------------------------------------
1 | module Plotrb
2 | VERSION = '0.0.1'
3 | end
4 |
--------------------------------------------------------------------------------
/lib/plotrb/visualization.rb:
--------------------------------------------------------------------------------
1 | module Plotrb
2 |
3 | # The container for all visual elements.
4 | # See {https://github.com/trifacta/vega/wiki/Visualization}
5 | class Visualization
6 |
7 | include ::Plotrb::Base
8 | include ::Plotrb::Kernel
9 |
10 | # @!attributes name
11 | # @return [String] the name of the visualization
12 | # @!attributes width
13 | # @return [Integer] the total width of the data rectangle
14 | # @!attributes height
15 | # @return [Integer] the total height of the data rectangle
16 | # @!attributes viewport
17 | # @return [Array(Integer, Integer)] the width and height of the viewport
18 | # @!attributes padding
19 | # @return [Integer, Hash] the internal padding from the visualization
20 | # @!attributes data
21 | # @return [Array] the data for visualization
22 | # @!attributes scales
23 | # @return [Array] the scales for visualization
24 | # @!attributes marks
25 | # @return [Array] the marks for visualization
26 | # @!attributes axes
27 | # @return [Array] the axes for visualization
28 | add_attributes :name, :width, :height, :viewport, :padding, :data, :scales,
29 | :marks, :axes
30 |
31 | def initialize(&block)
32 | define_single_val_attributes(:name, :width, :height, :viewport, :padding)
33 | define_multi_val_attributes(:data, :scales, :marks, :axes)
34 | self.instance_eval(&block) if block_given?
35 | end
36 |
37 | def generate_spec(format=nil)
38 | if format == :pretty
39 | JSON.pretty_generate(self.collect_attributes)
40 | else
41 | JSON.generate(self.collect_attributes)
42 | end
43 | end
44 |
45 | private
46 |
47 | def attribute_post_processing
48 |
49 | end
50 |
51 | end
52 |
53 | end
54 |
--------------------------------------------------------------------------------
/plotrb.gemspec:
--------------------------------------------------------------------------------
1 | lib = File.expand_path('../lib', __FILE__)
2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3 | require 'plotrb/version'
4 |
5 | Gem::Specification.new do |spec|
6 | spec.name = 'plotrb'
7 | spec.version = Plotrb::VERSION
8 | spec.authors = ['Zuhao Wan']
9 | spec.email = 'wanzuhao@gmail.com'
10 | spec.description = %q{Plotrb is a plotting tool in Ruby.}
11 | spec.summary = %q{Plotrb is a plotting tool in Ruby, built on Vega and D3, and is part of the SciRuby Project.}
12 | spec.homepage = 'https://github.com/sciruby/plotrb'
13 | spec.license = 'BSD 2-clause'
14 |
15 | spec.files = `git ls-files`.split($/)
16 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18 | spec.require_paths = %w(lib)
19 |
20 | spec.add_development_dependency 'bundler', '~> 1.3'
21 | spec.add_development_dependency 'rake'
22 | spec.add_development_dependency 'rspec'
23 | spec.add_development_dependency 'yard'
24 | spec.add_dependency 'yajl-ruby'
25 | spec.add_dependency 'activemodel'
26 | end
27 |
28 |
--------------------------------------------------------------------------------
/spec/plotrb/axes_spec.rb:
--------------------------------------------------------------------------------
1 | require_relative '../spec_helper'
2 |
3 | describe 'Axis' do
4 |
5 | subject { ::Plotrb::Axis.new(:x) }
6 |
7 | describe '#type' do
8 |
9 | it 'raises error if type is neither x or y' do
10 | subject.instance_variable_set(:@type, :foo)
11 | expect { subject.send(:process_type) }.to raise_error(ArgumentError)
12 | end
13 |
14 | end
15 |
16 | describe '#scale' do
17 |
18 | it 'sets the scale backing the axis by name' do
19 | subject.from('foo_scale')
20 | subject.scale.should == 'foo_scale'
21 | end
22 |
23 | it 'sets the scale backing the axis by the scale object' do
24 | scale = ::Plotrb::Scale.new.name('foo_scale')
25 | subject.from(scale)
26 | subject.send(:process_scale)
27 | subject.scale.should == 'foo_scale'
28 | end
29 |
30 | it 'raises error if scale is not found' do
31 | subject.from('foo_scale')
32 | ::Plotrb::Kernel.stub(:find_scale).and_return(nil)
33 | expect { subject.send(:process_scale) }.to raise_error(ArgumentError)
34 | end
35 |
36 | end
37 |
38 | describe '#orient' do
39 |
40 | it 'sets the orient of the axis' do
41 | subject.at_bottom
42 | subject.orient.should == :bottom
43 | end
44 |
45 | end
46 |
47 | describe '#title' do
48 |
49 | it 'sets title of the axis' do
50 | subject.title('foo')
51 | subject.title.should == 'foo'
52 | end
53 |
54 | end
55 |
56 | describe '#title_offset' do
57 |
58 | it 'sets offset of the title' do
59 | subject.offset_title_by(5)
60 | subject.title_offset.should == 5
61 | end
62 |
63 | end
64 |
65 | describe 'format' do
66 |
67 | it 'accepts valid format specifier' do
68 | subject.format('04d')
69 | expect { subject.send(:process_format) }.to_not raise_error(ArgumentError)
70 | end
71 |
72 | it 'raises error if format specifier is invalid' do
73 | subject.format('{$s04d,g')
74 | expect { subject.send(:process_format) }.to raise_error(ArgumentError)
75 | end
76 |
77 | end
78 |
79 | describe '#ticks' do
80 |
81 | it 'sets ticks of the axis' do
82 | subject.with_20_ticks
83 | subject.ticks.should == 20
84 | end
85 |
86 | end
87 |
88 | describe '#values' do
89 |
90 | it 'sets values if given as an array' do
91 | subject.values([1,2,3,4])
92 | subject.values.should match_array([1,2,3,4])
93 | end
94 |
95 | it 'sets values if given one by one as arguments' do
96 | subject.values(1,2,3,4)
97 | subject.values.should match_array([1,2,3,4])
98 | end
99 |
100 | end
101 |
102 | describe '#subdivide' do
103 |
104 | it 'sets subdivide of the ticks' do
105 | subject.subdivide_by(10)
106 | subject.subdivide.should == 10
107 | end
108 |
109 | end
110 |
111 | describe '#tick_padding' do
112 |
113 | it 'sets padding for the ticks' do
114 | subject.tick_padding(5)
115 | subject.tick_padding.should == 5
116 | end
117 |
118 | end
119 |
120 | describe '#tick_size' do
121 |
122 | it 'sets size for the ticks' do
123 | subject.tick_size(5)
124 | subject.tick_size.should == 5
125 | end
126 |
127 | end
128 |
129 | describe '#tick_size_major' do
130 |
131 | it 'sets major tick size' do
132 | subject.major_tick_size(10)
133 | subject.tick_size_major.should == 10
134 | end
135 |
136 | end
137 |
138 | describe '#tick_size_minor' do
139 |
140 | it 'sets minor tick size' do
141 | subject.minor_tick_size(10)
142 | subject.tick_size_minor.should == 10
143 | end
144 |
145 | end
146 |
147 | describe '#tick_size_end' do
148 |
149 | it 'sets end tick size' do
150 | subject.end_tick_size(10)
151 | subject.tick_size_end.should == 10
152 | end
153 |
154 | end
155 |
156 | describe '#offset' do
157 |
158 | it 'sets offset of the axis' do
159 | subject.offset_by(10)
160 | subject.offset.should == 10
161 | end
162 |
163 | end
164 |
165 | describe '#layer' do
166 |
167 | it 'sets the layer of the axis' do
168 | subject.in_front
169 | subject.layer.should == :front
170 | end
171 |
172 | end
173 |
174 | describe 'above' do
175 |
176 | it 'sets the layer to front' do
177 | subject.above
178 | subject.layer.should == :front
179 | subject.above?.should be_true
180 | end
181 |
182 | end
183 |
184 | describe 'below' do
185 |
186 | it 'sets the layer to back' do
187 | subject.below
188 | subject.layer.should == :back
189 | subject.below?.should be_true
190 | end
191 |
192 | end
193 |
194 | describe '#grid' do
195 |
196 | it 'sets if grid-lines should be shown' do
197 | subject.show_grid
198 | subject.grid?.should be_true
199 | end
200 |
201 | end
202 |
203 | describe '#method_missing' do
204 |
205 | it 'sets ticks if in_some_ticks is called' do
206 | subject.with_20_ticks
207 | subject.ticks.should == 20
208 | end
209 |
210 | it 'sets subdivide if subdivide_by_some is called' do
211 | subject.subdivide_by_10
212 | subject.subdivide.should == 10
213 | end
214 |
215 | it 'sets orient if at_orient_position is called' do
216 | subject.at_bottom
217 | subject.orient.should == :bottom
218 | end
219 |
220 | it 'sets layer if at_layer_position is called' do
221 | subject.in_front
222 | subject.layer.should == :front
223 | end
224 |
225 | end
226 |
227 | end
228 |
--------------------------------------------------------------------------------
/spec/plotrb/base_spec.rb:
--------------------------------------------------------------------------------
1 | require_relative '../spec_helper'
2 |
3 | describe 'Base' do
4 |
5 | class FooClass
6 | include ::Plotrb::Base
7 | end
8 |
9 | class BarClass
10 | end
11 |
12 | class BazClass
13 | end
14 |
15 | describe 'ClassMethods' do
16 |
17 | let(:foo) { Class.new { include ::Plotrb::Base } }
18 |
19 | describe '.attributes' do
20 |
21 | it 'has attributes' do
22 | foo.respond_to?(:attributes).should be_true
23 | end
24 |
25 | end
26 |
27 | describe '.add_attributes' do
28 |
29 | before(:each) do
30 | foo.class_eval { add_attributes :foo_foo }
31 | end
32 |
33 | it 'keeps track of all attributes defined for class' do
34 | foo.attributes.should match_array([:foo_foo])
35 | end
36 |
37 | it 'adds setter method for the attribute' do
38 | bar = foo.new
39 | bar.foo_foo = 1
40 | bar.instance_variable_get(:@foo_foo).should == 1
41 | end
42 |
43 | end
44 |
45 | end
46 |
47 | describe '#attributes' do
48 |
49 | let(:foo) { FooClass.new }
50 | before(:each) do
51 | foo.singleton_class.class_eval { add_attributes :foo_foo }
52 | end
53 |
54 | it 'tracks both class-defined and instance-defined attributes' do
55 | foo.add_attributes(:bar_bar)
56 | foo.attributes.should match_array([:foo_foo, :bar_bar])
57 | end
58 |
59 | end
60 |
61 | describe '#set_attribuets' do
62 |
63 | let(:foo) { FooClass.new }
64 |
65 | it 'creates attributes and sets values' do
66 | foo.set_attributes(a: 1, b: 2)
67 | foo.attributes.should match_array([:a, :b])
68 | foo.instance_variable_get(:@a).should == 1
69 | foo.instance_variable_get(:@b).should == 2
70 | end
71 |
72 | end
73 |
74 | describe '#add_attributes' do
75 |
76 | let(:foo) { FooClass.new }
77 | let(:bar) { FooClass.new }
78 | before(:each) do
79 | FooClass.add_attributes(:foo_class)
80 | end
81 |
82 | it 'adds attributes to specific instance only' do
83 | foo.add_attributes(:foo_foo)
84 | bar.add_attributes(:bar_bar)
85 | foo.attributes.should match_array([:foo_class, :foo_foo])
86 | bar.attributes.should match_array([:foo_class, :bar_bar])
87 | end
88 |
89 | end
90 |
91 | describe '#defined_attributes' do
92 |
93 | let(:foo) { FooClass.new }
94 |
95 | it 'only returns non-nil attributes' do
96 | foo.set_attributes(a: 1, b: 2, c: nil)
97 | foo.defined_attributes.should match_array([:a, :b])
98 | end
99 |
100 | end
101 |
102 | describe '#collect_attributes' do
103 |
104 | let(:foo) { FooClass.new }
105 | let(:bar) { BarClass.new }
106 | let(:baz) { BazClass.new }
107 | before(:each) do
108 | foo.add_attributes(:attr)
109 | foo.stub(:attribute_post_processing)
110 | end
111 |
112 | it 'recursively collects attributes' do
113 | BarClass.any_instance.stub(:respond_to?).with(:collect_attributes).
114 | and_return(true)
115 | BarClass.any_instance.stub(:collect_attributes).and_return('bar_values')
116 | foo.attr = bar
117 | foo.collect_attributes.should == { 'attr' => 'bar_values' }
118 | end
119 |
120 | it 'collects attributes of each of the array element' do
121 | BarClass.any_instance.stub(:respond_to?).with(:collect_attributes).
122 | and_return(true)
123 | BazClass.any_instance.stub(:respond_to?).with(:collect_attributes).
124 | and_return(true)
125 | BarClass.any_instance.stub(:collect_attributes).and_return('bar_values')
126 | BazClass.any_instance.stub(:collect_attributes).and_return('baz_values')
127 | foo.attr = [bar, baz]
128 | foo.collect_attributes.should == { 'attr' => %w(bar_values baz_values)}
129 | end
130 |
131 | end
132 |
133 | describe '#define_boolean_attribute' do
134 |
135 | let(:foo) { FooClass.new }
136 |
137 | before(:each) do
138 | foo.define_boolean_attribute(:bar)
139 | end
140 |
141 | it 'creates setter' do
142 | foo.respond_to?(:bar).should be_true
143 | end
144 |
145 | it 'sets attribute to true when called' do
146 | foo.bar
147 | foo.instance_variable_get('@bar').should == true
148 | end
149 |
150 | it 'creates getter' do
151 | foo.respond_to?(:bar?).should be_true
152 | end
153 |
154 | end
155 |
156 | describe '#define_single_val_attribute' do
157 |
158 | let(:foo) { FooClass.new }
159 |
160 | context 'when no proc is provided' do
161 |
162 | before(:each) do
163 | foo.define_single_val_attribute(:bar)
164 | end
165 |
166 | it 'creates setter and getter' do
167 | foo.respond_to?(:bar).should be_true
168 | end
169 |
170 | it 'acts as getter when no argument is provided' do
171 | foo.should_receive(:instance_variable_get).with('@bar')
172 | foo.bar
173 | end
174 |
175 | it 'sets value of the attribute if provided' do
176 | foo.should_receive(:instance_variable_set).with('@bar', 1)
177 | foo.bar(1)
178 | end
179 |
180 | it 'raises error if more than one value is given' do
181 | expect { foo.bar(1,2,3) }.to raise_error(ArgumentError)
182 | end
183 |
184 | end
185 |
186 | context 'when a proc is provided' do
187 |
188 | it 'process the value before assigning to the attribute' do
189 | p = lambda { |x| x + 1}
190 | foo.define_single_val_attribute(:bar, p)
191 | foo.bar(1)
192 | foo.bar.should == 2
193 | end
194 |
195 | it 'raises error if condition check fails in proc' do
196 | p = lambda { |x|
197 | if x > 0
198 | x + 1
199 | else
200 | raise ArgumentError
201 | end
202 | }
203 | foo.define_single_val_attribute(:bar, p)
204 | foo.bar(1)
205 | foo.bar.should == 2
206 | expect { foo.bar(0) }.to raise_error(ArgumentError)
207 | end
208 |
209 | end
210 |
211 | end
212 |
213 | describe '#define_multi_val_attribute' do
214 |
215 | let(:foo) { FooClass.new }
216 |
217 | context 'when no proc is provided' do
218 |
219 | before(:each) do
220 | foo.define_multi_val_attribute(:bar)
221 | end
222 |
223 | it 'creates setter and getter' do
224 | foo.respond_to?(:bar).should be_true
225 | end
226 |
227 | it 'acts as getter when no argument is provided' do
228 | foo.should_receive(:instance_variable_get).with('@bar')
229 | foo.bar
230 | end
231 |
232 | it 'sets single value of the attribute if provided' do
233 | foo.should_receive(:instance_variable_set).with('@bar', [1])
234 | foo.bar(1)
235 | end
236 |
237 | it 'sets array of values' do
238 | foo.should_receive(:instance_variable_set).with('@bar', [1, 2, 3])
239 | foo.bar([1, 2, 3])
240 | end
241 |
242 | it 'sets multiple values' do
243 | foo.should_receive(:instance_variable_set).with('@bar', [1, 2, 3])
244 | foo.bar(1,2,3)
245 | end
246 |
247 | end
248 |
249 | context 'when a proc is provided' do
250 |
251 | it 'process the values before assigning to the attribute' do
252 | p = lambda { |*x| x.each { |v| v + 1 } }
253 | foo.define_multi_val_attribute(:bar, p)
254 | foo.bar(1,2,3)
255 | foo.bar.should == [1, 2, 3]
256 | end
257 |
258 | it 'is able to check argument size' do
259 | p = lambda { |x, y, z| [x + 1, y + 1, z + 1] }
260 | foo.define_multi_val_attribute(:bar, p)
261 | foo.bar(1,2,3)
262 | foo.bar.should == [2, 3, 4]
263 | expect { foo.bar(1,2,3,4) }.to raise_error(ArgumentError)
264 | end
265 | end
266 |
267 | end
268 |
269 | describe '#classify' do
270 |
271 | let(:foo) { Class.new { extend ::Plotrb::Base } }
272 |
273 | it 'classifies string' do
274 | foo.classify('visualization').should == 'Visualization'
275 | end
276 |
277 | it 'changes snake_case to CamelCase' do
278 | foo.classify('foo_bar').should == 'FooBar'
279 | end
280 |
281 | it 'changes snake_case to camelCaseInJson' do
282 | foo.classify('foo_bar_baz', :json).should == 'fooBarBaz'
283 | end
284 |
285 | end
286 |
287 | describe 'Hash' do
288 |
289 | describe '#reverse_merge' do
290 |
291 | it 'should respond to reverse_merge' do
292 | Hash.new.respond_to?(:reverse_merge).should be_true
293 | end
294 |
295 | it 'reverse merges hash' do
296 | hash = {a: 1, b: 2}
297 | default = {a:2, c:3}
298 | hash.reverse_merge(default).should == {a: 1, b: 2, c: 3}
299 | end
300 |
301 | end
302 |
303 | describe '#collect_attributes' do
304 |
305 | it 'should respond to collect_attributes' do
306 | Hash.new.respond_to?(:collect_attributes).should be_true
307 | end
308 |
309 | it 'recursively collects attributes' do
310 | hash = {foo: FooClass.new, bar: BarClass.new}
311 | FooClass.any_instance.stub(:collect_attributes).and_return('foo_value')
312 | BarClass.any_instance.stub(:collect_attributes).and_return('bar_value')
313 | hash.collect_attributes.should == {'foo' => 'foo_value',
314 | 'bar' => 'bar_value'}
315 | end
316 |
317 | end
318 |
319 | end
320 |
321 | end
322 |
--------------------------------------------------------------------------------
/spec/plotrb/data_spec.rb:
--------------------------------------------------------------------------------
1 | require_relative '../spec_helper'
2 |
3 | describe 'Data' do
4 |
5 | subject { ::Plotrb::Data.new }
6 |
7 | describe '#name' do
8 |
9 | it 'sets name if valid' do
10 | subject.name('foo')
11 | subject.name.should == 'foo'
12 | end
13 |
14 | it 'raises error if name is nil' do
15 | expect { subject.send(:process_name) }.to raise_error(ArgumentError)
16 | end
17 |
18 | it 'raises error if name in blank' do
19 | subject.name = ' '
20 | expect { subject.send(:process_name) }.to raise_error(ArgumentError)
21 | end
22 |
23 | it 'raises error if name is not unique' do
24 | ::Plotrb::Kernel.stub(:duplicate_data?).and_return(false)
25 | expect { subject.send(:process_name) }.to raise_error(ArgumentError)
26 | end
27 |
28 | end
29 |
30 | describe '#format' do
31 |
32 | it 'sets format as a new Format instance' do
33 | ::Plotrb::Data::Format.should_receive(:new).with(:foo)
34 | subject.format(:foo)
35 | end
36 |
37 | end
38 |
39 | describe '#values' do
40 |
41 | it 'sets values if valid JSON is given' do
42 | subject.values('{"foo":1, "bar":{"baz":2}}')
43 | subject.values.should == '{"foo":1, "bar":{"baz":2}}'
44 | end
45 |
46 | it 'sets values if array is given' do
47 | subject.values([1,2,3,4])
48 | subject.values.should match_array([1,2,3,4])
49 | end
50 |
51 | it 'sets values if hash is given' do
52 | subject.values(foo: 1, bar: 2)
53 | subject.values.should == {foo: 1, bar: 2}
54 | end
55 |
56 | it 'raises error if values are invalid JSON' do
57 | subject.values("{foo:1, 'bar':}")
58 | expect { subject.send(:process_values) }.to raise_error ArgumentError
59 | end
60 |
61 | end
62 |
63 | describe '#source' do
64 |
65 | it 'sets source if valid' do
66 | subject.source('foo')
67 | subject.source.should == 'foo'
68 | end
69 |
70 | it 'raises error if source does not exist' do
71 | ::Plotrb::Kernel.stub(:find_data).and_return(false)
72 | subject.source('foo')
73 | expect { subject.send(:process_source) }.to raise_error ArgumentError
74 | end
75 |
76 | it 'gets the name of the existing data object' do
77 | foo = ::Plotrb::Data.new.name('foo')
78 | subject.source(foo)
79 | subject.send(:process_source)
80 | subject.source.should == 'foo'
81 | end
82 |
83 | end
84 |
85 | describe '#url' do
86 |
87 | it 'sets valid absolute url' do
88 | subject.url('http://foo.com/bar')
89 | subject.url.should == 'http://foo.com/bar'
90 | end
91 |
92 | it 'sets valid relative url' do
93 | subject.url('data/bar.json')
94 | subject.url.should == 'data/bar.json'
95 | end
96 |
97 | it 'raises error when url is invalid' do
98 | subject.url('http://foo/#-|r|$@')
99 | expect { subject.send(:process_url) }.to raise_error ArgumentError
100 | end
101 |
102 | end
103 |
104 | describe '#file' do
105 |
106 | it 'sets url if file exists'
107 |
108 | it 'raises error is file does not exist'
109 |
110 | end
111 |
112 | describe '#transform' do
113 |
114 | class Bar; end
115 |
116 | let(:foo) { ::Plotrb::Transform.new(:array) }
117 | let(:bar) { Bar.new }
118 |
119 | it 'sets transform if a transform object is given' do
120 | subject.transform(foo)
121 | subject.transform.should == [foo]
122 | end
123 |
124 | it 'sets transform if multiple transforms are given' do
125 | subject.transform(foo, foo)
126 | subject.transform.should == [foo, foo]
127 | end
128 |
129 | it 'raises error if array contains non-transforms' do
130 | subject.transform(foo, bar)
131 | expect { subject.send(:process_transform) }.to raise_error ArgumentError
132 | end
133 |
134 | end
135 |
136 | describe '#method_missing' do
137 |
138 | it 'sets format via as_foo' do
139 | subject.should_receive(:format).with(:csv)
140 | subject.as_csv
141 | end
142 |
143 | end
144 |
145 | describe 'Format' do
146 |
147 | it 'raises error if format type is not recognized' do
148 | expect { ::Plotrb::Data::Format.new(:foo) }.to raise_error ArgumentError
149 | end
150 |
151 | context 'json' do
152 |
153 | subject { ::Plotrb::Data::Format.new(:json) }
154 |
155 | it 'has parse and property attributes' do
156 | subject.attributes.should match_array([:type, :parse, :property])
157 | end
158 |
159 | describe '#parse' do
160 |
161 | it 'sets parse if valid hash is given' do
162 | subject.parse('foo' => :number, 'bar' => :date)
163 | subject.parse.should == {'foo' => :number, 'bar' => :date }
164 | end
165 |
166 | it 'raises error if parse object has unknown data type' do
167 | subject.parse('foo' => :bar)
168 | expect { subject.send(:process_parse) }.to raise_error ArgumentError
169 | end
170 |
171 | end
172 |
173 | describe '#as_date' do
174 |
175 | it 'parses the field as date' do
176 | subject.as_date('foo')
177 | subject.parse['foo'].should == :date
178 | end
179 |
180 | it 'allows setting multiple fields' do
181 | subject.as_date('foo', 'bar')
182 | subject.parse.should == {'foo' => :date, 'bar' => :date}
183 | end
184 |
185 | end
186 |
187 | describe '#as_boolean' do
188 |
189 | it 'parses the field as boolean' do
190 | subject.as_boolean('foo')
191 | subject.parse['foo'].should == :boolean
192 | end
193 |
194 | end
195 |
196 | describe '#as_number' do
197 |
198 | it 'parses the field as number' do
199 | subject.as_number('foo')
200 | subject.parse['foo'].should == :number
201 | end
202 |
203 | end
204 |
205 | describe '#property' do
206 |
207 | it 'sets the property' do
208 | subject.property('values.features')
209 | subject.property.should == 'values.features'
210 | end
211 |
212 | end
213 |
214 | end
215 |
216 | context 'csv' do
217 |
218 | subject { ::Plotrb::Data::Format.new(:csv) }
219 |
220 | it 'has parse attribute' do
221 | subject.attributes.should match_array([:type, :parse])
222 | end
223 |
224 | end
225 |
226 | context 'tsv' do
227 |
228 | subject { ::Plotrb::Data::Format.new(:tsv) }
229 |
230 | it 'has parse attribute' do
231 | subject.attributes.should match_array([:type, :parse])
232 | end
233 |
234 | end
235 |
236 | context 'topojson' do
237 |
238 | subject { ::Plotrb::Data::Format.new(:topojson) }
239 |
240 | it 'has feature and mesh attribute' do
241 | subject.attributes.should match_array([:type, :feature, :mesh])
242 | end
243 |
244 | end
245 |
246 | context 'treejson' do
247 |
248 | subject { ::Plotrb::Data::Format.new(:treejson) }
249 |
250 | it 'has parse and children attribute' do
251 | subject.attributes.should match_array([:type, :parse, :children])
252 | end
253 |
254 | end
255 |
256 | end
257 |
258 | end
259 |
--------------------------------------------------------------------------------
/spec/plotrb/kernel_spec.rb:
--------------------------------------------------------------------------------
1 | require_relative '../spec_helper'
2 |
3 | describe 'Kernel' do
4 |
5 | class Object
6 | include ::Plotrb::Kernel
7 | end
8 |
9 | describe '#visualization' do
10 |
11 | it 'creates new visualization object' do
12 | v = visualization
13 | v.is_a?(::Plotrb::Visualization).should be_true
14 | end
15 |
16 | end
17 |
18 | describe '#pdata' do
19 |
20 | it 'creates new data object' do
21 | d = pdata
22 | d.is_a?(::Plotrb::Data).should be_true
23 | end
24 |
25 | end
26 |
27 | describe '#method_missing' do
28 |
29 | it 'creates axis object' do
30 | a = x_axis
31 | a.is_a?(::Plotrb::Axis).should be_true
32 | a.type.should == :x
33 | end
34 |
35 | it 'creates scale object' do
36 | s = linear_scale
37 | s.is_a?(::Plotrb::Scale).should be_true
38 | s.type.should == :linear
39 | end
40 |
41 | it 'creates mark object' do
42 | m = rect_mark
43 | m.is_a?(::Plotrb::Mark).should be_true
44 | m.type.should == :rect
45 | end
46 |
47 | it 'creates transform object' do
48 | t = filter_transform
49 | t.is_a?(::Plotrb::Transform).should be_true
50 | t.type.should == :filter
51 | end
52 | end
53 |
54 | end
55 |
--------------------------------------------------------------------------------
/spec/plotrb/marks_spec.rb:
--------------------------------------------------------------------------------
1 | require_relative '../spec_helper'
2 |
3 | describe 'Mark' do
4 |
5 | subject { ::Plotrb::Mark.rect }
6 |
7 | describe '#initialize' do
8 |
9 | context 'when type is group' do
10 |
11 | subject { ::Plotrb::Mark.group }
12 |
13 | it 'has additional scales, axes, and marks attribute' do
14 | subject.attributes.should include(:scales, :axes, :marks)
15 | end
16 |
17 | end
18 |
19 | end
20 |
21 | describe 'properties' do
22 |
23 | it 'allows multiple properties' do
24 | ::Plotrb::Kernel.stub(:find_data).with('some_data').and_return(true)
25 | subject.from('some_data')
26 | subject.enter
27 | subject.exit
28 | subject.properties.keys.should match_array([:enter, :exit])
29 | end
30 |
31 | end
32 |
33 | describe '#from' do
34 |
35 | it 'recognizes Data and Transform objects' do
36 | foo = ::Plotrb::Transform.facet
37 | bar = ::Plotrb::Transform.filter
38 | ::Plotrb::Kernel.stub(:find_data).with('some_data').and_return(true)
39 | subject.from('some_data', foo, bar)
40 | subject.send(:process_from)
41 | subject.from.should == {:data => 'some_data', :transform => [foo, bar]}
42 | end
43 |
44 | end
45 |
46 | end
47 |
--------------------------------------------------------------------------------
/spec/plotrb/scales_spec.rb:
--------------------------------------------------------------------------------
1 | require_relative '../spec_helper'
2 |
3 | describe 'Scale' do
4 |
5 | subject { ::Plotrb::Scale.new }
6 |
7 | describe '#name' do
8 |
9 | it 'sets name of the scale' do
10 | subject.name 'foo_scale'
11 | subject.name.should == 'foo_scale'
12 | end
13 |
14 | it 'raises error if the name is not unique' do
15 | subject.name 'foo'
16 | ::Plotrb::Kernel.stub(:duplicate_scale?).and_return(true)
17 | expect { subject.send(:process_name) }.to raise_error(ArgumentError)
18 | end
19 |
20 | end
21 |
22 | describe '#type' do
23 |
24 | it 'does not allow changing type once initialized' do
25 | expect { subject.type(:other_type) }.to raise_error(ArgumentError)
26 | end
27 |
28 | end
29 |
30 | describe '#domain' do
31 |
32 | context 'when domain is a string reference to a data source' do
33 |
34 | before(:each) do
35 | ::Plotrb::Kernel.stub(:find_data).and_return(::Plotrb::Data.new)
36 | end
37 |
38 | it 'separates data source and data field' do
39 | subject.from('some_data.some_field')
40 | ::Plotrb::Data.any_instance.stub(:extra_fields).and_return([])
41 | subject.send(:process_domain)
42 | subject.domain.data.should == 'some_data'
43 | subject.domain.field.should == 'data.some_field'
44 | end
45 |
46 | it 'defaults field to index if not provided' do
47 | subject.from('some_data')
48 | ::Plotrb::Data.any_instance.stub(:extra_fields).and_return([])
49 | subject.send(:process_domain)
50 | subject.domain.data.should == 'some_data'
51 | subject.domain.field.should == 'index'
52 | end
53 |
54 | it 'deals with index field properly' do
55 | subject.from('some_data.index')
56 | ::Plotrb::Data.any_instance.stub(:extra_fields).and_return([])
57 | subject.send(:process_domain)
58 | subject.domain.data.should == 'some_data'
59 | subject.domain.field.should == 'index'
60 | end
61 |
62 | it 'recognizes extra fields added by transform' do
63 | subject.from('some_data.field')
64 | ::Plotrb::Data.any_instance.stub(:extra_fields).and_return([:field])
65 | subject.send(:process_domain)
66 | subject.domain.data.should == 'some_data'
67 | subject.domain.field.should == 'field'
68 | end
69 | end
70 |
71 | context 'when domain is actual ordinal/categorical data' do
72 |
73 | it 'sets domain directly' do
74 | data_set = %w(foo bar baz qux)
75 | subject.from(data_set)
76 | subject.domain.should == data_set
77 | end
78 |
79 | end
80 |
81 | context 'when domain is a quantitative range' do
82 |
83 | it 'sets domain as a two-element array' do
84 | subject.from([1,100])
85 | subject.domain.should == [1,100]
86 | end
87 |
88 | end
89 |
90 | end
91 |
92 | describe '#range' do
93 |
94 | context 'when range is numeric' do
95 |
96 | it 'sets range as a two-element array' do
97 | subject.to([1,100])
98 | subject.send(:process_range)
99 | subject.range.should == [1,100]
100 | end
101 |
102 | end
103 |
104 | context 'when range is ordinal' do
105 |
106 | it 'sets range directly' do
107 | range_set = %w(foo bar baz qux)
108 | subject.to(range_set)
109 | subject.send(:process_range)
110 | subject.range.should == range_set
111 | end
112 |
113 | end
114 |
115 | context 'when range is special literal' do
116 |
117 | it 'sets correct range literal' do
118 | subject.to_more_colors
119 | subject.send(:process_range)
120 | subject.range.should == :category20
121 | end
122 |
123 | it 'does not set invalid range literal' do
124 | expect { subject.to_foo_bar_range }.to raise_error(NoMethodError)
125 | subject.range.should be_nil
126 | end
127 |
128 | end
129 |
130 | end
131 |
132 | describe '#exponent' do
133 |
134 | it 'sets the exponent of scale transformation' do
135 | subject.in_exponent(10)
136 | subject.exponent.should == 10
137 | end
138 |
139 | end
140 |
141 | describe '#nice' do
142 |
143 | context 'when scale is time or utc' do
144 |
145 | subject { ::Plotrb::Scale.time }
146 |
147 | it 'sets valid nice literal' do
148 | subject.in_seconds
149 | subject.nice.should == :second
150 | end
151 |
152 | it 'does not set invalid nice literal' do
153 | expect { subject.in_millennium }.to raise_error(NoMethodError)
154 | subject.nice.should be_nil
155 | end
156 |
157 | end
158 |
159 | context 'when scale is quantitative' do
160 |
161 | subject { ::Plotrb::Scale.linear }
162 |
163 | it 'sets nice to true' do
164 | subject.nicely
165 | subject.nice?.should be_true
166 | end
167 |
168 | end
169 |
170 | end
171 |
172 | describe '#method_missing' do
173 |
174 | it 'calls nice if in_foo is called' do
175 | subject.type = :time
176 | subject.should_receive(:nice).with(:second)
177 | subject.in_seconds
178 | end
179 |
180 | it 'calls range if to_foo is called' do
181 | subject.should_receive(:range).with(:colors)
182 | subject.to_colors
183 | end
184 |
185 | end
186 |
187 | end
188 |
--------------------------------------------------------------------------------
/spec/plotrb/transforms_spec.rb:
--------------------------------------------------------------------------------
1 | require_relative '../spec_helper'
2 |
3 | describe 'Transform' do
4 |
5 | describe '#initialize' do
6 |
7 | subject { Plotrb::Transform }
8 |
9 | it 'calls respective type method to initialize attributes' do
10 | subject.any_instance.should_receive(:send).with(:foo)
11 | subject.new(:foo)
12 | end
13 |
14 | it 'raises error if type is not recognized' do
15 | expect { subject.new(:foo) }.to raise_error(NoMethodError)
16 | end
17 |
18 | end
19 |
20 | describe '#array' do
21 |
22 | subject { Plotrb::Transform.new(:array) }
23 |
24 | it 'responds to #take' do
25 | subject.take('foo', 'bar')
26 | subject.send(:process_array_fields)
27 | subject.fields.should match_array(['data.foo', 'data.bar'])
28 | end
29 |
30 | end
31 |
32 | describe '#copy' do
33 |
34 | subject { Plotrb::Transform.new(:copy) }
35 |
36 | it 'responds to #take' do
37 | subject.take('foo_field', 'bar_field').from('some_data').as('foo', 'bar')
38 | subject.fields.should match_array(['foo_field', 'bar_field'])
39 | end
40 |
41 | it 'raises error if as and fields are of different size' do
42 | subject.take('foo', 'bar').from('data').as('baz')
43 | expect { subject.send(:process_copy_as) }.to raise_error(ArgumentError)
44 | end
45 |
46 | end
47 |
48 | describe '#cross' do
49 |
50 | subject { Plotrb::Transform.new(:cross) }
51 |
52 | it 'raises error if the secondary data does not exist' do
53 | subject.with('foo')
54 | ::Plotrb::Kernel.stub(:find_data).and_return(nil)
55 | expect { subject.send(:process_cross_with) }.to raise_error(ArgumentError)
56 | end
57 |
58 | end
59 |
60 | describe '#facet' do
61 |
62 | subject { Plotrb::Transform.new(:facet) }
63 |
64 | it 'responds to #group_by' do
65 | subject.group_by('foo', 'bar')
66 | subject.send(:process_facet_keys)
67 | subject.keys.should match_array(['data.foo', 'data.bar'])
68 | end
69 |
70 | end
71 |
72 | describe '#filter' do
73 |
74 | subject { Plotrb::Transform.new(:filter) }
75 |
76 | it 'adds variable d if not present in the test expression' do
77 | subject.test('x>10')
78 | expect { subject.send(:process_filter_test) }.to raise_error(ArgumentError)
79 | end
80 |
81 | end
82 |
83 | describe '#flatten' do
84 |
85 | subject { Plotrb::Transform.new(:flatten) }
86 |
87 | end
88 |
89 | describe '#fold' do
90 |
91 | subject { Plotrb::Transform.new(:fold) }
92 |
93 | it 'responds to #into' do
94 | subject.into('foo', 'bar')
95 | subject.send(:process_fold_fields)
96 | subject.fields.should match_array(['data.foo', 'data.bar'])
97 | end
98 |
99 | end
100 |
101 | describe '#formula' do
102 |
103 | subject { Plotrb::Transform.new(:formula) }
104 |
105 | it 'responds to #apply and #into' do
106 | subject.apply('some_expression').into('some_field')
107 | subject.field.should == 'some_field'
108 | subject.expr.should == 'some_expression'
109 | end
110 |
111 | end
112 |
113 | describe '#slice' do
114 |
115 | subject { Plotrb::Transform.new(:slice) }
116 |
117 | it 'slices by a single value'
118 |
119 | it 'slices by a range'
120 |
121 | it 'slices by special positions'
122 |
123 | it 'raises error otherwise'
124 |
125 | end
126 |
127 | describe '#sort' do
128 |
129 | subject { Plotrb::Transform.new(:sort) }
130 |
131 | it 'adds - in front for reverse sort'
132 |
133 | end
134 |
135 | describe '#stats' do
136 |
137 | subject { Plotrb::Transform.new(:stats) }
138 |
139 | it 'responds to #from, #include_median, and #store_stats' do
140 | subject.from('foo').include_median.store_stats
141 | subject.value.should == 'foo'
142 | subject.median.should be_true
143 | subject.assign.should be_true
144 | end
145 |
146 | end
147 |
148 | describe '#truncate' do
149 |
150 | subject { Plotrb::Transform.new(:truncate) }
151 |
152 | it 'responds to #from, #to, and #max_length' do
153 | subject.from('foo').to('bar').max_length(5)
154 | subject.send(:process_truncate_value)
155 | subject.value.should == 'data.foo'
156 | subject.output.should == 'bar'
157 | subject.limit.should == 5
158 | end
159 |
160 | it 'responds to #in_position' do
161 | subject.in_front
162 | subject.position.should == :front
163 | end
164 |
165 | end
166 |
167 | describe '#unique' do
168 |
169 | subject { Plotrb::Transform.new(:unique) }
170 |
171 | it 'responds to #from and #to' do
172 | subject.from('foo').to('bar')
173 | subject.send(:process_unique_field)
174 | subject.field.should == 'data.foo'
175 | subject.as.should == 'bar'
176 | end
177 |
178 | end
179 |
180 | describe '#window' do
181 |
182 | subject { Plotrb::Transform.new(:window) }
183 |
184 | end
185 |
186 | describe '#zip' do
187 |
188 | subject { Plotrb::Transform.new(:zip) }
189 |
190 | it 'responds to #match and #against' do
191 | subject.with('foo').as('bar').match('foo_field').against('bar_field')
192 | subject.send(:process_zip_key)
193 | subject.send(:process_zip_with_key)
194 | subject.key.should == 'data.foo_field'
195 | subject.with_key.should == 'data.bar_field'
196 | end
197 |
198 | end
199 |
200 | describe '#force' do
201 |
202 | subject { Plotrb::Transform.new(:force) }
203 |
204 | end
205 |
206 | describe '#geo' do
207 |
208 | subject { Plotrb::Transform.new(:geo) }
209 |
210 | end
211 |
212 | describe '#geopath' do
213 |
214 | subject { Plotrb::Transform.new(:geopath) }
215 |
216 | end
217 |
218 | describe '#link' do
219 |
220 | subject { Plotrb::Transform.new(:link) }
221 |
222 | end
223 |
224 | describe '#pie' do
225 |
226 | subject { Plotrb::Transform.new(:pie) }
227 |
228 | end
229 |
230 | describe '#stack' do
231 |
232 | subject { Plotrb::Transform.new(:stack) }
233 |
234 | end
235 |
236 | describe '#treemap' do
237 |
238 | subject { Plotrb::Transform.new(:treemap) }
239 |
240 | end
241 |
242 | describe '#wordcloud' do
243 |
244 | subject { Plotrb::Transform.new(:wordcloud) }
245 |
246 | end
247 |
248 | end
249 |
--------------------------------------------------------------------------------
/spec/plotrb/visualization_spec.rb:
--------------------------------------------------------------------------------
1 | require_relative '../spec_helper'
2 |
3 | describe 'Visualization', :broken => true do
4 |
5 | context 'properties' do
6 |
7 | before(:each) do
8 | @vis = ::Plotrb::Visualization.new
9 | end
10 |
11 | it 'sets name' do
12 | @vis.name = 'Foo'
13 | @vis.name.should == 'Foo'
14 | end
15 |
16 | it 'raises error when name is nil' do
17 | expect { @vis.name = nil }.to raise_error ::Plotrb::InvalidInputError
18 | end
19 |
20 | it 'sets width when given' do
21 | @vis.width = 200
22 | @vis.width.should == 200
23 | end
24 |
25 | it 'sets width to 500 by default' do
26 | @vis.width.should == 500
27 | end
28 |
29 | it 'raises error when width is not a number' do
30 | expect { @vis.width = [:foo] }.to raise_error ::Plotrb::InvalidInputError
31 | end
32 |
33 | it 'sets height when given' do
34 | @vis.height = 200
35 | @vis.height.should == 200
36 | end
37 |
38 | it 'sets height to 500 by default' do
39 | @vis.height.should == 500
40 | end
41 |
42 | it 'raises error when height is not a number' do
43 | expect { @vis.height = [:foo] }.to raise_error ::Plotrb::InvalidInputError
44 | end
45 |
46 | it 'sets viewport when given as an array' do
47 | @vis.viewport = [400, 500]
48 | @vis.viewport.should == [400, 500]
49 | end
50 |
51 | it 'sets viewport when given as a hash' do
52 | @vis.viewport = {:width => 400, :height => 500}
53 | @vis.viewport.should == [400, 500]
54 | end
55 |
56 | it 'sets viewport to default width and height when not given' do
57 | vis = ::Plotrb::Visualization.new(:width => 300, :height => 400)
58 | vis.viewport.should == [300, 400]
59 | end
60 |
61 | it 'raises error when viewport contains nil' do
62 | expect { @vis.viewport = [100, :foo] }.
63 | to raise_error ::Plotrb::InvalidInputError
64 | end
65 |
66 | it 'sets padding when given as an integer' do
67 | @vis.padding = 2
68 | @vis.padding.should == {:top => 2, :left => 2, :right => 2, :bottom => 2}
69 | end
70 |
71 | it 'sets padding when given as a hash' do
72 | @vis.padding = {:bottom => 5, :top => 4, :right => 3, :left => 2 }
73 | @vis.padding.should == {:top => 4, :left => 2, :right => 3, :bottom => 5}
74 | end
75 |
76 | it 'sets padding to 5 by default' do
77 | @vis.padding = {:top => 5, :left => 5, :right => 5, :bottom => 5}
78 | end
79 |
80 | it 'raises error when any padding value is missing' do
81 | expect { @vis.padding = {:top => 4} }.
82 | to raise_error ::Plotrb::InvalidInputError
83 | end
84 |
85 | it 'raises error when any padding value is not a number' do
86 | expect { @vis.padding = {
87 | :top => 4, :left => :foo, :right => 'bar', :bottom => nil
88 | } }.to raise_error ::Plotrb::InvalidInputError
89 | end
90 |
91 | end
92 |
93 | end
94 |
--------------------------------------------------------------------------------
/spec/plotrb_spec.rb:
--------------------------------------------------------------------------------
1 | require_relative 'spec_helper'
2 |
3 | describe 'Plotrb' do
4 |
5 | end
6 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2 | $LOAD_PATH.unshift(File.dirname(__FILE__))
3 | require 'rspec'
4 | require 'plotrb'
5 |
6 | # Requires supporting files with custom matchers and macros, etc,
7 | # in ./support/ and its subdirectories.
8 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
9 |
10 | RSpec.configure do |config|
11 | config.filter_run_excluding :broken => true
12 | end
13 |
--------------------------------------------------------------------------------