Asynchronous Google Chart Data With jQuery in Grails
Google Charts is an awesome platform to create great-looking interactive charts, from simple scatter plots to timelines or treemaps.
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 = [ , [v:3]]], , [v:1]]], , [v:1]]], , [v:1]]], , [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.
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 << , [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.
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, checkhttp://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 passjsonData
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”.
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()
andfail()
promises.
The error would now be displayed where the chart would be:
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!
Reference: | Asynchronous Google Chart Data With jQuery in Grails from our WCG partner Ted Vinke at the Ted Vinke’s Blog blog. |