Google Charts is an awesome platform to create great-looking interactive charts, from simple scatter plots to timelines or treemaps.Google Charts Piechart

I needed a way to create the required data for a chart on the server-side, instead of in-lining it in the JavaScript itself — as seen in many of the examples. 

Why asynchronously?

Although in the example below we’re using a small hard-coded dataset, for which there’s no real need to have this done asynchronously, this might not always be the case in the real world. Loading in data asynchronously improves the user experience especially if calculating or getting the data for the chart might take significant time. E.g. the page containing the charts is displayed quickly in the browser — while the actual graphs are rendered when the data comes in.

Sometimes you might even have multiple charts on a page, which leans even more to a situation where the page response time can become too lengthy if we have to wait on all graph data to be fetched or computed first before rendering the page.

So I took the PHP example and re-created it in a Grails way. I’m also taking care of exceptional situations and logging errors on the client side. This is just an example which works for me – there might be other approaches.

The HTML

For simplicity’s sake I just took the example code quite literally and put it in the head section of a Grails GSP page.

For those wondering whether or not they can download the JS files and have it processed with the Asset Pipeline plugin. The answer is: no. Per the FAQ:

Sorry; our terms of service do not allow you to download and save or host the google.load or google.visualization code.

So we have to include the jsapi JavaScript in the head – which you can do on your specific chart page (as seen below) or in your main.gsp layout. If your Grails application by default is still shipped with jQuery as a dependency, then you don’t need to include it explicitly on your page ofcourse – but I have left the jquery.min.js line below intact for now.

<%@ page contentType="text/html;charset=UTF-8" %>
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <meta name="layout" content="main" />
  <script type="text/javascript" src="https://www.google.com/jsapi"></script>
  <script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
  <script>

    // Load the Visualization API and the piechart package.
    google.load('visualization', '1.1', {'packages':['corechart']});

    // Set a callback to run when the Google Visualization API is loaded.
    google.setOnLoadCallback(drawChart);

    function drawChart() {
      var jsonData = $.ajax({
        url: "${createLink(controller:'chart', action:'toppings')}",
        dataType: "json",
        async: false
      }).responseText;

      // Create our data table out of JSON data loaded from server.
      var data = new google.visualization.DataTable(jsonData);

      // Instantiate and draw our chart, passing in some options.
      var chart = new google.visualization.PieChart(document.getElementById('chart_div'));
      chart.draw(data, {width: 400, height: 240});
    }
  </script>
</head>
<body>


<div id="chart_div"></div>


    
</body>
</html>

The $.ajax statement is jQuery’s way of performing an asynchronous HTTP (Ajax) request. In the url I’m creating a link – with Grails’ createLink-tag – to the ChartController and toppings action responsible for giving me the JSON chart data.

Initially you won’t see a thing when the page is rendered. However, as soon as the <script> block is encountered, the browser interprets it, loads the Google libraries which invokes our callback function drawChart. This callback has jQuery make a new request to fetch the chart data. The response of the request is loaded into a variable jsonData which is passed to the constructor of Google’s own DataTable JS class.

The chart is drawn and the HTML div – with id chart_id – is updated with a chart.

The JSON

The JSON is the result of calling controller action below:

import grails.converters.JSON

class ChartController {

  def toppings() {

    def cols = [
      [label: "Topping", type:"string"],
      [label: "Slices", type:"number"]
    ]

    def rows = [
      [c: [[v: "Mushrooms"], [v:3]]],
      [c: [[v: "Onions"], [v:1]]],
      [c: [[v: "Olives"], [v:1]]],
      [c: [[v: "Zucchini"], [v:1]]],
      [c: [[v: "Pepperoni"], [v:2]]]
    ]

    def data = [cols: cols, rows: rows]
    render data as JSON
  }
}

It’s all Maps in Maps in Lists etc. in Groovy syntax. If you’re wondering what these “c” and “v” values mean; they’re the essential properties for defining an array of cells in that row and defining the value for that cell. There are more parameters, which you can use to tweak formatting and more.

If you’ve got the nesting right, just using Grails’ grails.converters.JSON will do the trick to get this structure rendered as valid JSON, something our Google Chart JavaScript understands very well.

By accessing your url directly in the browser on e.g.

http://localhost:8080/yourapp/chart/toppings

you can inspect the raw JSON:

{"cols":[{"label":"Topping","type":"string"},{"label":"Slices","type":"number"}],"rows":[{"c":[{"v":"Mushrooms"},{"v":3}]},{"c":[{"v":"Onions"},{"v":1}]},{"c":[{"v":"Olives"},{"v":1}]},{"c":[{"v":"Zucchini"},{"v":1}]},{"c":[{"v":"Pepperoni"},{"v":2}]}]}

