Graphviz On Rails
This article is a description of how to build an interesting ajax'd Rails application that allows the user to enter a Graphviz input file, and then render it with a variety of different options. The output appears alongside the input file, as an SVG object (therefore you'll need a browser that can display inline SVG. Firefox 3 seems to be best, Opera 9 tries hard.)
Have a look at the demo app itself, and try an input of “a→b” as a starting point …
First, get a new default Rails environment set up …
$ rails graphviz
… then make sure you can connect your web browser to the app, either by configuring Apache, or by running the provided WEBrick server. There's no need to do any more configuration, we're not going to be using a database at all.
Now change directory to your new environment, and use the generate to create a new controller :-
$ cd graphviz $ script/generate controller Graphviz
This will create the standard directories for files, and a stub file for the controller itself, in app/controllers/graphviz_conroller.rb. We should now edit the controller file, and add a default action … at the moment the action will need no code, as all we want it to do is to invoke the template renderer …
app/controller/graphviz_controller.rb
class GraphvizController < ApplicationController def index end end
Now we have to go to the app/views/graphviz directory, and create our index.rhtml file. This will be rendered whenever the Graphviz controller is called.
To make this controller active as the default response for your server, as it is in the demo app, we have to update the Routing information for URLs … config/routes.rb
... map.connect '', :controller => "graphviz"
and remove public/index.html
Because I promised you ajax in this example, we have to include the prototype.js javascript library, and create a named div field where the output will go. At the top of the page I'll place the form that we will take user input from.
app/views/graphviz/index.rhtml
<html>
<head>
<title>Graphviz on Rails</title>
<%= javascript_include_tag "prototype" %>
</head>
<body>
<h1>Graphviz on Rails</h1>
<%= form_remote_tag(:update=>"dotsvg",:url=>{:action=>'update_dotsvg'}) %>
digraph G {<br />
<%= text_area(:graph, @params[:graph], :cols => 40, :rows => 5) %>
<br />
}
<%= submit_tag "Go" %>
<%= end_form_tag %>
<div id="dotsvg">
<!-- An SVG graphic will go here when the form is submitted -->
</div>
</body>
</html>
So, let's look at this; in the HEAD block we have the javascript_include_tag providing the reference to the prototype library for us. Have a look at the rendered HTML for the page; Rails has expanded this out to :-
<script src="/javascripts/prototype.js?1146359518" type="text/javascript"></script>
Now we move on to the form_remote_tag. This needs two parameters, the name of the DIV tag in your document that will take the results (“dotsvg” for us), and a URL that will provide the results. In Rails fashion, we haven't provided an actual URL, but the name of an action. This is assumed to be within the current controller, and therefore is expected to be a section within the app/controllers/graphviz_controller.rb file. We haven't put anything there yet, so if you actually try to submit this form you will see a nice error message added to your page – “Unknown action”.
So, let's move back to the controller file, and add this action!
When the form is submitted, the contents of the texa_area :graph are added into the instance variable params, which would actually store every input field from the form. We need to take the :graph, and pass it to the Graphviz executable, /usr/bin/dot. In return we will get an SVG file, which we'll put into the instance variable gvsvg.
To actually invoke the dot command we'll use the IO.popen method, which allows us to control exactly when we collect the command output and when we provide it input. Using STDIO like this means we avoid the necessity to create and manage any temporary files.
app/controllers/graphviz_controller.rb
def update_dotsvg
gv=IO.popen("/usr/bin/dot -q -Tsvg","w+")
gv.puts "digraph G{", @params[:graph], "}"
gv.close_write
@gvsvg=gv.read
end
In the view for this action, we output an object tag, and include the gvsvg data in it.
app/views/graphviz/update_dotsvg.rhtml
<object data="data:image/svg+xml;base64,<%=Base64.encode64(@gvsvg)%>" />
If you get all this right, and run the application, and enter a simple graph, like “a→b” and press the Go button … you might wonder why nothing seemed to happen. Here's the bad news … sending the object data inline for SVG doesn't seem to work on very many browsers. It's in the specifications, but isn't widely implemented. Firefox 1.5 and Opera9 will both have a decent attempt at the job, however.
Try viewing the source of the page, and you might see the returned object. It's more likely that you won't though – because View Source in most web browsers sends a new HTTP request. You'll benefit from the Firefox extension FireBug as this is one of the few ways of seeing what AJAX methods are being invoked, and what data is being returned. To go with this, the View Formatted Source extension can be useful. Toggling inline mode can sometimes cause Firefox to display an otherwise-hidden SVG image.
Anyway, assuming that we can see anything at all ;-) let's continue to improve things. The object doesn't seem to be large enough for the SVG data, so let's see if we can improve that. The SVG file as generated by dot has size attributes in it, so let's extract them as the data is generated, and make them available to the view, so we can resize the object box. Rails comes with the REXML library, which makes it easy to extract the attributes we want – we then have to remove the “point” unit text that's there, and convert the result to an integer (actually, at this point we don't have to convert it, as it'll work perfectly in HTML regardless of the original form; but if we want to do anything numeric to the size values it wil be essential to convert it)
app/controller/graphviz_controller.rb
def update_dotsvg ... @gvsvg=gv.read parse_svg=REXML::Document.new(@gvsvg) @svg_width=parse_svg.root.attributes["width"].gsub(/pt$/,'').to_i @svg_height=parse_svg.root.attributes["height"].gsub(/pt$/,'').to_i
app/views/graphviz/update_dotsvg.rhtml
<object data="data:image/svg+xml;base64,<%=Base64.encode64(@gvsvg)%>" width="<%=@svg_width%>" height="<%=@svg_height%>" />
Now let's address a user interface issue – there is very little visual feedback to the end user that their data has been received and is being worked on. This is mostly important because the graphviz programs themselves are not especially fast, and neither are the SVG renderers in the browser. Let's fix that with a little hack from the effects javascript library …
app/views/graphviz/index.rhtml
<%= javascript_include_tag "prototype", "effects" %>A
...
<%= form_remote_tag(
:update => "dotsvg",
:url => {:action=>'update_dotsvg'},
:loading => "new Element.update('dotsvg','<p>Loading graph ...</p>')",
:complete => "new Effect.Highlight('dotsvg');"
) %>
Now, as soon as the form is submitted the text “Loading graph …” will appear in the SVG output area, and when the graph comes back from the server we will see the Yellow Fade effect to announce it.
We've used a lot of technologies on our way to this app, so we should have a quick overview of them now :-