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