You could create some helper methods to make creating this data somewhat easier. E.g. to remove the repetition, you could use a Groovy closure like this:

def rows = []
def addRow = { name, value ->			
	rows << [c: [[v: name], [v: value]]]
}

addRow("Mushrooms", 3)
addRow("Onions", 1)
addRow("Olives", 1)
addRow("Zucchini", 1)
addRow("Pepperoni", 2)

Upon accessing the page I successfully see my pie chart.Google Charts Piechart

Not done yet

However, I’m also getting a JavaScript warning:

Synchronous XMLHttpRequest on the main thread is deprecated because of its detrimental effects to the end user’s experience. For more help, check http://xhr.spec.whatwg.org/.

Seems the example I took still had async: false in place. Overlooked it. Just set async explicitly to true (or leave it out – since it defaults to true) to make it do its call asynchronously.

var jsonData = $.ajax({
  url: "${createLink(controller:'chart', action:'toppings')}",
  dataType: "json",
  async: true
}).responseText;

// Create our data table out of JSON data loaded from server.
var data = new google.visualization.DataTable(jsonData);

Unfortunately, this causes the AJAX call not to block any more and immediately pass jsonData to the DataTable constructor – even before any data actually came back from the controller. At this point jsonData is undefined and the Google Chart fails with an error “Table has no columns”.

google-charts-failure-table-has-no-columns

To properly fix this in jQuery the documentation on jquery.ajax suggests to use the promises of the jqXHR object. We have a few, such as done – a callback which is fired when the operation successfully completed.

If we’re in the callback, we know we have the JSON data. Here’s after the changes:

function drawChart() {
  $.ajax({
    url: "${createLink(controller:'chart', action:'toppings')}",
    dataType: "json"
  }).done(function(jsonData) {
 
    // Create our data table out of JSON data loaded from server.
    var data = new google.visualization.DataTable(jsonData);

    // Instantiate and draw our chart, passing in some options.
    var chart = new google.visualization.PieChart(document.getElementById('chart_div'));
    chart.draw(data, {width: 400, height: 240});
  });
}

Great!

Exceptional situations

But where not there yet. What if the calculation of the chart data fails due to some reason? E.g. ChartController fails at run-time? Right now, in this case we’re getting an HTTP 500 (“Internal Server Error”) for that asynchronous request and consequently NO chart is drawn – just an empty space on the page. In these exceptional situations we could at least inform the user where his chart has gone to provide a better user experience.

We can use the fail promise for this. It’s a special method called when the request is “rejected”. It gets passed a few parameters, such as jqXHR, textStatus and errorThrown, but there’s nothing really interesting to get from these right now when an Internal Server Error occurs. The reason of failure can be anything.

So, things could be as simple as displaying an alert.

}).done(function(jsonData) {
  ...
}).fail(function() {
  alert('Failed to load data for the chart.');
});

This is not really a “better user experience” of course 🙂 We need a way to leverage the existing error reporting, such as the earlier error when the chart failed with “Table has no columns”. The Google Chart API docs describe several functions to help you display error messages to users.

It seems there are some static function in the google.visualization.errors namespace we can leverage, such as addError. This function accepts a minimum amount of parameters: the container, our chart DOM element, and a message, so it can display an error block in the visualization.

Let’s put it to use:

$.ajax({
  url: "${createLink(controller:'chart', action:'toppings')}",
  dataType: "json"
}).done(function(jsonData) {
					  
  // Create our data table out of JSON data loaded from server.
  var data = new google.visualization.DataTable(jsonData);

  // Instantiate and draw our chart, passing in some options.
  var chart = new google.visualization.PieChart(chartDiv);
  chart.draw(data, {width: 400, height: 240});

}).fail(function() {
  google.visualization.errors.addError(chartDiv, 
    "Failed to load data for the chart.");
});

I’ve extracted the chart div element to a higher-level to be able to re-use it in both done() and fail() promises.

The error would now be displayed where the chart would be:

google-charts-failure-failed-to-load-data-for-the-chart

Development tip

You can optionally use the google.visualization.dataTableToCsv helper function to display the loaded data for development purposes in the browser’s console.

...
chart.draw(data, options);

<g:if env="development">
// Debug the output to the browser's console
if (console && console.log) {
 var csv = google.visualization.dataTableToCsv(data);
 console.log(csv);
}
</g:if>

That’s it. Happy charting!

Advertisements