Skip to content

Commit 15bfbed

Browse files
committed
Merge pull request #1 from cjfuller/simple_scatter
added simplified method for making a 2D scatter plot
2 parents 0c16893 + f497854 commit 15bfbed

File tree

2 files changed

+258
-0
lines changed

2 files changed

+258
-0
lines changed

lib/plotrb/simple.rb

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
#--
2+
# simple.rb: Shortcuts for making some simple plots.
3+
# Copyright (c) 2013 Colin J. Fuller and the Ruby Science Foundation
4+
# All rights reserved.
5+
#
6+
# Redistribution and use in source and binary forms, with or without
7+
# modification, are permitted provided that the following conditions
8+
# are met:
9+
# - Redistributions of source code must retain the above copyright
10+
# notice, this list of conditions and the following disclaimer.
11+
#
12+
# - Redistributions in binary form must reproduce the above copyright
13+
# notice, this list of conditions and the following disclaimer in the
14+
# documentation and/or other materials provided with the
15+
# distribution.
16+
#
17+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18+
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19+
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20+
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21+
# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
22+
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
23+
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
24+
# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
25+
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
26+
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
27+
# WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28+
# POSSIBILITY OF SUCH DAMAGE.
29+
#++
30+
31+
require 'plotrb'
32+
33+
module Plotrb
34+
module Simple
35+
36+
SCATTER_DATA_NAME = 'scatter'
37+
SCATTER_X_SCALE_NAME = 'scatter_x'
38+
SCATTER_Y_SCALE_NAME = 'scatter_y'
39+
40+
#
41+
# Data name used by scatter plot
42+
#
43+
def self.scatter_data_name
44+
SCATTER_DATA_NAME
45+
end
46+
47+
#
48+
# Scale name used by scatter plot for x axis
49+
#
50+
def self.scatter_x_scale_name
51+
SCATTER_X_SCALE_NAME
52+
end
53+
54+
#
55+
# Scale name used by scatter plot for y axis
56+
#
57+
def self.scatter_y_scale_name
58+
SCATTER_Y_SCALE_NAME
59+
end
60+
61+
#
62+
# Generate a simple 2d scatter plot.
63+
#
64+
# @param [NMatrix, Array] x the x datapoints; if a single row, will be used
65+
# for all y dataseries, if multiple rows, each row of x will be used for
66+
# the corresponding row of y
67+
# @param [NMatrix, Array] y the y datapoints. Can be a single dimensional array,
68+
# or 2D with multiple series in rows; column dimension should match the
69+
# number of elements in x.
70+
# @param [String, Array<String>] symbol the type of symbol to be used to
71+
# plot the points. Can be any symbol Vega understands: circle, square,
72+
# cross, diamond, triangle-up, triangle-down. If a single String is
73+
# provided, this will be used for all the points. If an Array of Strings
74+
# is provided, each symbol in the array will be used for the corresponding
75+
# data series (row) in y. Default: 'circle'
76+
# @param [String, Array<String>] color the color to be used to plot the
77+
# points. If a single String is provided, this will be used for all the
78+
# points. If an Array of Strings is provided, each color in the array will
79+
# be used for the corresponding data series (row) in y. Default: 'blue'
80+
# @param [Numeric] markersize the size of the marker in pixels. Default: 20
81+
# @param [Numeric] width the visualization width in pixels. Default: 640
82+
# @param [Numeric] height the visualization height in pixels. Default: 480
83+
# @param [Array, String] domain the domain for the plot (limits on the
84+
# x-axis). This can be a 2-element array of bounds or any other object
85+
# that Plotrb::Scale::from understands. Default: scale to x data
86+
# @param [Array, String] range the range for the plot (limits on the
87+
# y-axis). This can be a 2-element array of bounds or any other object
88+
# that Plotrb::Scale::from understands. Default: scale to first row of
89+
# y.
90+
#
91+
# @return [Plotrb::Visualization] A visualization object. (This can be
92+
# written to a json string for Vega with #generate_spec.)
93+
#
94+
# method signature for ruby 2.0 kwargs:
95+
# def scatter(x, y, symbol: 'circle', color: 'blue', markersize: 20,
96+
# width: 640, height: 480, domain: nil, range: nil)
97+
def self.scatter(x, y, kwargs={})
98+
kwargs = {symbol: 'circle', color: 'blue', markersize: 20, width: 640,
99+
height: 480, domain: nil, range: nil}.merge(kwargs)
100+
symbol = kwargs[:symbol]
101+
color = kwargs[:color]
102+
markersize = kwargs[:markersize]
103+
width = kwargs[:width]
104+
height = kwargs[:height]
105+
domain = kwargs[:domain]
106+
range = kwargs[:range]
107+
108+
datapoints = []
109+
n_sets = 1
110+
x_n_sets = 1
111+
x_size = x.size
112+
113+
if x.respond_to?(:shape) and x.shape.length > 1 then # x is 2D NMatrix
114+
x_n_sets = x.shape[0]
115+
x_size = x.shape[1]
116+
elsif x.instance_of? Array and x[0].instance_of? Array then # x is nested Array
117+
x_n_sets = x.size
118+
x_size = x[0].size
119+
end
120+
121+
if y.respond_to?(:shape) and y.shape.length > 1 then # y is 2D NMatrix
122+
n_sets = y.shape[0]
123+
elsif y.instance_of? Array and y[0].instance_of? Array then # y is nested array
124+
n_sets = y.size
125+
end
126+
127+
x_size.times do |i|
128+
dp = {}
129+
n_sets.times do |j|
130+
131+
xj = j.modulo(x_n_sets)
132+
if x.respond_to?(:shape) and x.shape.length > 1 then
133+
dp["x#{xj}".to_sym] = x[xj, i]
134+
elsif x.instance_of? Array and x[0].instance_of? Array then
135+
dp["x#{xj}".to_sym] = x[xj][i]
136+
else
137+
dp["x#{xj}".to_sym] = x[i]
138+
end
139+
140+
indices = [i]
141+
if y.respond_to?(:shape) and y.shape.length > 1 then
142+
indices = [j,i]
143+
end
144+
if y.instance_of? Array and y[0].instance_of? Array then
145+
dp["y#{j}".to_sym] = y[j][*indices]
146+
else
147+
dp["y#{j}".to_sym] = y[*indices]
148+
end
149+
end
150+
151+
datapoints << dp
152+
end
153+
154+
Plotrb::Kernel.data.delete_if { |d| d.name == scatter_data_name }
155+
dataset= Plotrb::Data.new.name(scatter_data_name)
156+
dataset.values(datapoints)
157+
158+
domain_in = "#{scatter_data_name}.x0"
159+
if domain then
160+
domain_in = domain
161+
end
162+
range_in = "#{scatter_data_name}.y0"
163+
if range then
164+
range_in = range
165+
end
166+
167+
Plotrb::Kernel.scales.delete_if { |d| d.name == scatter_x_scale_name or d.name == scatter_y_scale_name }
168+
169+
xs = linear_scale.name(scatter_x_scale_name).from(domain_in).to_width
170+
ys = linear_scale.name(scatter_y_scale_name).from(range_in).to_height
171+
172+
marks = []
173+
n_sets.times do |j|
174+
marks << symbol_mark.from(dataset) do
175+
c_j = color.instance_of?(Array) ? color[j] : color
176+
s_j = symbol.instance_of?(Array) ? symbol[j] : symbol
177+
x_j = j.modulo(x_n_sets)
178+
enter do
179+
x_start { scale(xs).from("x#{x_j}") }
180+
y_start { scale(ys).from("y#{j}") }
181+
size markersize
182+
shape s_j
183+
fill c_j
184+
end
185+
end
186+
end
187+
188+
visualization.width(width).height(height) do
189+
data dataset
190+
scales xs, ys
191+
marks marks
192+
axes x_axis.scale(xs), y_axis.scale(ys)
193+
end
194+
end
195+
end
196+
end
197+

