Ziya Charts for Rails

Posted by chris olsen on September 15, 2007

We are an information driven society and nothing puts a damper on things like having some statistics displayed for you to analyze in a tabular format. Don’t get me wrong, numbers are good, but for the most part people want to be able to look at the data and gain instant knowledge to what the numbers mean. This is obviously not a new concept, but often when creating an application if displaying information in a graphical format it may be crossed off the todo list.

There are a few different options out there for creating charts with Rails, but Ziya is on the top of my list. The displayed output has a professional feel and making things work can be done in only a couple lines.

To integrate Ziya charts into your project you will have to obtain the latest version from their repository.

ruby script/plugin install svn://rubyforge.org/var/svn/liquidrail/plugins/ziya/trunk

This will add all the required files to your project’s vendor folder.

It is now time to create our controller for the model that we wish to graph the data for.

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
27
28
29
class TrendItemsController < ApplicationController
  include Ziya
 
  before_filter :init
 
  def graph
    graph = Ziya::Charts::Line.new( nil, nil, "custom_bar" )
 
    # obtain the data we wish to show and insert into two corresponding arrays
    dates = @trend.trend_items.map {|item| item.taken_on}
    values = @trend.trend_items.map {|item| item.value}
 
    # custom method that 'massages' the data a bit.  I will talk about this in a bit
    graph_data = fill_empty_values(dates, values)
 
    # bind the data to the chart
    graph.add :axis_category_text, graph_data[0].map { |item| item.to_s(:short)}
    graph.add :series, "Weights (lbs.)", graph_data[1]
 
    # send the data back the browser in an xml format
    render :xml => graph.to_s
  end
 
  private
 
  def init
    @trend = Trend.find(params[:trend_id])
  end
end

Before anything we have to make the methods provided to use in the Ziya plugin available within the controller which is done by the include statement on line 2.

The next thing that has to be done is create a method that will be called via ajax to return the data in an xml format. If you are using REST this means you will also have to map the method to make it available in the routes.rb file.

1
2
3
4
5
6
ActionController::Routing::Routes.draw do |map|
  map.resources :trends do |trend|
    trend.resources :trend_items, :collection => {:report => :get, :open_report => :get, :graph => :get}
  end
  ....
end

The example above is what I used since I have an additional TrendsController for the trend model that has a “has many” relation to the trend_item model, so you will have to make the necessary adjustments to fit your model relations.

The last thing that has to be done is add the html embedded code required to make the chart appear. To do this I placed the following in the trend_items/index.rhtml file.

<%= ziya_chart(graph_trend_items_path(@trend), {:bgcolor => "#666666", :height => 250, :width => 400}) %>

It is a pretty simple method call where the first argument is the RESTful url that will call on the graph method in the TrendItemsController. You can also pass it an hash array of options that will be inserted into html <object> tag that is returned on the ajax call.

graph = Ziya::Charts::Line.new( nil, nil, "custom_bar" )

In the first line of code in the graph function there is a value of “custom_bar” that is passed to the Ziya:Line when created. This is an optional parameter and, when passed in, allows us to customize the appearance of the chart. The value represents a custom .yml file located in the public folder of our project, in this case with the filename custom_bar.yml. If you want to put the file into a subfolder within the public folder you can and will just have to set the value passed in the Line object creation to “the_folder/custom_bar”.

Below is what I put together in somewhat replicating an example that I saw on the XML/SWF Charts site. It may look a little cryptic, but it will make better sense if you visit the reference section at the XML/SWF Charts site.

#Overriden bar chart styles
<%=chart :Line %>
 
  <%= component :chart_pref %>
    line_thickness:   2
    fill_shape:       false
    point_shape:      circle
 
  <%=component :chart_transition %>
    type:             dissolve
    duration:         0.5
 
  <%=component :chart_value %>
    alpha:            60
    position:         cursor
 
  # Change y axis thickness
  <%=component :chart_border%>
    left_thickness:   2
    right_thickness:  2
    bottom_thickness: 2
    top_thickness:    2
    color:            333333
 
  <%=component :chart_grid_v %>
    thickness:        0
 
  # Change x axis label colors
  <%=component :axis_value%>
    color:          cccccc
    alpha:          80
    min:            150
    max:            350
 
  <%=component :axis_ticks %>
    minor_color:    333333
    major_color:    333333
 
  # Change y axis label colors
  <%=component :axis_category%>
    orientation:    horizontal
    color:          cccccc
    alpha:          80
    orientation:    diagonal_down
    skip:           1
 
  # Change legend rectangle
  <%=component :legend_rect%>
    x:              40
    fill_color:     666666
 
  <%=component :legend_label %>
    color:          ffffff
 
  <%=component :series_color %>
    colors: FF6600, FFCC00, FF9900
 
  # Change chart rectangle
  <%=component :chart_rect%>
    negative_color: c0b15c
    positive_color: 333333
    negative_alpha: 30
    x:              40
    y:              25
    height:         160
    width:          310    
 
  # Add a chart title
  <%=component :draw%>   
    components:         
      - <%=drawing :text%>
        transition: slide_down
        delay:      0
        duration:   0.5
        bold:       true
        rotation:   270
        color:      ffffff
        alpha:      20
        size:       25
        x:          0
        y:          230
        text:       Weight
      - <%=drawing :text%>
        transition: slide_left
        delay:      0.5
        duration:   0.8
        bold:       true
        rotation:   0
        color:      ffffff
        alpha:      20
        size:       25
        x:          50
        y:          20
        text:       Months
    # custom method that 'massages' the data a bit.  I will talk about this in a bit
    graph_data = fill_empty_values(dates, values, true)

