Skip to content

September 22, 2012

PhantomJS: a new way for running Highcharts on the server side

by Joe Kuan

While I was writing up a survey for running Highcharts on the server side which accounts for 3 approaches:

  • Xvfb + web browser on server side (Unix only)
  • Rhino + Env.js (Java solution)
  • Node/Node.js + Highcharts NodeJs module (Javascript solution)

I didn’t realise there is another one, PhantomJS (thanks to the remark by Torstein Hønsi).

PhantomJS is a headless webkit which makes running JavaScripts on the server side a lot easier and no more issue on DOM implementations. It’s like running a HTML5 browser on the server without it’s user interface. There are several tasks which comes very handy with PhantomJS and server side SVG rendering is one of them.

Running PhantomJS is as simple as launching a command on the server side like:

phantomjs exportLineChart.js seriesData.js

Lets see how we can create exportChart.js. First of all, load up the required basic modules. The most interesting is the 2nd line, which loads up a webpage module and creates a WebPage object. This WebPage object has empty content and understand DOM.

var system = require('system');
var page = require('webpage').create();
var fs = require('fs');

Then we load up all the necessary JavaScripts files into the page. Before we run any JavaScripts code in the page, we must first setup the page’s console message handling with onConsoleMessage which we redirect the messages onto our console screen.

page.injectJs("../jquery-1.7.1.min.js") || ( console.log("Unable to load jQuery") && phantom.exit());
page.injectJs("../highcharts/js/highcharts.js") || ( console.log("Unable to load Highcharts") && phantom.exit());
page.injectJs("../highcharts/js/modules/exporting.js") || (console.log("Unable to load Highcharts") && phantom.exit());

page.onConsoleMessage = function (msg) {
    console.log(msg);
};

Then we process the command line arguments before we run our export script:

phantom.injectJs(system.args[1]) || (console.log("Unable to load json file") && phantom.exit());

var width = 350, height = 300;
if (system.args.length == 4) {
    width = parseInt(system.args[2], 10);
    height = parseInt(system.args[3], 10);
}

console.log("Loaded result file");

// Build up result and chart size args for evaluate function
var evalArg = {
   result: result,
   width: width,
   height: height
};

Then we call page.evaluate method, which executes JavaScripts inside the page object. Note that inside the JavaScript code, it is executing inside the scope of the page. Hence, you cannot access any PhantomJS modules at all but you can pass data to the code. In here, we are passing an object with series data into the evaluate’s callback function.

var svg = page.evaluate(function(opt) {
    .... 
}, evalArg);

As we are running with an empty page object, the first thing we need to do is to setup a container in the DOM for Highcharts to render on. After that, we can construct a Highcharts configuration object with the series data passing via the callback argument.

var svg = page.evaluate(function(opt) {

    // Inject container, so Highcharts can render to
    $('body').append('<div id="container"></div>');

    var seriesArray = [];
    $.each(opt.result.drivers, function(idx, driver) {
         seriesArray.push({
                           name: driver.name,
                           data: driver.laps,
                           color: driver.color,
                           animation: false
         });
    });

    // Normal code to create Highcharts
    var chart = new Highcharts.Chart({
          chart: {
              animation: false,
              renderTo: 'container',
              width: opt.width,
              height: opt.height
          },
          series: seriesArray,
          ....
    });

    return chart.getSVG();
}, evalArg);

Once the chart is created, then we can return the exported SVG content from the evaluate method and save it to a file. As for creating seriesData.js, since these are all executed on the server side, it is up to the application requirement on extracting the series data (from a DB or some files) and storing into a JavaScripts file. For instance on PHP, we can simply do

file_put_contents('seriesData.js', "var result = " . json_encode($data) . ";"); 

According to Torstein Hønsi, the good news is that Highcharts 3.0 will come with a PhantomJS script. All we need to do is to pass a JavaScripts file with chart configuration in it. This purpose of this article is to show you that you can use Highcharts on the server side and probably many other tasks.

Here is the full list of the sample program:

var system = require('system');
var page = require('webpage').create();
var fs = require('fs');

page.injectJs("../jquery-1.7.1.min.js") || ( console.log("Unable to load jQuery") && phantom.exit());
page.injectJs("../highcharts/js/highcharts.js") || ( console.log("Unable to load Highcharts") && phantom.exit());
page.injectJs("../highcharts/js/modules/exporting.js") || (console.log("Unable to load Highcharts") && phantom.exit());

page.onConsoleMessage = function (msg) {
    console.log(msg);
};

phantom.injectJs(system.args[1]) || (console.log("Unable to load json file") && phantom.exit());

var width = 350, height = 300;
if (system.args.length == 4) {
    width = parseInt(system.args[2], 10);
    height = parseInt(system.args[3], 10);
}

console.log("Loaded result file");

// Build up result and chart size args for evaluate function
var evalArg = {
   result: result,
   width: width,
   height: height
};

var svg = page.evaluate(function(opt) {

    // Inject container, so Highcharts can render to
    $('body').append('<div id="container"></div>');

    var seriesArray = [];
    $.each(opt.result.drivers, function(idx, driver) {
         seriesArray.push({
                           name: driver.name,
                           data: driver.laps,
                           color: driver.color,
                           animation: false
         });
    });

    var chart = new Highcharts.Chart({
             chart: {
                   renderTo: 'container',
                   animations: false,
                   width: opt.width,
                   height: opt.height
             },
             exporting: {
                   enabled: false,
             },
             series: seriesArray,
             title: {
                   text: 'Belgian Grand Prix 2012 - Drivers Lap Times'
             },
             credits: {
                   text: 'Source: www.f1fanatic.co.uk'
             },
             yAxis: {
                   title: {
                       text: 'Time (secs)'
                   },
                   maxPadding: 0.17
             },
             plotOptions: {
                   series: {
                       pointStart: 1
                    }
             },
             xAxis: {
                   title: {
                       text: 'Laps',
                       align: 'high'
                   }
             }
       });

       console.log("Exported to SVG");
       return chart.getSVG();
}, evalArg);

fs.isFile("/tmp/chart.svg") && fs.remove("/tmp/chart.svg");
console.log("Saved SVG to file");
fs.write("/tmp/chart.svg", svg);
phantom.exit();

Leave a comment

Note: HTML is allowed. Your email address will never be published.

Subscribe to comments