Search this site


Metadata

Articles

Projects

Presentations

More rmagick/rvg playtime

While working on graphs tonight, I found that calculation and labelling of ticks should be provided by special 'tick' classes. The iterator (the 'each' method) takes a min and max value and yields ticks in that range. This allows you to:
  • A 'tick' provider should just be an iterable class (foo.each, etc) which provides the tick position and optional label.
  • A graph can have multiple ticks per axis, allowing you to have 'major' ticks labeled while 'minor' ticks are not labeled, and even more than two layers of ticks on each axis.
  • The same tick classes can easily be used to draw both the graph ticks and the grid.
  • Trivially have 'time format' tickers
  • Have a 'smart time ticker' that looks at the min/max and determines the correct set of time ticks to display (display format, tick distance, tick alignment, etc). Can use multiple 'time ticker' instances internally (code reuse!)
I'm sure this has all been though of before, but it's a research experience for me :)

At any rate, I'm finding myself wondering if RMagick/rvg is really the right tool. It certainly makes doing graphics trivial, but even for what I see as a simple graph it takes a little over a second to render, which would hurt usability if multiple graphs needed rendering simultaneously.

The bottleneck seems to be with text rendering. If I disable text display in the graph (tick labels, etc), graph rendering drops by 0.5 seconds (from 1.1). Switching from 'gif' to 'png' output shaved 0.2 seconds on average of rendering, which is interesting.

Today's results, with real data:

graph = RPlot::Graph.new(400, 200, "Ping results for www.google.com")
pingsource = RPlot::ArrayDataSource.new
File.open("/b/pingdata").each do |line|
  time,latency = line.split
  pingsource.points << [time.to_f, latency.to_f]
end
pingsource.points = pingsource.points[-300..-1]
graph.sources << pingsource
graph.xtickers << RPlot::SmartTimeTicker.new
graph.ytickers << RPlot::LabeledTicker.new(alignment=0, step=25)
graph.render("test.png")

Graphs in Ruby with RMagick

I'm always finding myself wanting to graph random data. Gnuplot is nice, but not enjoyably scriptable. Matplotlib in python is too matlab-ish, or was when I looked at it last (though it looks much improved now). Some ruby options exist (even ruby+gnuplot), but none were much to my tastes.

I started fiddling around with RMagick and stumbled across what it calls "RVG" (ruby vector graphics). From the site:

RVG (Ruby Vector Graphics) is a facade for RMagick's Draw class that supplies a drawing API based on the Scalable Vector Graphics W3C recommendation.
The API is pretty reasonable and hasn't hindered me yet and feels good after having hacked with it for a few hours: Simple operations like point translation, scaling, rotating, flipping, etc are simple in code; the api is well documented; images can be embedded easily into another which allows for easily writing modular code.

Anyway, the goal of this adventure was to come up with something that would produce non-crappy plots. Main emphasis on having a means to apply axis labels and ticks that wasn't painful. The result is below: (x-axis ticks are hour-aligned and have 12 hour steps, y-axis ticks are single-value aligned)

Here's the code that generates the above graph (using rplot.rb). A lot of things (like axis label tick alignment and stepping) are hardcoded right now, but that will obviously change if I decide this project needs attention (and I don't find something that does the same thing but better).

# graph some random stuff, like log(x) and sin(x)
# use time for the 'x' to demo time formatting
# each point is an hour (i * 3600)
graph = RPlot.new(400, 200, "Happy Graph")

points = 60
axis = GraphAxis.new
(1..points).each do |i|
  axis.points << [Time.now.to_f + i*3600, Math.log(i)]
end

axis2 = GraphAxis.new
(1..points).each do |i|
  axis2.points << [Time.now.to_f + i*3600, Math.sin((i / 2.0).to_f) + 1]
end

graph.axes << axis
graph.axes << axis2

graph.render("/home/jls/public_html/test.gif")