Blogger.create { :name =>'Matt Aimonetti',
:location => 'San Diego, Ca',
:email => mattaimonetti AT gmail.com,
:linkedin => Matt's Linkedin page,
:recommend_me => HERE,
:contractor => true}

Sexy charts in less than 5 minutes

Written by matt on October 3rd, 2007

NOV 04 Update: demo app now available there. Sexy charts BDD style presentation at the SDRuby group to be posted soon on video podcast

Last time, in our 'do it in less than 5 minutes' series, we saw how to add quickly and simply add Ajax pagination.

This time we'll see how to add some sexy/fancy charts to your rails app.

The goal is to end up with something like:

chart

charts2

Various options

You might have heard or even tried solution such as Gruff or JFreeChart.

While these solutions are great, they are certainly a pain in the butt. Gruff requires RMagick (avoid RMagick as much as can) and creates static files (a real pain when your graphs change all the time) JFreeChart on the other hand requires Java, Java skills and I hate the way you create graphs:

1
2
3
4
5
  def CreateChart
         pipe = IO.popen "java -cp C:\\InstantRails\\rails_apps\\project\\jfree\\src;C:\\InstantRails\\rails_apps\\project\\jfree\\lib\\jcommon-1.0.0-rc1.jar;C:\\InstantRails\\rails_apps\\project\\jfree\\lib\\jfreechart-1.0.0-rc1.jar; CreateChart" 
         pipe.close
         redirect_to "/graph/report" 
      end

Anyway, none of these solutions would let us create our charts in less than 5 minutes so let's cut the story short. The best solution IMHO is to use Flash. But wait, you don't need to know ActionScript or to own a license of Flash or Flex, we have libraries available for us to use without any Flash knowledge :)

XML/SWF is cool Flash library which should fulfill our needs, you can even find a rails plugin to make things easier.

amCharts

But, to be honest I'd like to have something a bit "cleaner/sexy/fancy" and easier to setup. So we're going to use amCharts Don't get me wrong, XML/SWF is a great library and you can make your graphs look nice (but you have to pay for support). Since we are running out of time let's see how to implement a nice graph using *my* favorite library.

amcharts

