Ruby on Rails: Beautiful Charts Made Easy – using Flash with Rails RXML templates

I’m in the process of developing an application in Ruby on Rails, the main purpose of which is to collect information and output reports. One of the main requirements I was given was that we needed “cool looking reports with charts and stuff”. My job has been reduced to making stuff. Sigh. Moving on…

Having some past experience with reporting and charting, I immediately started cringing at the thought of the overload generating some of these reports place on the server. Creating a chart requires writing out images and either caching them or directly streaming them out to the browser…. which can place an undue strain on the server. To further complicate things, this application is going to be hosted by a regular provider, so I have to make sure that whatever provider I choose supports the image generation library I pick. Now I’m sad again.

Enter Flash Charts.

A friend pointed me over to these XML/SWF Charts on maani.us and said “Why can’t you use that?” Good point, I said. Let me think about it…

  • Using Flash based charts eliminates the need to do any image generation on the server-side.
  • It solves our problem with libraries on shared servers.
  • And doesn’t Google Analytics use Flash based charts? I think we’re onto something here.

Disclaimer: For the purposes of this article, we’re gonna use the charts from maani.us, but you could just as easily use any other chart product. My using them is not an endorsement of them whatsoever, it’s just cause I haven’t found an actual Open Source chart component. They do provide a free feature-limited version of their component. In fact, the Fusion Charts look real good, and they have a free license for Open Source projects.  They also use XML for the data source, so they’d integrate pretty well. On to the article!

 

Installation of the Charts

Installing the charting component is extremely simple, it consists of unzipping the download file into your /pubilc/ directory. Not too bad, eh? Now I’m wondering why I needed a bold title for this section.

Using the Charts in Rails

For the purposes of this article, we are going to pretend we have a Book Store application for our customer. I’m not sure why, but it seems almost identical to the Book Store application in the Agile Rails Development book. The customer is looking for a report on how many of each book has been sold. At this point, they want to know about the total number of each book sold, because they are trying to pick who is going in the dunk tank at this year’s picnic.

Fortunately for us, we’ve already got a table in the database that stores all the line items on all the orders. Our table structure looks like this:

create_table "products", :force => true do |t| t.column "title", :string t.column "description", :text t.column "price", :integer end create_table "line_items", :force => true do |t| t.column "product_id", :integer t.column "order_id", :integer t.column "quantity", :integer end

The first thing we’re gonna do is make a new controller to handle the reports. You don’t have to make a new controller, you could just add a new action to whatever controller you feel like, but I like to keep things seperated in case I want to add more functionality later. After the picnic is over, they’ll probably want some more useful reports, right ?

  

> ruby script/generate controller Reports index summaryreport exists app/controllers/ exists app/helpers/ create app/views/reports exists test/functional/ create app/controllers/reports_controller.rb create test/functional/reports_controller_test.rb create app/helpers/reports_helper.rb create app/views/reports/index.rhtml create app/views/reports/summaryreport.rhtml >

Lets open up the generated reports_controller.rb file and grab the data that we are going to pull out. We’re going to use the new Rails 1.1 ActiveRecord::Calculations functions to get the total number of each book sold, and put it into a hash to use later:  

  

@totalshash = LineItem.sum(:quantity, :group=>:product_id)

You could also use a find_by_sql method to pull the data you want, it’s really up to you. In the background, Rails is going to generate some SQL that works something like this:

  

SELECT product_id, sum(quantity) from line_items group by product_id

If we pop up script/console, we’d see that the @totalshash now contains something that looks like this:

?> @totalshash = LineItem.sum(:quantity, :group=>:product_id) => [[2, 2], [3, 1], [4, 2]] >> ?> @totalshash[3] # Let's check product 3 => 1

Alright, on to the fun part.  We’re going to grab a list of products with Product.find(:all), and move on to creating the xml for our report. The first thing we want to do is rename that summaryreport.rhtml file to summaryreport.rxml, and then copy in the code in the next section. For those of you unfamiliar with RXML templates, they work just like RHTML templates do, except for XML data. You will name them using the same conventions, so if your :action=>”summaryreport”,  then Rails will look for either the summaryreport.rhtml or the summaryreport.rxml file in the  /app/views/controllername/ directory. (or .rjs, but that’s another article)

xml.chart do xml.chart_data do xml.row do xml.null("") xml.string("") end for @product in @products xml.row do xml.string(@product.title) xml.number(@totalshash[@product.id]) end end end end

If we navigate in our browser to /reports/summaryreport/ , we should see XML that resembles this:

<chart> <chart_data> <row> <null/> <string/> </row> <row> <string>Agile Rails Programming</string> <number>182</number> </row> <row> <string>Non-Agile Rails Programming Unleashed!</string> <number>1</number> </row> <row> <string>Another Programming Book</string> <number>3</number> </row> <row> <string>Mountain Dew Rules!</string> <number>15</number> </row> </chart_data> </chart>

At this point in the process, it’s important to read the documentation… Unless you are reading this article, in which case you can just copy in this code to your index.rhtml template:

<OBJECT classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,0,0" WIDTH="400" HEIGHT="250" id="charts" ALIGN=""> <PARAM NAME=movie VALUE="/charts.swf?library_path=/charts_library&xml_source=<%=url_for :action=>"summaryreport",:only_path=>true%>"> <PARAM NAME=quality VALUE=high> <PARAM NAME=bgcolor VALUE=#666666> <EMBED src="/charts.swf?library_path=/charts_library&xml_source=<%=url_for :action=>"summaryreport",:only_path=>true%>" quality=high bgcolor=#666666 WIDTH="400" HEIGHT="250" NAME="charts" ALIGN="" swLiveConnect="true" TYPE="application/x-shockwave-flash" PLUGINSPAGE="http://www.macromedia.com/go/getflashplayer"> </EMBED> </OBJECT>

