Ruby on Rails and SVG

Or, “How to add native SVG support to you Rails application”.

If you’re looking to display some charts in Rails, there are several options.

First are the hosted group and chief among them Google’s Chart API.  Through construction of image URLs, many types of charts can be generated.  I’ll actually be using some Google charts in my current project (line charts, spark lines, and bullet graphs) though it isn’t capable of generating a type of chart suggested by Mike Cohn: the release burndown bar chart, an advanced visualization of SCRUM progress.

Next is the popular Gruff Graphs though I tend to somewhat value my sanity and run for the hills whenever something is even peripherally related to ImageMagick.  Back in the good old days, resolving ImageMagick dependencies while attempting to install various Linux windows managers could drive a grown man to tears.  Of course, that was 10 years ago and with the advent of MacPorts and humanity in general, I’m sure it’s no longer as painful.  On the other hand, this does presume that your host of choice offers ImageMagick (GoDaddy does).

For my custom charting, because my application is an internal one, I decided to rely on native SVG support included in the latest versions of Firefox, Safari and Opera (and one can hope IE8).  In this quick guide to getting SVGs served from within a Rails app, I’ll run through everything you need to know to get up and running quickly.

Disclaimer: I’m new to Rails (and Ruby) so please feel free to post any corrections or clarifications in the comments and I’ll be sure to update this guide.

SVG Primer

SVG is basically an XML description of an image.  Mozilla has a terrific hub for SVG information, including links to examples and tutorials of various complexity.  Here’s one we can use for our purposes – a 100 pixel green square:




Step 1 – Allow SVG Recognition

In order for our Rails app to return a new representation of our chart resource via respond_to (we’ll get to this shortly), first update config/initializers/mime_types.rb:

# Be sure to restart your server when you modify this file.
# Add new mime types for use in respond_to blocks:
Mime::Type.register "image/svg+xml", :svg

Without this modification, Rails won’t be able to route your SVG format request and you’ll receive this error:

uninitialized constant Mime::SVG

Step 2 – Resources and SVGs

In my application model, a “project” is where everything begins.  A project has an active release, and this release has a release burn down.  Naturally, this is how I would like my charts to be served:

    http://localhost:3000/projects/51/release_burn_down_chart.svg

I’ll start by generating my controller:

    script/generate controller ReleaseBurnDownCharts show

And then nesting it inside my project by editing routes.rb:

    map.resources :projects do |project|
      project.resource :release_burn_down_chart
    end

This results in many routes, one of which we’ll be using:

    /projects/:project_id/release_burn_down_chart.:format

Step 3 – Quick Controller Test

Before going nuts, let’s verify that our controller can indeed serve SVGs and that Firefox will display them. My controller:

class ReleaseBurnDownChartsController < ApplicationController
  def show
    respond_to do |format|
      format.svg {
        render :inline => ""
      }
    end
  end
end

Pardon the inline rendering, it’s only a test! ;-) Now let’s see… if I request this URL in Firefox:

    http://localhost:3000/projects/51/release_burn_down_chart.svg

I should end up with a green square.

Score!  Rails magically understands the “.svg” and maps it to “:format” from the route I posted in Step 2.  This allows us to choose an alternative representation (XML in this case) as our response to the request.

Step 4 – Convention Over Configuration

What you might not have realized is that all along, Rails has been assuming a format of HTML and mapping this to your templates.  For example, show.html.erb.  What do you think would happen if we removed all of this un-Railsy code from our controller and reduced it to this?

class ReleaseBurnDownChartsController < ApplicationController
  def show
    # Ruh-roh!
  end
end

You guessed it – Rails figured out that it should be looking for an SVG file since that’s our new format.

You’re now left with a complaint from Rails that the default action wasn’t possible because we’re missing the assumed file.

Step 5 – Finally, An SVG File!

Create your show.svg.erb file, and drop it into the views directory for your controller.  How about using a nice Venn diagram from Mozilla?  Voila!  All done!  Let’s have a look:

Oops!  What happened?  Where’s our image?  To understand why, open up Tools -> Page Info in Firefox.

While Rails is indeed serving the correct SVG template, Firefox is confused because the content type HTTP header is still set to text/html.

Step 6 – Content-Type Correction

Have a look inside your chart controller and add the following:

class ReleaseBurnDownChartsController < ApplicationController
  after_filter :set_content_type

  def set_content_type
    headers["Content-Type"] = "image/svg+xml"
  end

  def show
    # Ruh-roh!
  end
end

Refresh the page and now we’ve got it!

Now you’re able to request SVGs directly from yours Rails app, and you can even use controller/view code in your sweet new SVG ERB templates.

Step 7 – Embed (or Object) and Profit!

Now what good is our spiffy new SVG if we can’t include it another page, just like an <img>?

I’ve been unable to figure out how to get Firefox 3 (and that’s all I’m working with at the moment) to treat the image/svg+xml MIME type as an actual image when nested among HTML content.  Attempting to display an SVG by using an image tag:

<img src="foo.svg"/>

…nets you a bag of nothing and a blank page.  Firefox requests the image, Rails serves it with an HTTP 200 and the correct MIME type, yet Firefox displays the alt text instead of the rendered SVG image.  Many sites, though some quite old, suggest using either the “embed” or “object” tags, which works just fine.  Sure I’d prefer to use image_tag though you can’t win ‘em all.

<embed src="<%= formatted_project_release_burn_down_chart_path(@current_project, "svg") %>" width="300" height="300"/>

Wait, did you forget that with our spiffy setup comes the ability to use named routes to generate links to our SVG images? :-)

Hope this helps,

Rob