[DISCLAIMER: amCharts is NOT open source and NOT free. But, it's cheap (85 euros per site) especially when you think of how much time you will save. AND there is a FREE version. The Free version is the same as the full version but with a link back to amcharts.com]

Setup

Let's go ahead and download one of the package: http://www.amcharts.com/column/download/ for instance.

Unpack the files and put them in their own folder in your public folder. Make sure you have the .swf file (amcolumn.swf for instance), a XML settings file and the fonts folder. (You might want to also create an empty amcharts_key.txt in the same folder since the plugin tries to load the key and you don't want to pollute your logs.)

Usage

Now you need to understand how amCharts works.

After being loaded, amCharts expects a datastream. The datastream is then parsed and displayed as a chart. You can modify the aspect of any chart by changing its settings. Settings are set at runtime and/or in a setting file.

Great! I won't cover the settings file. It's a well documented XML file you just copied in your public folder. (or check the documentation)

What we want to focus on, is the datastream. Basically we just need to create a XML file that can be parsed by amCharts.

Let's imagine that we have a reports_controller.rb file We want to display the population of the cities in California.

let's add a new action to render our XML file:

1
2
3
4
5
6
7
8
  def population
    @cities = City.find(:all)
    @population_data_link = formatted_population_reports_url(:xml)
    respond_to do |format|
      format.html
      format.xml  { render :action => "population.xml.builder", :layout => false }
    end
  end

(notice that I'm using rails 2.0 and that's why my XML template is not RXML)

As you can see, we have 2 values: @cities and @populationdatalink

@cities contains all the City records, including their population etc..

@populationdatalink contains the url to retrieve the datastream.

If you wonder how I got this url? I'm simply using a named route defined in my routes.rb:


  map.resources :reports, :collection => {:population => :get}

(note that you don't need to create a restful route for that, a simple named route would have worked too)

Flash detection

Since we are going to use Flash, we want to make sure that people have the Flash plugin installed on their browser. For that we will use swfobject. Simply make sure to add swfobject.js (available in any amChart package) to your public/javascript folder. Then make sure you linked the javascript in your header:


  <%= javascript_include_tag 'swfobject' %>

We now need to create our 2 views: population.html.erb and population.xml.builder

population.html.erb

Basically, this view only loads amCharts and provides it with the details of the datastream:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  <div id="population_chart" class='chart'>
    <strong>Text displayed when the user doesn't have Flash. You might want to display a simple table with the population, search engines and visitor without flash would love that.</strong>
    <p> To see this page properly, you need to upgrade your Flash Player, please visit the Adobe web site</p>
  </div>

  <script type="text/javascript">
    // <![CDATA[    
    var so = new SWFObject("/amcolumn/amcolumn.swf", "population_chart", "800", "380", "8", "#000000");
    so.addVariable("path", "/amcolumn/");
    so.addVariable("settings_file", escape("/amcolumn/column_settings.xml"));
    so.addVariable("data_file", escape("<%= @population_data_link %>"));
    so.addVariable("additional_chart_settings", "<settings><labels><label><x>250</x><y>25</y><text_size>18</text_size><text><![CDATA[<b>California Population as of <%= Time.now.to_s(:db) %></b>]]></text></label></labels></settings>");
    so.addVariable("preloader_color", "#000000");
    so.write("population_chart");
    // ]]>
  </script>

As you can see, we have a div called population_chart. This div is replaced at load time by the Flash object if the visitor has Flash setup locally. Think about providing some data in case the user doesn't have Flash.

The rest is simple Javascript. I unpacked the amchart column lib in mypublic/amcolumn folder and that's why I setup the path as "amcolumn"


  so.addVariable("path", "/amcolumn/");

My settings file is called column_settings.xml :


  so.addVariable("settings_file", escape("/amcolumn/column_settings.xml"));

and the most important part:


  so.addVariable("data_file", escape("<%= @population_data_link %>"));

Finally, I added some dynamic settings just to show you how easy it is:

1
2
  so.addVariable("additional_chart_settings",
  "<settings><labels><label><x>250</x><y>25</y><text_size>18</text_size><text><![CDATA[<b>California Population as of <%= Time.now.to_s(:db) %></b>]]></text></label></labels></settings>");

Ok, let's now create our XML view:

population.xml.builder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
  xml.instruct! :xml, :version=>"1.0", :encoding=>"UTF-8"
  xml.chart do
    # xml.message "You can broadcast any message to chart from data XML file", :bg_color => "#FFFFFF", :text_color => "#000000"
    xml.series do    
      @cities.each_with_index do |city, index|
        xml.value city.name, :xid => index
      end
    end

    xml.graphs do
     #the gid is used in the settings file to set different settings just for this graph
      xml.graph :gid => 'population' do
        @cities.each_with_index do |city, index|
          population = city.population
          case population
            # When the population is > 1 million, show the bar in red/pink
            when > 100000
              xml.value value, :xid => index, :color => "#ff43a8", :gradient_fill_colors => "#960040,#ff43a8", :description => level
            else
              xml.value value, :xid => index, :color => "#00C3C6", :gradient_fill_colors => "#009c9d,#00C3C6", :description => level
            end
        end
      end
    end

  end

Nothing fancy, we first created a series with all the city names:

1
2
3
4
5
  xml.series do    
    @cities.each_with_index do |city, index|
      xml.value city.name, :xid => index
    end
  end

Then we created another node with the values for each city. Since it would be cool to display some bars in a different color, we used a case-switch statement:

1
2
3
4
5
6
7
8
9
10
11
12
    xml.graph :gid => 'population' do
      @cities.each_with_index do |city, index|
        population = city.population
        case population
          # When the population is > 1 million, show the bar in red/pink
          when > 100000
            xml.value value, :xid => index, :color => "#ff43a8", :gradient_fill_colors => "#960040,#ff43a8", :description => level
          else
            xml.value value, :xid => index, :color => "#00C3C6", :gradient_fill_colors => "#009c9d,#00C3C6", :description => level
          end
      end
    end

Depending on what you want to display, you might need to have different colors or a different tooltip text, or load an animation or image... and as you can see, it's REALLY easy.

Got to http://yoursite.com/reports/population to enjoy your new fancy graph.

That's it, you are done!

Time to tweak your settings file to make your graph look awesome. Since you now have a lot of free time, you can start re-factoring your code and make sure you have a good test coverage.

Good luck!



Comments

  • subcorpus on 04 Oct 20:51

    they are sexy ... thats for sure ...

  • David Gurba on 05 Oct 08:22

    though not *my* favorite library my co-workers have used http://www.fusioncharts.com/ instead? Same setup, similar xml, similar price (1 website for a single developer license is $69 USD. Nearly similar output.

  • Ravi Kumar on 05 Oct 17:37

    To make it totally free, you can use FusionCharts Free from http://www.fusioncharts.com/free - it doesn't come with the imprints or link backs on charts.

  • Uros Jurglic on 08 Oct 08:14

    Thanks for advice, I've integrated it in my site and I am very happy with it. Great component for sure, in most cases much better than jgruff or ziya.

  • Fernand on 10 Oct 08:18

    Nice article. I do believe XML/SWF Charts framework actually supports more chart types and packs more features. It think you have misrepresented the price. XML/SWF Charts is free for basic usage and $45 will get you a license and unlimited support. With ZiYa you can pretty much render the same chart as

    chart = Ziya::Charts::Column.new chart.add( :axiscategorytext, cities.map( &:name ) ) chart.add( :series, "Population", cities.map( &:population ) ) respond_to do |fmt| fmt.xml {chart.to_xml} end

    Seems sexy/clean and fancy to me...But then again I might be a bit biased ;-)

  • jamiew on 12 Oct 16:43

    Following along I had mild compile errors with undeclared vars/methods "level" and "value" in the xml.builder.

    Thanks for a great article, my graphs are looking slick.

  • Tom on 14 Oct 23:25

    I just switched from Ziya with XML/SWF to FusionCharts.

    XML/SWF sucks in IE. Sucks bad

    The configuration for FusionCharts is a lot easier to work with than the confusing terminology that XML/SWF uses. Fusion renders faster than XML/SWF too.

    If Fusion ends up failing us in some major way, amCharts will be my next stop.

  • Brian Terlson on 15 Oct 09:10

    Something to keep in mind about FusionCharts free is that it cannot be used for commercial sites, according to its license. The free version of AmCharts simply has a link back to their website when you use an unlicensed version, so I would say that's a win for AmCharts on cost - assuming you can deal w/ the link.

    As for XML/SWF vs AmCharts vs FusionCharts, I had major problems with XML/SWF and printing in IE and FF, while the other two behaved much better. Having used both AmCharts and FusionCharts with rails, I think AmCharts is a bit better as far usage is concerned, but the biggest problem with AmCharts is the complete lack of solid documentation. At least that I could find.

  • Jack Danger on 15 Oct 09:31

    I've come to appreciate Plotr (http://www.solutoire.com/plotr/) - a charting library entirely in javascript based on Prototype.

    I'm using it to push a whole lot of calculations onto the client and it seems to be working phenomenally well. Plus it's easily extensible with custom javascript.

  • Reiner Balt on 15 Oct 10:50

    For Tracks, a RoR based GTD webapp, we're using Open Flash Chart (http://teethgrinder.co.uk/open-flash-chart/)

    This is a complete open source flash solution for creating charts. Very nice. Needs better ruby support though

  • Nakul on 16 Oct 22:31

    I have been using amcharts(amline to be specific) for sometime now (free version though) and I must say it rocks, The number of options it provides to customize the graph are awesome.

    One think I would like to mention is you don't need to build xml format data all the time, It also supports CSV format. XML is only useful when

    you want to highlight some region of graph (using events) or

    display some graph points using custom bullets.

    Also, creating empty file "amcharts_key.txt" in amline folder is not working for me. It still sends a request for it as a action in current controller :(

  • Nakul on 16 Oct 22:36

    sorry for breaking the format, didn't meant to do that. I Just used ordered list using textile markup

  • Matthias on 19 Oct 07:51

    Hello,

    Sorry, i try this exemple but i have some error and i need more explications.

    First : where goes the "population.xml.builder" and "population.html.erb" file??? in app/view???

    I have this error : NameError in ReportsController#population

                              uninitialized constant ReportsController::City
    

    Could you give me more info to how install this excellent application

    thanks

  • Graham on 23 Oct 12:03

    Thanks so much. I'm having a hard time figuring out where all of these files should go. Any chance you can post the skeleton rails app for this tutorial?

  • Matt Aimonetti on 29 Oct 18:05

    @graham I have a presentation scheduled for the next SDRuby group meeting (Nov 1).

    I'll release the skeleton app at the same time as the presentation video.

  • Graham on 30 Oct 16:06

    Great, thanks Matt!

    As an aside our organization might want help jump-starting an effort to monitor our energy production (solar, wind, etc) and noticed up top that you're available for hire. Could you email me when you get a chance to discuss? Thanks!

  • vvatever on 05 Nov 02:15

    Thanks Matt!!

    Two small corrections (?); for the example to work, I had to do the following:

    1) rename population.html.erb to population.rhtml 2) rename population.xml.builder to population.rxml and call population.rxml from my controller instead of population.xml.builder

    there's still a fair bit of tweaking required to get good results.. but thanks a lot for this post, very useful!

    Regards vvatever

  • Matt Aimonetti on 05 Nov 19:52

    @vvatever

    As mentioned in the article, I'm using Rails 2.0 hence the usage of .html.erb and xml.builder I added a link to the demo app so feel free to check it out. (Rails 2.0 Edge is frozen)

  • Morten on 15 Nov 13:28

    The one problem with a flash based solution is that it's hardly printable (hard copy).

  • Matt Aimonetti on 15 Nov 16:31

    @morten amcharts has a right click option to save the graph as an image :)

  • Pallav on 02 Jan 18:32

    Brian,

    FusionCharts Free v2 (http://www.fusioncharts.com/free) was launched recently with completely free licensing model. Also, FusionCharts v3.0.5 supports save as image in right click too.

    Thanks, Pallav FusionCharts Team

  • Aye on 14 Feb 17:52

    Matt, Thanks for the tip. I would never have thought of using flash for charts before and this will be great for prototype demos to wow customers :)

  • Yasser K. on 27 Feb 07:57

    I'm still not understanding what doe this line do: map.resources :reports, :collection => {:population => :get}.

    If I chose to create a graph of stock values under a controller name "stock", does this mean that both the html.erb and xml.builder both have to be in the view that was made from stock?

    What are the lines in the .html.erb file meant to do?

    Sorry for asking, but I'm a student and I'm learning to use the graph generator.

  • Arthur Martins on 04 Apr 08:19

    I am using Rails 1.2, you can use the AmmCharts this version of Rails? Could you tell me what change in its code of example?

  • Alain on 30 Apr 09:37

    Thanks for the post, the promises look great. I do have an error at the last steps of displaying the graph as it seems that the amcharts javascript is started and displayed. The black background is done, and I have the following error in a yellow box : error loading file http://localhost:3000/reports/population.xml It seams that I was not able to generate of feed the amcolumn javascript with the datas. Any hints ?

  • Alain on 07 May 01:35

    The problem was coming from the compile error in the xml.builder as reported by jamiew. Can you update your code as I do not guess whit what to replace label. By the way I am a duplo addict ;-)

Post a comment