The key part to take a look at is the two lines that list out the URL. We’re creating the URL dynamically with Rails helper method url_for, and we are passing in the :o nly_path=>true parameter because we only really need the relative path, not the full url from the base of the server. It should work either way. If your app is different, you can pass in the other parameters like :controller.

Now, navigate in your browser to /reports/   You should see something that looks like this:

 

It’s magic! But now we notice something….  the legend is overlapping with the data, and it just doesn’t look quite right. We need to add some options! We check on the reference page and discover that there are a host of options for configuring the display. Let’s add them into our RXML template at the top.

xml.chart do xml.chart_type("bar") xml.legend_label("","layout"=>"vertical","size"=>12) xml.legend_rect("","x"=>12,"width"=>500) xml.chart_rect("","x"=>12,"y"=>100,"height"=>60,"width"=>500)

Now if we navigate to /reports/summaryreport/ in our browser, we’ll notice the XML has a bunch more options. Note that the syntax of the xml.element_name allows you to pass in multiple attributes,  by using the format  “attributename”=>”value”.  If we want to nest items, we’ll put them inside the “xml.element_name do” block.

<chart> <chart_type>bar</chart_type> <legend_label size="12" layout="vertical"/> <legend_rect x="12" width="500"/> <chart_rect x="12" y="100" height="60" width="500"/>

Now our chart is looking quite a bit better!

We now have Flash / XML charts in our Rails app. The next task is to wrap up this functionality into a helper method….  perhaps next article. =)

 

Update: If you are using layout templates, you’ll want to disable them for the summaryreport action. You can do that by putting a line at the top of your controller:

layout "application", :except => "summaryreport"

11 Responses to “Ruby on Rails: Beautiful Charts Made Easy – using Flash with Rails RXML templates”

  1. Magnus Says:

    For added niceness by slicing off an request and also making the graphing fragment search engine friendly in regards for the graphing data (or xslt friendly) you can actually embed the XML as an island in the HTML-document. Pass to flash via ExternalInterface.

    Ted Patrick uses this for Flex Templates, see http://www.onflex.org/ted/2006/08/fxt-flex-templating.php

    Flash/Flex/XML is teh kool :)

  2. Mike Says:

    I’m getting the following error:

    You have a nil object when you didn’t expect it!
    You might have expected an instance of Array.
    The error occurred while evaluating nil.each
    Extracted source (around line #14):

    11: xml.null(“”)
    12: xml.string(“”)
    13: end
    14: for @product in @products
    15: xml.row do
    16: xml.string(@product.title)
    17: xml.number(@totalshash[@product.id])

    I’m using the Agile Bookstore/depot app and building Flash with Rails RXML templates per your example. Any suggestions?
    :D

    Mike

  3. rbaldwin Says:

    See http://ziya.liquidrail.com/ for a Rails plugin wrapper for the XML/SWF charts. Not all chart types are supported yet, but those that are work nicely.

  4. divotdave Says:

    Response to Mike – and anyone else this might help…

    You have to also grab an instance of @products (or whatever model you have) in your controller action, like so:

    def index
    end

    def summaryreport
    @product_totals = Order.sum(:qty, :group => ‘product_id’)
    @products = Product.find(:all)
    end

    This will clear up the nnil error, which is telling you there is nothing in the @products hash.

    Cheers,
    divotdave

  5. David Says:

    This is a great tutorial, and I was able to create some nice bar charts.

    I’d like to be able to create a line graph from data that I pull from my database as:

    [#"3",
    "prequalified"=>"3", "office"=>"Denver, CO", "passed"=>"1",
    "qualified"=>"3"}>, #"1",
    "prequalified"=>"1", "office"=>"McLean, VI", "passed"=>"1",
    "qualified"=>"1"}>, #"1",
    "prequalified"=>"1", "office"=>"Raleigh, NC", "passed"=>"1",
    "qualified"=>"1"}>, #"1",
    "prequalified"=>"1", "office"=>"Troy, MI", "passed"=>"0",
    "qualified"=>"1"}>]

    Office would be on the Y-axis and Interested, Prequalified, Passed, and Qualified would be on the X-axis.

    Any advice on how I would generate the XML from the activerecord?

    Thanks,

    David

  6. David Says:

    Oops, I think some of the characters got stripped from the data, but you should be able to see what I mean.

    David

  7. monk.e.boy Says:

    http://teethgrinder.co.uk/open-flash-chart/

    Similar charts to maani, but open source.

    No need to specify legend sizes in pixels. The chart will figure out the size of each element, then display them correctly.

    monk.e.boy

  8. Chirag Patel Says:

    I get the below error. I’ve tried many solutions with no luck. Why is it looking for a “get” method in charts.swf?

    Thanks!
    Chirag

    ActionController::RoutingError (no route found to match “/sandbox/charts.swf” with {:method=>:get})

  9. Stéphane Tavera Says:

    Thank you for your excellent tutorial !
    I was able to produce charts in a short time ;-)

  10. Terry Says:

    I’m having trouble moving from a standalone reports controller, to an existing non-reports controller. And I do use layouts.

    In my scenario, the definition you have in your index.rhtml would be in a partial for a view.

    Can you describe for all of us how using a non-reports controller would be constructed?

  11. PullMonkey Says:

    Rails version of monk.e.boy’s open flash chart available here – http://pullmonkey.com/projects/open_flash_chart

    Enjoy :)