diff --git a/lib/plotrb/simple.rb b/lib/plotrb/simple.rb new file mode 100644 index 0000000..fa86183 --- /dev/null +++ b/lib/plotrb/simple.rb @@ -0,0 +1,197 @@ +#-- +# simple.rb: Shortcuts for making some simple plots. +# Copyright (c) 2013 Colin J. Fuller and the Ruby Science Foundation +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# - Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# - Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the +# distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS +# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED +# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY +# WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +#++ + +require 'plotrb' + +module Plotrb + module Simple + + SCATTER_DATA_NAME = 'scatter' + SCATTER_X_SCALE_NAME = 'scatter_x' + SCATTER_Y_SCALE_NAME = 'scatter_y' + + # + # Data name used by scatter plot + # + def self.scatter_data_name + SCATTER_DATA_NAME + end + + # + # Scale name used by scatter plot for x axis + # + def self.scatter_x_scale_name + SCATTER_X_SCALE_NAME + end + + # + # Scale name used by scatter plot for y axis + # + def self.scatter_y_scale_name + SCATTER_Y_SCALE_NAME + end + + # + # Generate a simple 2d scatter plot. + # + # @param [NMatrix, Array] x the x datapoints; if a single row, will be used + # for all y dataseries, if multiple rows, each row of x will be used for + # the corresponding row of y + # @param [NMatrix, Array] y the y datapoints. Can be a single dimensional array, + # or 2D with multiple series in rows; column dimension should match the + # number of elements in x. + # @param [String, Array] symbol the type of symbol to be used to + # plot the points. Can be any symbol Vega understands: circle, square, + # cross, diamond, triangle-up, triangle-down. If a single String is + # provided, this will be used for all the points. If an Array of Strings + # is provided, each symbol in the array will be used for the corresponding + # data series (row) in y. Default: 'circle' + # @param [String, Array] color the color to be used to plot the + # points. If a single String is provided, this will be used for all the + # points. If an Array of Strings is provided, each color in the array will + # be used for the corresponding data series (row) in y. Default: 'blue' + # @param [Numeric] markersize the size of the marker in pixels. Default: 20 + # @param [Numeric] width the visualization width in pixels. Default: 640 + # @param [Numeric] height the visualization height in pixels. Default: 480 + # @param [Array, String] domain the domain for the plot (limits on the + # x-axis). This can be a 2-element array of bounds or any other object + # that Plotrb::Scale::from understands. Default: scale to x data + # @param [Array, String] range the range for the plot (limits on the + # y-axis). This can be a 2-element array of bounds or any other object + # that Plotrb::Scale::from understands. Default: scale to first row of + # y. + # + # @return [Plotrb::Visualization] A visualization object. (This can be + # written to a json string for Vega with #generate_spec.) + # + # method signature for ruby 2.0 kwargs: + # def scatter(x, y, symbol: 'circle', color: 'blue', markersize: 20, + # width: 640, height: 480, domain: nil, range: nil) + def self.scatter(x, y, kwargs={}) + kwargs = {symbol: 'circle', color: 'blue', markersize: 20, width: 640, + height: 480, domain: nil, range: nil}.merge(kwargs) + symbol = kwargs[:symbol] + color = kwargs[:color] + markersize = kwargs[:markersize] + width = kwargs[:width] + height = kwargs[:height] + domain = kwargs[:domain] + range = kwargs[:range] + + datapoints = [] + n_sets = 1 + x_n_sets = 1 + x_size = x.size + + if x.respond_to?(:shape) and x.shape.length > 1 then # x is 2D NMatrix + x_n_sets = x.shape[0] + x_size = x.shape[1] + elsif x.instance_of? Array and x[0].instance_of? Array then # x is nested Array + x_n_sets = x.size + x_size = x[0].size + end + + if y.respond_to?(:shape) and y.shape.length > 1 then # y is 2D NMatrix + n_sets = y.shape[0] + elsif y.instance_of? Array and y[0].instance_of? Array then # y is nested array + n_sets = y.size + end + + x_size.times do |i| + dp = {} + n_sets.times do |j| + + xj = j.modulo(x_n_sets) + if x.respond_to?(:shape) and x.shape.length > 1 then + dp["x#{xj}".to_sym] = x[xj, i] + elsif x.instance_of? Array and x[0].instance_of? Array then + dp["x#{xj}".to_sym] = x[xj][i] + else + dp["x#{xj}".to_sym] = x[i] + end + + indices = [i] + if y.respond_to?(:shape) and y.shape.length > 1 then + indices = [j,i] + end + if y.instance_of? Array and y[0].instance_of? Array then + dp["y#{j}".to_sym] = y[j][*indices] + else + dp["y#{j}".to_sym] = y[*indices] + end + end + + datapoints << dp + end + + Plotrb::Kernel.data.delete_if { |d| d.name == scatter_data_name } + dataset= Plotrb::Data.new.name(scatter_data_name) + dataset.values(datapoints) + + domain_in = "#{scatter_data_name}.x0" + if domain then + domain_in = domain + end + range_in = "#{scatter_data_name}.y0" + if range then + range_in = range + end + + Plotrb::Kernel.scales.delete_if { |d| d.name == scatter_x_scale_name or d.name == scatter_y_scale_name } + + xs = linear_scale.name(scatter_x_scale_name).from(domain_in).to_width + ys = linear_scale.name(scatter_y_scale_name).from(range_in).to_height + + marks = [] + n_sets.times do |j| + marks << symbol_mark.from(dataset) do + c_j = color.instance_of?(Array) ? color[j] : color + s_j = symbol.instance_of?(Array) ? symbol[j] : symbol + x_j = j.modulo(x_n_sets) + enter do + x_start { scale(xs).from("x#{x_j}") } + y_start { scale(ys).from("y#{j}") } + size markersize + shape s_j + fill c_j + end + end + end + + visualization.width(width).height(height) do + data dataset + scales xs, ys + marks marks + axes x_axis.scale(xs), y_axis.scale(ys) + end + end + end +end + diff --git a/spec/plotrb/simple_spec.rb b/spec/plotrb/simple_spec.rb new file mode 100644 index 0000000..9305e04 --- /dev/null +++ b/spec/plotrb/simple_spec.rb @@ -0,0 +1,61 @@ +#-- +# simple_spec.rb: specs for simple plot shortcuts +# Copyright (c) 2013 Colin J. Fuller and the Ruby Science Foundation +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# - Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# - Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the +# distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS +# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED +# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY +# WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +#++ + +require 'spec_helper' +require 'plotrb/simple' + +describe Plotrb::Simple do + + context "scatter plots" do + before :each do + @xdata = [0,1,2,3,4,5] + @ydata = [[0,1,4,9,16,25],[0,1,8,27,64,125]] + end + + it "should correctly make a scatter plot with supplied options" do + Plotrb::Simple.scatter(@xdata, @ydata, markersize: 40, domain: [0,10], range: [0,125], color: ['red', 'blue'], width: 1000, height: 1000, symbol: ['triangle-up', 'triangle-down']).generate_spec.should eq '{"width":1000,"height":1000,"data":[{"name":"scatter","values":[{"x0":0,"y0":0,"y1":0},{"x0":1,"y0":1,"y1":1},{"x0":2,"y0":4,"y1":8},{"x0":3,"y0":9,"y1":27},{"x0":4,"y0":16,"y1":64},{"x0":5,"y0":25,"y1":125}]}],"scales":[{"name":"scatter_x","type":"linear","domain":[0,10],"range":"width"},{"name":"scatter_y","type":"linear","domain":[0,125],"range":"height"}],"marks":[{"type":"symbol","from":{"data":"scatter"},"properties":{"enter":{"size":{"value":40},"shape":{"value":"triangle-up"},"x":{"field":"data.x0","scale":"scatter_x"},"y":{"field":"data.y0","scale":"scatter_y"},"fill":{"value":"red"}}}},{"type":"symbol","from":{"data":"scatter"},"properties":{"enter":{"size":{"value":40},"shape":{"value":"triangle-down"},"x":{"field":"data.x0","scale":"scatter_x"},"y":{"field":"data.y1","scale":"scatter_y"},"fill":{"value":"blue"}}}}],"axes":[{"type":"x","scale":"scatter_x"},{"type":"y","scale":"scatter_y"}]}' + end + + it "should correctly make a scatter plot with default options and multiple data series" do + Plotrb::Simple.scatter(@xdata, @ydata).generate_spec.should eq '{"width":640,"height":480,"data":[{"name":"scatter","values":[{"x0":0,"y0":0,"y1":0},{"x0":1,"y0":1,"y1":1},{"x0":2,"y0":4,"y1":8},{"x0":3,"y0":9,"y1":27},{"x0":4,"y0":16,"y1":64},{"x0":5,"y0":25,"y1":125}]}],"scales":[{"name":"scatter_x","type":"linear","domain":{"data":"scatter","field":"data.x0"},"range":"width"},{"name":"scatter_y","type":"linear","domain":{"data":"scatter","field":"data.y0"},"range":"height"}],"marks":[{"type":"symbol","from":{"data":"scatter"},"properties":{"enter":{"size":{"value":20},"shape":{"value":"circle"},"x":{"field":"data.x0","scale":"scatter_x"},"y":{"field":"data.y0","scale":"scatter_y"},"fill":{"value":"blue"}}}},{"type":"symbol","from":{"data":"scatter"},"properties":{"enter":{"size":{"value":20},"shape":{"value":"circle"},"x":{"field":"data.x0","scale":"scatter_x"},"y":{"field":"data.y1","scale":"scatter_y"},"fill":{"value":"blue"}}}}],"axes":[{"type":"x","scale":"scatter_x"},{"type":"y","scale":"scatter_y"}]}' + end + + it "should correctly make a scatter plot with default options and a single data series" do + Plotrb::Simple.scatter(@xdata, [0,1,4,9,16,25]).generate_spec.should eq '{"width":640,"height":480,"data":[{"name":"scatter","values":[{"x0":0,"y0":0},{"x0":1,"y0":1},{"x0":2,"y0":4},{"x0":3,"y0":9},{"x0":4,"y0":16},{"x0":5,"y0":25}]}],"scales":[{"name":"scatter_x","type":"linear","domain":{"data":"scatter","field":"data.x0"},"range":"width"},{"name":"scatter_y","type":"linear","domain":{"data":"scatter","field":"data.y0"},"range":"height"}],"marks":[{"type":"symbol","from":{"data":"scatter"},"properties":{"enter":{"size":{"value":20},"shape":{"value":"circle"},"x":{"field":"data.x0","scale":"scatter_x"},"y":{"field":"data.y0","scale":"scatter_y"},"fill":{"value":"blue"}}}}],"axes":[{"type":"x","scale":"scatter_x"},{"type":"y","scale":"scatter_y"}]}' + end + + it "should correctly make a scatter plot with default options and multiple x and y data series" do + @xdata = [[0,1,2,3,4,16], [10,11,12,13,14,15]] + Plotrb::Simple.scatter(@xdata, @ydata).generate_spec.should eq '{"width":640,"height":480,"data":[{"name":"scatter","values":[{"x0":0,"y0":0,"x1":10,"y1":0},{"x0":1,"y0":1,"x1":11,"y1":1},{"x0":2,"y0":4,"x1":12,"y1":8},{"x0":3,"y0":9,"x1":13,"y1":27},{"x0":4,"y0":16,"x1":14,"y1":64},{"x0":16,"y0":25,"x1":15,"y1":125}]}],"scales":[{"name":"scatter_x","type":"linear","domain":{"data":"scatter","field":"data.x0"},"range":"width"},{"name":"scatter_y","type":"linear","domain":{"data":"scatter","field":"data.y0"},"range":"height"}],"marks":[{"type":"symbol","from":{"data":"scatter"},"properties":{"enter":{"size":{"value":20},"shape":{"value":"circle"},"x":{"field":"data.x0","scale":"scatter_x"},"y":{"field":"data.y0","scale":"scatter_y"},"fill":{"value":"blue"}}}},{"type":"symbol","from":{"data":"scatter"},"properties":{"enter":{"size":{"value":20},"shape":{"value":"circle"},"x":{"field":"data.x1","scale":"scatter_x"},"y":{"field":"data.y1","scale":"scatter_y"},"fill":{"value":"blue"}}}}],"axes":[{"type":"x","scale":"scatter_x"},{"type":"y","scale":"scatter_y"}]}' + end + end +end + +