spec/plotrb/simple_spec.rb

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
#--
2+
# simple_spec.rb: specs for simple plot shortcuts
3+
# Copyright (c) 2013 Colin J. Fuller and the Ruby Science Foundation
4+
# All rights reserved.
5+
#
6+
# Redistribution and use in source and binary forms, with or without
7+
# modification, are permitted provided that the following conditions
8+
# are met:
9+
# - Redistributions of source code must retain the above copyright
10+
# notice, this list of conditions and the following disclaimer.
11+
#
12+
# - Redistributions in binary form must reproduce the above copyright
13+
# notice, this list of conditions and the following disclaimer in the
14+
# documentation and/or other materials provided with the
15+
# distribution.
16+
#
17+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18+
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19+
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20+
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21+
# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
22+
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
23+
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
24+
# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
25+
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
26+
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
27+
# WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28+
# POSSIBILITY OF SUCH DAMAGE.
29+
#++
30+
31+
require 'spec_helper'
32+
require 'plotrb/simple'
33+
34+
describe Plotrb::Simple do
35+
36+
context "scatter plots" do
37+
before :each do
38+
@xdata = [0,1,2,3,4,5]
39+
@ydata = [[0,1,4,9,16,25],[0,1,8,27,64,125]]
40+
end
41+
42+
it "should correctly make a scatter plot with supplied options" do
43+
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"}]}'
44+
end
45+
46+
it "should correctly make a scatter plot with default options and multiple data series" do
47+
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"}]}'
48+
end
49+
50+
it "should correctly make a scatter plot with default options and a single data series" do
51+
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"}]}'
52+
end
53+
54+
it "should correctly make a scatter plot with default options and multiple x and y data series" do
55+
@xdata = [[0,1,2,3,4,16], [10,11,12,13,14,15]]
56+
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"}]}'
57+
end
58+
end
59+
end
60+
61+

0 commit comments

Comments
 (0)