Aug 24, 2012

Integration Time Series Visualization Cubism.js in SmartGWT

This article demonstrates the integration JS library Cubism.js in SmartGWT application. Provides examples of configuration and guidelines for the project preparation to integration.
I assume that you are familiar with SmartGWT, so I will not describe this framework. It is better to focus on the task - integrate graphics library visualization to SmartGWT.
Cubism.js it is JavaScript Library with realtime time series visualization support. It based on the D3 library. Cubism.js library supports two types of charts: horizon and comparison.

So, let's start.

First of all, we need to ensure the availability of library from GWT hosted page. If this is not done, then JS debugger will constantly says "the library not found" and therefore nothing will work.

Loading JS libraries into the project

  1. Download libraries from github cubism.v1.js, d3.v2.js and place its into webapp directory of the yours project;
  2. Add two lines to the file of your project_name.gwt.xml:
    
    <script src="d3.v2.js"></script>
    <script src="cubism.v1.js"></script>
    
  3. Add two lines to the file of your project
    
    <script type="text/javascript" language="javascript"
                src="d3.v2.js"></script>
    <script type="text/javascript" language="javascript"
                src="cubism.v1.js"></script>
    
  4. Start your Application server and verify that the files are available at URL.

Write a JSNI method


Now that connected all the JS libraries, we can begin programming JSNI. The code for the JSNI method:

public native void drawCharts() /*-{
    var context = $wnd.cubism.context()        
        .serverDelay(this.@full.class.Name::serverDelay) 
        .clientDelay(this.@full.class.Name::clientDelay)
        .step(this.@full.class.Name::step)
        .size(this.full.class.Name::width);

    var chartDiv = "#" + this.@full.class.Name::chartId;
    var jsonMetrics = eval(this.@full.class.Name::getMetrics()());

    var metrics = [];
    var self = this;

    function createMetric(name, metricIndex)
    {
        var metric = context.metric(function (start, stop, step, callback)
        {

            var jsonPoints = self.@full.class.Name::getPoints(IDD)(metricIndex,
                    start.getTime(), stop.getTime());
            try
            {
                var json = jsonPoints ? eval("tmp=" + jsonPoints) : null;
            }
            catch (e)
            {
                alert(e + jsonPoints);
            }
            callback(null, json);
        }, name);
        metrics.push(metric);
        return metric;
    }

    var selection = $wnd.d3.select(chartDiv)
            .call(function (div)
            {
                div.append("div")
                        .attr("class", "axis")
                        .call(context.axis().orient("top"));
                div.append("div")
                        .attr("class", "rule")
                        .call(context.rule());
            });
    for (var i = 0; i < jsonMetrics.length; i++)
    {
        var jsonMetric = jsonMetrics[i];
        var metricI = createMetric(jsonMetric.label, jsonMetric.metricIndex);
        var horizonI;

        if (jsonMetric.metricUnit != 'PERCENTAGE')
        {
            horizonI = context.horizon();
        }
        else
        {
            horizonI = context.horizon()
                    .format($wnd.d3.format(".2%"));
        }
        selection.call(function (div)
        {
            div.datum(metricI);
            div.append("div")
                    .attr("class", "horizon")
                    .call(horizonI);
        });
    }
    // On mousemove, reposition the chart values to match the rule.
    context.on("focus", function (i)
    {
        $wnd.d3.selectAll(".value")
                .style("right", i == null ? null : context.size() - i + "px");
    });
}-*/;
A little explanation. This code create and initialize a Cubims context:

var context = $wnd.cubism.context()        
    .serverDelay(this.@full.class.Name::serverDelay) 
    .clientDelay(this.@full.class.Name::clientDelay)
    .step(this.@full.class.Name::step)
    .size(this.@full.class.Name::width);
Where

this.@full.class.Name::serverDelay,
this.@full.class.Name::clientDelay,
this.@full.class.Name::step,
this.@full.class.Name::width
are fields of class.
Here we customize the div id. This is necessary so that we can use multiple charts on one page:

var chartDiv = "#" + this.@full.class.Name::chartId;

Here we obtain a chart metrics from java:

var jsonMetrics = eval(this.@full.class.Name::getMetrics()());
Java method getMetrics generates JSON structure:

public String getJSONMetrics(List metrics)
{
    String s = "[";
    MetricDisplaySummary metric;
    for (int i = 0; i < metrics.size(); i++)
    {
        metric = metrics.get(i);
        s += " {label:'" + metric.getLabel() + "',metricIndex:" + i
            + ",metricName:'" + metric.getMetricName() + "',metricUnit:'"
            + metric.getUnits() + "'},";
    }
    s += "]";
    return s;
}
Cubism request periodically a data with 'step' interval:

var jsonPoints = self.@full.class.Name::getPoints(IDD)(metricIndex,
        start.getTime(), stop.getTime());
Constantly calling this method, Cubism provides visualization of data in real time. Note the method signature - cubism requests data for the period from "start" to "stop". The data will look more beautiful if in this period will be at least one point on the "step" interval.
The last point about build the graph:

for (var i = 0; i < jsonMetrics.length; i++)
{
    var jsonMetric = jsonMetrics[i];
    var metricI = createMetric(jsonMetric.label, jsonMetric.metricIndex);
    var horizonI;

    if (jsonMetric.metricUnit != 'PERCENTAGE')
    {
        horizonI = context.horizon();
    }
    else
    {
        horizonI = context.horizon()
                .format($wnd.d3.format(".2%"));
    }
    selection.call(function (div)
    {
        div.datum(metricI);
        div.append("div")
                .attr("class", "horizon")
                .call(horizonI);
    });
}
In this loop shows you how you can customize each metric separately, including to set the height, colors, units, etc. So in your hands have a full control of the graph building and you can use all library visualization features.

Written above is enough to integrate Cubism graphs in SmartGWT. I will share experiences, how to make this code more concise. GWT developers recommend to use JSNI code very sparingly. In this example JSNI used except a plot building also to initialize the metrics and data acquisition. The result is a need to write additional code. Much more practical to implement in your project getting metrics and data via REST API in JSON format. Then your JSNI code will be able to get the data yourself, as most JS visualization libraries involves obtaining data this way. And once implementing REST API + JSON, you get rid of binding in the java code to a specific JS library and can easily replace visualization library if necessary.

References:

  1. Cubism.js: http://square.github.com/cubism;
  2. JSNI Coding Basics: https://developers.google.com/web-toolkit/doc/latest/DevGuideCodingBasicsJSNI;
  3. SmartGWT documentation: http://www.smartclient.com/product/documentation.jsp.

1 comment: