Monday, June 03, 2013

A JavaScript Charting Solution - FusionCharts

Recently I have been working on FusionCharts to build dashboard pages. I think it's worthy writing some notes of my experience of using FusionCharts.

First of all, FusionCharts customer services are very good. I downloaded a trial version and did some testing. Then I have some issues and questions to ask. I always got their reply on the same day. FusionCharts is an India company. A few times I even received their replies in the afternoon which is midnight there (I am in UTC-5 zone). Overall they are helpful and respond quickly even although I am not their customer yet.

Secondly FusionCharts are purely client site implementation using Flash and JavaScript. All my discussion in this article is based on their JavaScript solution. That's different from some other data visualization tools I used before, such as Dundas, Infragistrics and DevExpress, all of which are .NET components, and they are considered as server side solution where you build your charts in ASP.NET code behind, and the charting images are generated in the server side.

The good part of client side approach is easy to develop and deploy. You simply create a HTML page, add desired FusionCharts objects, set their data source using JavaScript then you are done. The chart page can be put to anywhere that the user can access to such as in a web server, a shared drive or even local box, as long as the data source is also accessible to the user. In addition the client side JavaScript is considered as a cross-platform solution being able to run in different environment.

The drawbacks of client side approach include the potential performance issue, and the cross-domain data source restriction, as discussed below in detail.

Performance Issue

The charts are populated and rendered inside the browser. The performance could be an issue if you have many charts on a page. One simple dashboard test page with 10 charts consuming static content takes 3-5 seconds to render in latest Firefox and Chrome, and IE8/9 are even worst taking a least double time to render the page. Note that the tests are conducted locally so in reality the network round-trip would introduce some more delay. This may not be unacceptable in some cases. One suggestion from FusionCharts to improve the performance is wrap the charting JavaScript inside a setTimeout function and that would make the page a little bit more responsive during the page initial load:

    setTimeout(function () {
        var myChart= new FusionCharts( { id: "chart1", type: "Column2D", width: "400", height: "250"} );  
        myChart.setXMLUrl("chart1.xml");      
        myChart.render("chart1"); 
    }, 0);

Cross Domain Data Source Issue

FusionCharts support XML or JSON data source. The chart data can be embedded inside the page, static file or dynamically loaded from the server. There will be a cross-domain issue if the data source is not coming from the same domain as the original one hosting the page. For instance, if the chart page is from http://companyA.com, then all data from http://companyB.com will be rejected by the browser.

Recent version of Firefox and Chrome as well as IE10 support CORS standard to resolve the cross-domain issue. Basically the response from http://companyB.com includes some specific headers informing the browser to accept this cross-domain data. For above example the header "Access-Control-Allow-Origin: http://companyA.com" or "Access-Control-Allow-Origin: *" would work. Another generic solution for cross-domain requests is use JSONP. The client sends a request with a callback function name and the server wraps all the data as a parameter for that callback function.

Following code snippet demos how to implement CORS and JSONP in .NET:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Text;

namespace DataService
{
    /// <summary>
    /// Web handler to serve FusionCharts data using JSONP or CORS to resolve cross-domain issue 
    /// </summary>
    public class FusionChartService : IHttpHandler
    {

        public void ProcessRequest(HttpContext context)
        {
            string data = GetData(); // Populate chart data
            bool isJsonFormat = false; // Are data JSON format?
            
            context.Response.ContentEncoding = System.Text.Encoding.UTF8;
            context.Response.ContentType = "text/plain";

            if (string.IsNullOrEmpty(callback)) // Add CORS headers if NOT JSONP
            {
                context.Response.AppendHeader("Access-Control-Allow-Origin", "*");
                context.Response.AppendHeader("Access-Control-Allow-Headers", "Content-Type");
                string requestHeaders = context.Request.Headers["Access-Control-Request-Headers"];
                if (!String.IsNullOrEmpty(requestHeaders))
                    context.Response.AppendHeader("Access-Control-Allow-Headers", requestHeaders);
                context.Response.Write(data);
            }
            else // Wrap data inside the callback founction for JSONP
            {
                context.Response.Write(string.Format("{0}({1})", callback, isJsonFormat ? data : EscapeString(data)));
            }
        }

        private static string EscapeString(string s)
        {
            StringBuilder sb = new StringBuilder();
            sb.Append("\"");
            foreach (char c in s)
            {
                switch (c)
                {
                    case '\"':
                        sb.Append("\\\"");
                        break;
                    case '\\':
                        sb.Append("\\\\");
                        break;
                    case '\b':
                        sb.Append("\\b");
                        break;
                    case '\f':
                        sb.Append("\\f");
                        break;
                    case '\n':
                        sb.Append("\\n");
                        break;
                    case '\r':
                        sb.Append("\\r");
                        break;
                    case '\t':
                        sb.Append("\\t");
                        break;
                    default:
                        int i = (int)c;
                        if (i < 32 || i > 127)
                            sb.AppendFormat("\\u{0:X04}", i);
                        else
                            sb.Append(c);
                        break;
                }
            }
            sb.Append("\"");

            return sb.ToString();
        }

        public bool IsReusable
        {
            get
            {
                return false;
            }
        }
    }
}
An example of Ajax call consuming the JSONP service using jQuery (testing with version of 1.8.3):
    var params = { url: 'http://companyB.com/FusionChartService.ashx', cache: false, crossDomain: true, dataType: 'jsonp text'};
    $.ajax(params).done(function(data) {
        var myChart = new FusionCharts( { id: "chart1", type: "Column2D", width: "400", height: "250", debugMode : false } );  
        myChart.setXMLData(data);      
        myChart.render("chart1"); 
    }).fail(function(xhr, err) {
        console.log(err);
    }); 

Cross Domain Authentication Issue

Cross-domain authentication is the major challenge for client side JavaScript solution. Windows and Kerberos authentication simply don't work. There's not much you can do with JavaScript in terms of cross-domain authentication. You can either enable anonymous access or implement BASIC authentication in the server. In the latter case, the client needs to pass plain text username and password for each cross-domain Ajax call which is not safe and is not allowed in most organizations. One workaround is use proxy server to authenticate the service. But that's not a flexible solution as that's hard to implement and maintain, introducing extra layer of delay, and pushing back some logic to the server. So FusionCharts is not recommended if do have such cross-domain use case.

2013-06 Update: More on cross-domain authentication.