In one of the comments in the controller there was some custom code that, as I put, massaged, the data. What I meant by that was often when plotting data values wrt time there are gaps in the frequency that that values were obtaine. When data exists containing these gaps, problems arise when the data is displayed as the distance between each point displayed in the graph is equal, and not properly representing the actual length of time, this can be seen in Figure 1 and 2. As I mentioned earlier, people like to be able to get a feel for the data by quickly glancing at the charts, and in this case could give people false impressions. As you may have guessed, the fill_empty_values method call fixes this problem by simply inserting filling time points into the dates and values array.

Figure 1

Picture 1

Figure 2

Picture 2

The last parameter, which by default is true, defines where to insert additional points where there were previously none, which is shown be the additional points in Figure 2 in comparison to Figure 1. The reason for doing this is that when creating a line graph, lines will only be created for consecutive points. This means that if you are missing a point a gap will appear in the chart. Passing the value true, or leaving it blank, will create 1..n points to allow the line to be properly rendered.

To make this method available we will have to add an additional include to the controller class.

class TrendItemsController < ApplicationController
  include Ziya, ZiyaHelper

And finally we will have to create the ZiyaHelper class in the helper folder containing the following code.

module ZiyaHelper
 
  # Inserts values into the two arrays to prevent inaccurate scaling of
  # values lying on the x-axis.  
  def fill_empty_values(time_arr, val_arr, insert_averages = true)  
 
    time_arr.sort!
    time_values = fill_timespan(time_arr)
    chart_values = match_values(time_values, time_arr, val_arr)
 
    if insert_averages : chart_values = insert_average_values(time_values, chart_values) end
 
    return [time_values, chart_values]
  end
 
  private
 
  def insert_average_values(time_values, chart_values)
    #insert avg values for current nil values
    previous_point = nil
 
    chart_values.each_with_index do |val, index|
      # does the current point require a calculated value
      if val.nil?        
        x2, y2 = calculate_point(chart_values, previous_point, time_values, index)
        #set the missing value
        chart_values[index] = y2
        #the new point is set to the previous point for the next loop
        previous_point = x2, y2
      else
        previous_point = time_values[index], val
      end
    end
 
    return chart_values
 
  end
 
  # Returns the point that lies on the slope between the previous_point and the 
  # next point in the chart.
  # This prevents breaks in the line running through the points in line type charts
  def calculate_point(chart_values, previous_point, time_values, previous_index)
 
    #find the next value that is not nil
    chart_values.each_with_index do |next_set_val, next_set_index|
      # have we found the next value -> x2, y2
      if next_set_val.nil? == false and next_set_index > previous_index
        x2 = time_values[next_set_index]
        y2 = next_set_val
 
        x1 = previous_point[0]
        y1 = previous_point[1]
 
        # slope
        m = (y2 - y1) / (x2 - x1)
 
        return time_values[previous_index], m.to_i + y1  # mx + b ~> m(1) + y1 seeing as x is one point ahead of the previous point
      end
    end
  end
 
  # creates an array filled with dates from the beginning to the end of the date_arr passed in.
  # This allows the graphing module to correctly scale the data based on when it was obtained
  def fill_timespan(date_arr)
 
    #determine the start and end date
    start_date = date_arr[0]
    end_date = date_arr[-1]
 
    #create an array with all the dates between the start and end
    timespan_arr = []
    temp_date = start_date
    #for each day
    while temp_date.jd <= end_date.jd
      timespan_arr << temp_date
      temp_date += 1  #add a day to the temp_date
    end
 
    return timespan_arr
  end
 
  # Creates an array that matches the times[] array in the same way
  # that the orig_times[] and values[] matched
  def match_values(times, orig_times, values)
    chart_values = []
    times.each do |t|
      #determine if the date exists in the data
      index = orig_times.index(t)
      unless index.nil?
        #obtain the corresponding value
        chart_values << values[index]
      else
        chart_values << nil
      end
    end
    chart_values
  end
 
end

There are other options out there, such as Gruff Graphs, that are also available for rails that you may want to check out as well. The good thing about the Gruff Graphs is that they are free, where as the XML/SWF graphs kinda require a licensing fee. The reason I say kinda is that without a license they work fine, but if a person clicks on a graph they will directed to the XML/SWF site. The good thing is that their licensing fees are not outrageous, and at the time of writing they were going for $45 US for a single application or $550 for unlimited usage. The current prices can be found here.

That wraps everything up. I hope this helps a few of you and feedback is always appreciated.

Trackbacks

Trackbacks are closed.

Comments

Leave a response

Comments