Subscribe:

Pages

Plotting Multiple Data Sources in RSRuby

In this post, I look at plotting multiple data sources on a single graph from RSRuby, and also how to use Ruby to simplify the method of constructing such graphs.

Plotting Multiple Data Sources

Consider the table below as a typical example, which contains some data on craters on different planetary bodies of the solar system. There are three columns. The first gives the parent body (planet) of the crater, the second gives the diameter of the crater, and the third gives the depth of the crater. What we would like to see is a plot with the two axes holding the quantitative information, and the labels of the points showing the planet. (Note: these are not real data.)











Planetary BodyDiameter (km)Depth (km)
Moon3355.7
Moon663.0
Moon412.5
Mars501.0
Mars801.5
Ganymede200.8
Ganymede701.0
Ganymede800.9


In a previous post, I looked at plotting a single set of points. For example, to make a simple graph of diameter vs depth for the Moon's craters, you could use the following code:
require 'rsruby'

@@r = RSRuby.instance

@@r.png("mygraph.png")
@@r.plot([335,66,41], [5.7,3.0,2.5],
:xlab => "Diameter (km)", :ylab => "Depth (km)",
:main => "Crater diameter vs. depth",
:xlim => [0, 350], :ylim => [0, 6],
:pch => 15, :col => "blue")
@@r.eval_R("dev.off()")

The points are plotted using a symbol, controlled by the number passed to :pch. The range of numbers for :pch goes from 1 to 25. http://www.phaget4.org/R/plot.html has a diagram showing the effect of the different numbers, but it is good to experiment a little for your own graph.

To add more points to the graph, with different symbols, we use the 'points' command for each group of data, before the 'eval' command:
require 'rsruby'

@@r = RSRuby.instance

@@r.png("mygraph.png")
@@r.plot([335,66,41], [5.7,3.0,2.5],
:xlab => "Diameter (km)", :ylab => "Depth (km)",
:main => "Crater diameter vs. depth",
:xlim => [0, 350], :ylim => [0, 6],
:pch => 15, :col => "blue")
@@r.points([50,80], [1.0,1.5], :pch => 19, :col => "green")
@@r.points([20,70,80], [0.8,1.0,0.9], :pch => 4, :col => "red")
@@r.eval_R("dev.off()")

The resulting graph is:


Simplifying the Code

Automation is a big benefit of doing the above kind of process from Ruby. How can we simplify the code to make it as easy as possible to construct and save a graph from a new set of data? For example, if we extend the above table with another two attributes, we might want to create all possible 6 graphs, one for each pair of attributes. To make the call as easy as possible, we need to encapsulate the graph construction process within a single function with some suitable parameters.

First, we need a structure to represent the data in a single group. Ruby provides a Struct data structure just for such purposes. We create a Struct holding the label, points and display information for the group of data.
DataItems = Struct.new(:name, :x_points, :y_points, :pch, :col)

Then, the graph construction function can process an array of these structures, adding the information to the graph. The construction process adds all the data using the 'points' method in R. For this to work, you have to provide 'xlim' and 'ylim' in the parameters so the graph is drawn to the correct size. The 'plot_graph' method works by iterating through a list of provided DataItems, and adding the points to the graph using the display attributes. As an extra, there is a line to add a legend to the graph, again by collecting information from the DataItems array:
module L
R = RSRuby.instance

def L.plot_graph(filename, data, params)
R.png(filename, :res => 100)
R.plot([], [], params)
data.each do |items|
R.points(items.x_points, items.y_points, :pch => items.pch, :col => items.col)
end
R.legend("bottomright", data.collect {|items| items.name},
:col => data.collect {|items| items.col},
:pch => data.collect {|items| items.pch})
R.eval_R("dev.off()")
end
end

Finally, this is how to plot the same graph as above:
L.plot_graph("graph.png",
[DataItems.new("Moon", [335,66,41], [5.7,3.0,2.5], 15, "blue"),
DataItems.new("Mars", [50,80], [1.0,1.5], 19, "green"),
DataItems.new("Ganymede", [20,70,80], [0.8,1.0,0.9], 4, "red")],
:xlim => [0, 350],
:ylim => [0, 6],
:xlab => "Diameter (km)",
:ylab => "Depth (km)",
:main => "Crater diameter vs. depth")


There are still parts that could be included as parameters to the function, such as the location of the legend. The advantage of an approach such as the above is not to make it into a general function, but to provide a pattern to copy and modify in a particular case. With the above, the call to create a graph is a single method call, and that can make automation of graph construction a lot easier. For example, to create a whole range of graphs from a table of data.