Ruby Ploticus
19 June 2006
In my recent post on EvaluatingRuby I mentioned that a colleague had put together a web app with some fancy numerical graphs. Someone emailed to ask how he did that. I added my short answer, ploticus, to the original bliki entry, but that led to the question of how he interfaced ruby with ploticus?
I actually ran into a similar problem myself recently as I wanted to use ploticus to graph some data for a personal project. The solution I came up with was actually very similar, if rather less refined, than the one my colleague used. As a result I thought I'd share it.
First a caveat - this is literally something I knocked up one evening. It isn't intended to be robust, performant or otherwise enterprisey. It's just for some data I use for me, myself, and I.
A sophisticated way to drive a C library like ploticus is to bind directly to the C API. Ruby makes this easy, so I'm told, but it's too much work for me (particularly if I want to be done before cocktail-hour). So my approach is to build a ploticus script and pipe it into ploticus. Ploticus can run by taking a script from standard input that controls what it does, so all I have to do is run ploticus within ruby and pipe commands into it. Roughly like this:
def generate script, outfile IO.popen("ploticus -png -o #{outfile} -stdin", 'w'){|p| p << script} end
To build up the script, I like to get objects that can work in my terms, and produce the necessary ploticus stuff. If you have anything that uses the prefabs, then putting together something is easy. I wanted to do clustered bar graphs, like this, which requires a ploticus script.
I built what I needed in three levels. At the lowest level is PloticusScripter, a class that builds up ploticus script commands. Here it is:
class PloticusScripter def initialize @procs = [] end def proc name result = PloticusProc.new name yield result @procs << result return result end def script result = "" @procs.each do |p| result << p.script_output << "\n\n" end return result end end class PloticusProc def initialize name @name = name @lines = [] end def script_output return (["#proc " + @name] + @lines).join("\n") end def method_missing name, *args, &proc line = name.to_s + ": " line.tr!('_', '.') args.each {|a| line << a.to_s << " "} @lines << line end end
As you see the scripter is just a list of proc commands (well they could be anything that responds to script_output - but I didn't need anything else yet). I can instantiate the scripter, call proc repeatedly to define my ploticus procs, then when I'm done call script to get the entire script for piping into ploticus.
The next level is something to build clustered bar graphs:
class PloticusClusterBar attr_accessor :rows, :column_names def initialize @rows = [] end def add_row label, data @rows << [label] + data end def getdata scripter scripter.proc("getdata") do |p| p.data generate_data end end def colors %w[red yellow blue green orange] end def clusters scripter column_names.size.times do |i| scripter.proc("bars") do |p| p.lenfield i + 2 p.cluster i+1 , "/", column_names.size p.color colors[i] p.hidezerobars 'yes' p.horizontalbars 'yes' p.legendlabel column_names[i] end end end def generate_data result = [] rows.each {|r| result << r.join(" ")} result << "\n" return result.join("\n") end end
This allows me to build a graph with simple calls to
add_row
to add data rows. This makes it much more easy
for me to build up the data for the graph.
To make a particular graph, I'll write a third class on top of that:
#produces similar to ploticus example in ploticus/gallery/students.htm class StudentGrapher def initialize @ps = PloticusScripter.new @pcb = PloticusClusterBar.new end def run load_data @pcb.getdata @ps areadef @pcb.clusters @ps end def load_data @pcb.column_names = ['Exam A', 'Exam B', 'Exam C', 'Exam D'] @pcb.add_row '01001', [44, 45, 71, 89] @pcb.add_row '01002', [56, 44, 54, 36] @pcb.add_row '01003', [46, 63, 28, 87] @pcb.add_row '01004', [42, 28, 39, 49] @pcb.add_row '01005', [52, 74, 84, 66] end def areadef @ps.proc("areadef") do |p| p.title "Example Student Data" p.yrange 0, 6 p.xrange 0, 100 p.xaxis_stubs "inc 10" p.yaxis_stubs "datafield=1" p.rectangle 1, 1, 6, 6 end end def generate outfile IO.popen("ploticus -png -o #{outfile} -stdin", 'w'){|p| p << script} end def script return @ps.script end end def run output = 'fooStudents.png' File.delete output if File.exists? output s = StudentGrapher.new s.run s.generate output end
It's a very simple example, but it's a good illustration of what I call the Gateway pattern. The PloticusClusterBar class is the gateway with the perfect interface for what I want to do. I make it transform between that convenient interface and what the real output needs to be. The PloticusScripter class is another level of gateway. Even for a simple thing like this I find a design of composed objects like this a good way to go. Which may only say how my brain's got twisted over the years.