├── .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
└── scatter.rb
├── lib
├── plotrb.rb
└── plotrb
│ ├── axes.rb
│ ├── base.rb
│ ├── data.rb
│ ├── kernel.rb
│ ├── legends.rb
│ ├── marks.rb
│ ├── scales.rb
│ ├── simple.rb
│ ├── transforms.rb
│ ├── version.rb
│ └── visualization.rb
├── plotrb.gemspec
└── spec
├── plotrb
├── axes_spec.rb
├── base_spec.rb
├── data_spec.rb
├── kernel_spec.rb
├── legends_spec.rb
├── marks_spec.rb
├── scales_spec.rb
├── simple_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 |
--------------------------------------------------------------------------------
/examples/scatter.rb:
--------------------------------------------------------------------------------
1 | require 'plotrb'
2 |
3 | raw_data = pdata.name('iris').url('iris_data.json')
4 | xs = linear_scale.name('x').from('iris.sepalWidth').to_width.nicely
5 | ys = linear_scale.name('y').from('iris.petalLength').to_height.nicely
6 | cs = ordinal_scale.name('c').from('iris.species').range(["#800", "#080", "#008"])
7 |
8 | xaxis = x_axis.scale(xs).offset(5).ticks(5).title('Sepal Width')
9 | yaxis = y_axis.scale(ys).offset(5).ticks(5).title('Petal Length')
10 |
11 | lgnd = legend.fill(cs).title('Species') do
12 | properties(:symbols) do
13 | fill_opacity 0.5
14 | stroke :transparent
15 | end
16 | end
17 |
18 | mark = symbol_mark.from(raw_data) do
19 | enter do
20 | x { scale(xs).field('sepalWidth') }
21 | y { scale(ys).field('petalLength') }
22 | fill { scale(cs).field('species') }
23 | fill_opacity 0.5
24 | end
25 | update do
26 | size 100
27 | stroke 'transparent'
28 | end
29 | hover do
30 | size 300
31 | stroke 'white'
32 | end
33 | end
34 |
35 | vis = visualization.name('arc').width(200).height(200) do
36 | data raw_data
37 | scales xs, ys, cs
38 | axes xaxis, yaxis
39 | legends lgnd
40 | marks mark
41 | end
42 |
43 | puts vis.generate_spec(:pretty)
44 |
--------------------------------------------------------------------------------
/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 | require_relative 'plotrb/legends'
15 | require_relative 'plotrb/simple'
16 |
17 | module Plotrb
18 |
19 | end
20 |
21 | class Object
22 |
23 | include ::Plotrb::Kernel
24 |
25 | end
26 |
27 | if defined? IRuby
28 | IRuby.display(IRuby.javascript(%{
29 | require.config({paths: {
30 | d3: 'http://d3js.org/d3.v3.min',
31 | d3_geo: 'http://trifacta.github.io/vega/lib/d3.geo.projection.min',
32 | topojson: 'http://trifacta.github.io/vega/lib/topojson',
33 | vg: 'http://trifacta.github.io/vega/vega.min'
34 | }});
35 | require(['d3', 'vg', 'd3_geo', 'topojson'], function(d3, vg, d3_geo, topojson) {
36 | window.d3 = d3;
37 | window.vg = vg;
38 | var event = document.createEvent('HTMLEvents');
39 | event.initEvent('load_plotrb',false,false);
40 | window.dispatchEvent(event);
41 | });
42 | }))
43 | end
44 |
--------------------------------------------------------------------------------
/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, "wrong number of arguments (#{args.size} for 0..1)"
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 | # a global space keeping track of all Transform objects defined
62 | def self.legends
63 | @legends ||= []
64 | end
65 |
66 | # Initialize ::Plotrb::Visualization object
67 |
68 | def visualization(&block)
69 | ::Plotrb::Visualization.new(&block)
70 | end
71 |
72 | # Initialize ::Plotrb::Data objects
73 |
74 | def pdata(&block)
75 | ::Plotrb::Data.new(&block)
76 | end
77 |
78 | def legend(&block)
79 | ::Plotrb::Legend.new(&block)
80 | end
81 |
82 | def method_missing(method, *args, &block)
83 | case method.to_s
84 | when /^(\w)_axis$/
85 | # Initialize ::Plotrb::Axis objects
86 | if ::Plotrb::Axis::TYPES.include?($1.to_sym)
87 | cache_method($1, 'axis')
88 | self.send(method)
89 | else
90 | super
91 | end
92 | when /^(\w+)_scale$/
93 | # Initialize ::Plotrb::Scale objects
94 | if ::Plotrb::Scale::TYPES.include?($1.to_sym)
95 | cache_method($1, 'scale')
96 | self.send(method)
97 | else
98 | super
99 | end
100 | when /^(\w+)_transform$/
101 | # Initialize ::Plotrb::Transform objects
102 | if ::Plotrb::Transform::TYPES.include?($1.to_sym)
103 | cache_method($1, 'transform')
104 | self.send(method)
105 | else
106 | super
107 | end
108 | when /^(\w+)_mark$/
109 | # Initialize ::Plotrb::Mark objects
110 | if ::Plotrb::Mark::TYPES.include?($1.to_sym)
111 | cache_method($1, 'mark')
112 | self.send(method)
113 | else
114 | super
115 | end
116 | else
117 | super
118 | end
119 | end
120 |
121 | protected
122 |
123 | def cache_method(type, klass)
124 | self.class.class_eval {
125 | define_method("#{type}_#{klass}") do |&block|
126 | # class names are constants
127 | # create shortcut methods to initialize Plotrb objects
128 | ::Kernel::const_get("::Plotrb::#{klass.capitalize}").
129 | new(type.to_sym, &block)
130 | end
131 | }
132 | end
133 |
134 | end
135 |
136 | end
137 |
--------------------------------------------------------------------------------
/lib/plotrb/legends.rb:
--------------------------------------------------------------------------------
1 | module Plotrb
2 |
3 | # Legends visualize scales. Legends aid interpretation of scales with ranges
4 | # such as colors, shapes and sizes.
5 | # See {https://github.com/trifacta/vega/wiki/Legends}
6 | class Legend
7 |
8 | include ::Plotrb::Base
9 |
10 | # @!attribute size
11 | # @return [Symbol] the name of the scale that determines an item's size
12 | # @!attribute shape
13 | # @return [Symbol] the name of the scale that determines an item's shape
14 | # @!attribute fill
15 | # @return [Symbol] the name of the scale that determines an item's fill color
16 | # @!attribute stroke
17 | # @return [Symbol] the name of the scale that determines an item's stroke color
18 | # @!attribute orient
19 | # @return [Symbol] the orientation of the legend
20 | # @!attribute title
21 | # @return [Symbol] the title for the legend
22 | # @!attribute format
23 | # @return [String] an optional formatting pattern for legend labels
24 | # @!attribute offset
25 | # @return [Integer] the offset of the legend
26 | # @!attribute values
27 | # @return [Array] explicitly set the visible legend values
28 | # @!attributes properties
29 | # @return [MarkProperty] the property set definitions
30 | LEGEND_PROPERTIES = [:size, :shape, :fill, :stroke, :orient, :title,
31 | :format, :offset, :values, :properties]
32 |
33 | add_attributes *LEGEND_PROPERTIES
34 |
35 | def initialize(&block)
36 | define_single_val_attributes(:size, :shape, :fill, :stroke, :orient,
37 | :title, :format, :offset)
38 | define_multi_val_attributes(:values)
39 | self.singleton_class.class_eval {
40 | alias_method :name, :title
41 | alias_method :offset_by, :offset
42 | }
43 | self.instance_eval(&block) if block_given?
44 | ::Plotrb::Kernel.legends << self
45 | self
46 | end
47 |
48 | def properties(element=nil, &block)
49 | @properties ||= {}
50 | return @properties unless element
51 | @properties.merge!(
52 | element.to_sym => ::Plotrb::Mark::MarkProperty.new(:text, &block)
53 | )
54 | self
55 | end
56 |
57 | def method_missing(method, *args, &block)
58 | case method.to_s
59 | when /^at_(left|right)$/ # set orient of the legend
60 | self.orient($1.to_sym, &block)
61 | when /^with_(\d+)_name/ # set the title of the legend
62 | self.title($1.to_s, &block)
63 | else
64 | super
65 | end
66 | end
67 |
68 | private
69 |
70 | def attribute_post_processing
71 | process_orient
72 | process_format
73 | process_properties
74 | process_size
75 | process_shape
76 | process_fill
77 | process_stroke
78 | end
79 |
80 | def process_orient
81 | return unless @orient
82 | unless %i(left right).include?(@orient.to_sym)
83 | raise ArgumentError, 'Invalid Axis orient'
84 | end
85 | end
86 |
87 | def process_format
88 | return unless @format
89 | # D3's format specifier has general form:
90 | # [[fill]align][sign][symbol][0][width][,][.precision][type]
91 | # the regex is taken from d3/src/format/format.js
92 | re =
93 | /(?:([^{])?([<>=^]))?([+\- ])?([$#])?(0)?(\d+)?(,)?(\.-?\d+)?([a-z%])?/i
94 | @format = @format.to_s
95 | if @format =~ re
96 | if "#{$1}#{$2}#{$3}#{$4}#{$5}#{$6}#{$7}#{$8}#{$9}" != @format
97 | raise ArgumentError, 'Invalid format specifier'
98 | end
99 | end
100 | end
101 |
102 | def process_size
103 | return unless @size
104 | case @size
105 | when String
106 | unless ::Plotrb::Kernel.find_scale(@size)
107 | raise ArgumentError, 'Scale not found'
108 | end
109 | when ::Plotrb::Scale
110 | @size = @size.name
111 | else
112 | raise ArgumentError, 'Unknown Scale'
113 | end
114 | end
115 |
116 | def process_shape
117 | return unless @shape
118 | case @shape
119 | when String
120 | unless ::Plotrb::Kernel.find_scale(@shape)
121 | raise ArgumentError, 'Scale not found'
122 | end
123 | when ::Plotrb::Scale
124 | @shape = @shape.name
125 | else
126 | raise ArgumentError, 'Unknown Scale'
127 | end
128 | end
129 |
130 | def process_fill
131 | return unless @fill
132 | case @fill
133 | when String
134 | unless ::Plotrb::Kernel.find_scale(@fill)
135 | raise ArgumentError, 'Scale not found'
136 | end
137 | when ::Plotrb::Scale
138 | @fill = @fill.name
139 | else
140 | raise ArgumentError, 'Unknown Scale'
141 | end
142 | end
143 |
144 | def process_stroke
145 | return unless @stroke
146 | case @stroke
147 | when String
148 | unless ::Plotrb::Kernel.find_scale(@stroke)
149 | raise ArgumentError, 'Scale not found'
150 | end
151 | when ::Plotrb::Scale
152 | @stroke = @stroke.name
153 | else
154 | raise ArgumentError, 'Unknown Scale'
155 | end
156 | end
157 |
158 |
159 | def process_properties
160 | return unless @properties
161 | valid_elements = %i(title labels symbols gradient legend)
162 | unless (@properties.keys - valid_elements).empty?
163 | raise ArgumentError, 'Invalid property element'
164 | end
165 | end
166 |
167 | end
168 | end
169 |
--------------------------------------------------------------------------------
/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.to_s.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/simple.rb:
--------------------------------------------------------------------------------
1 | #--
2 | # simple.rb: Shortcuts for making some simple plots.
3 | # Copyright (c) 2013 Colin J. Fuller and the Ruby Science Foundation
4 | # All rights reserved.
5 | #
6 | # Redistribution and use in source and binary forms, with or without
7 | # modification, are permitted provided that the following conditions
8 | # are met:
9 | # - Redistributions of source code must retain the above copyright
10 | # notice, this list of conditions and the following disclaimer.
11 | #
12 | # - Redistributions in binary form must reproduce the above copyright
13 | # notice, this list of conditions and the following disclaimer in the
14 | # documentation and/or other materials provided with the
15 | # distribution.
16 | #
17 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21 | # HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
22 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
23 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
24 | # OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
25 | # AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
26 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
27 | # WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28 | # POSSIBILITY OF SUCH DAMAGE.
29 | #++
30 |
31 | require 'plotrb'
32 |
33 | module Plotrb
34 | module Simple
35 |
36 | SCATTER_DATA_NAME = 'scatter'
37 | SCATTER_X_SCALE_NAME = 'scatter_x'
38 | SCATTER_Y_SCALE_NAME = 'scatter_y'
39 |
40 | #
41 | # Data name used by scatter plot
42 | #
43 | def self.scatter_data_name
44 | SCATTER_DATA_NAME
45 | end
46 |
47 | #
48 | # Scale name used by scatter plot for x axis
49 | #
50 | def self.scatter_x_scale_name
51 | SCATTER_X_SCALE_NAME
52 | end
53 |
54 | #
55 | # Scale name used by scatter plot for y axis
56 | #
57 | def self.scatter_y_scale_name
58 | SCATTER_Y_SCALE_NAME
59 | end
60 |
61 | #
62 | # Generate a simple 2d scatter plot.
63 | #
64 | # @param [NMatrix, Array] x the x datapoints; if a single row, will be used
65 | # for all y dataseries, if multiple rows, each row of x will be used for
66 | # the corresponding row of y
67 | # @param [NMatrix, Array] y the y datapoints. Can be a single dimensional array,
68 | # or 2D with multiple series in rows; column dimension should match the
69 | # number of elements in x.
70 | # @param [String, Array] symbol the type of symbol to be used to
71 | # plot the points. Can be any symbol Vega understands: circle, square,
72 | # cross, diamond, triangle-up, triangle-down. If a single String is
73 | # provided, this will be used for all the points. If an Array of Strings
74 | # is provided, each symbol in the array will be used for the corresponding
75 | # data series (row) in y. Default: 'circle'
76 | # @param [String, Array] color the color to be used to plot the
77 | # points. If a single String is provided, this will be used for all the
78 | # points. If an Array of Strings is provided, each color in the array will
79 | # be used for the corresponding data series (row) in y. Default: 'blue'
80 | # @param [Numeric] markersize the size of the marker in pixels. Default: 20
81 | # @param [Numeric] width the visualization width in pixels. Default: 640
82 | # @param [Numeric] height the visualization height in pixels. Default: 480
83 | # @param [Array, String] domain the domain for the plot (limits on the
84 | # x-axis). This can be a 2-element array of bounds or any other object
85 | # that Plotrb::Scale::from understands. Default: scale to x data
86 | # @param [Array, String] range the range for the plot (limits on the
87 | # y-axis). This can be a 2-element array of bounds or any other object
88 | # that Plotrb::Scale::from understands. Default: scale to first row of
89 | # y.
90 | #
91 | # @return [Plotrb::Visualization] A visualization object. (This can be
92 | # written to a json string for Vega with #generate_spec.)
93 | #
94 | # method signature for ruby 2.0 kwargs:
95 | # def scatter(x, y, symbol: 'circle', color: 'blue', markersize: 20,
96 | # width: 640, height: 480, domain: nil, range: nil)
97 | def self.scatter(x, y, kwargs={})
98 | kwargs = {symbol: 'circle', color: 'blue', markersize: 20, width: 640,
99 | height: 480, domain: nil, range: nil}.merge(kwargs)
100 | symbol = kwargs[:symbol]
101 | color = kwargs[:color]
102 | markersize = kwargs[:markersize]
103 | width = kwargs[:width]
104 | height = kwargs[:height]
105 | domain = kwargs[:domain]
106 | range = kwargs[:range]
107 |
108 | datapoints = []
109 | n_sets = 1
110 | x_n_sets = 1
111 | x_size = x.size
112 |
113 | if x.respond_to?(:shape) and x.shape.length > 1 then # x is 2D NMatrix
114 | x_n_sets = x.shape[0]
115 | x_size = x.shape[1]
116 | elsif x.instance_of? Array and x[0].instance_of? Array then # x is nested Array
117 | x_n_sets = x.size
118 | x_size = x[0].size
119 | end
120 |
121 | if y.respond_to?(:shape) and y.shape.length > 1 then # y is 2D NMatrix
122 | n_sets = y.shape[0]
123 | elsif y.instance_of? Array and y[0].instance_of? Array then # y is nested array
124 | n_sets = y.size
125 | end
126 |
127 | x_size.times do |i|
128 | dp = {}
129 | n_sets.times do |j|
130 |
131 | xj = j.modulo(x_n_sets)
132 | if x.respond_to?(:shape) and x.shape.length > 1 then
133 | dp["x#{xj}".to_sym] = x[xj, i]
134 | elsif x.instance_of? Array and x[0].instance_of? Array then
135 | dp["x#{xj}".to_sym] = x[xj][i]
136 | else
137 | dp["x#{xj}".to_sym] = x[i]
138 | end
139 |
140 | indices = [i]
141 | if y.respond_to?(:shape) and y.shape.length > 1 then
142 | indices = [j,i]
143 | end
144 | if y.instance_of? Array and y[0].instance_of? Array then
145 | dp["y#{j}".to_sym] = y[j][*indices]
146 | else
147 | dp["y#{j}".to_sym] = y[*indices]
148 | end
149 | end
150 |
151 | datapoints << dp
152 | end
153 |
154 | Plotrb::Kernel.data.delete_if { |d| d.name == scatter_data_name }
155 | dataset= Plotrb::Data.new.name(scatter_data_name)
156 | dataset.values(datapoints)
157 |
158 | domain_in = "#{scatter_data_name}.x0"
159 | if domain then
160 | domain_in = domain
161 | end
162 | range_in = "#{scatter_data_name}.y0"
163 | if range then
164 | range_in = range
165 | end
166 |
167 | Plotrb::Kernel.scales.delete_if { |d| d.name == scatter_x_scale_name or d.name == scatter_y_scale_name }
168 |
169 | xs = linear_scale.name(scatter_x_scale_name).from(domain_in).to_width
170 | ys = linear_scale.name(scatter_y_scale_name).from(range_in).to_height
171 |
172 | marks = []
173 | n_sets.times do |j|
174 | marks << symbol_mark.from(dataset) do
175 | c_j = color.instance_of?(Array) ? color[j] : color
176 | s_j = symbol.instance_of?(Array) ? symbol[j] : symbol
177 | x_j = j.modulo(x_n_sets)
178 | enter do
179 | x_start { scale(xs).from("x#{x_j}") }
180 | y_start { scale(ys).from("y#{j}") }
181 | size markersize
182 | shape s_j
183 | fill c_j
184 | end
185 | end
186 | end
187 |
188 | visualization.width(width).height(height) do
189 | data dataset
190 | scales xs, ys
191 | marks marks
192 | axes x_axis.scale(xs), y_axis.scale(ys)
193 | end
194 | end
195 | end
196 | end
197 |
198 |
--------------------------------------------------------------------------------
/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 Symbol
578 | "data.#{field}"
579 | when String
580 | if field.start_with?('data.') || extra_fields.include?(field.to_sym)
581 | field
582 | else
583 | "data.#{field}"
584 | end
585 | when ::Plotrb::Data
586 | 'data'
587 | else
588 | raise ArgumentError, 'Invalid data field'
589 | end
590 | end
591 |
592 | end
593 |
594 | end
595 |
--------------------------------------------------------------------------------
/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 | # @!attributes legends
29 | # @return [Array