Automate report generation using PhantomJS

Intro

You are already using amCharts libraries to visualize your data to your users on your website, intranet, CMS, even mobile applications.

But what about the situations when you want to generate the reports to your users. Automatically. On server-side. Without any human interaction whatsoever?

That's where server-side browsers come in.

Introducing PhantomJS - the crown jewel of such software. PhantomJS is a free, headless browser, that can generate an image or a PDF out of any web page, including dynamic content, such as charts and maps.

It's available for virtually any platform - Windows, Linux, Max OS, you name it. And it's using the same rendering engine as the Chrome and Safari browsers, so you know your code is compatible.

Download and Install

The installation depends on the platform.

You can download PhantomJS binaries for Windows or Mac OS, or source code, to compile on Linux directly from their Downloads page:

http://phantomjs.org/download.html

There are no official binaries available for Linux builds, yet. Individual Linux distributions like RedHat and Ubuntu might have their own binary packages available for install. Make sure you verify if maybe a ready-made package is already available for your Linux flavor.

Alternatively, you can follow PhantomJS own tutorial on how to compile your own binaries out of the source code:

http://phantomjs.org/build.html

Verify PhantomJS Install

Try running this command in your shell/command prompt:

phantomjs --version

Get PhantomJS version info? Great. Let's move on.

Getting Started

PhantomJS works by executing its binary in the shell or command prompt. It can be typed in by admin user, invoked from some script or scheduled job. (CRON)

To run PhantomJS, it needs a config file, which is basically a JavaScript code. You specify as a first parameter to phantomjs binary. I.e.:

phantomjs report.js

The PhantomJS quick intro tutorial does a good job of explaining the basics. Make sure you go through it before proceeding with this article. We're not going to repeat that here:

http://phantomjs.org/quick-start.html

Generating a Report

Now that we know how PhantomJS works, let's build a config script that:

  1. Accepts the URL of the web page to generate as a PDF;
  2. Requests that page and produces the output, which is then saved to the some directory.

Here we have a report-like test page, with several charts we're going to be using as a lab test rat.

See the Pen Multiple charts on the page (PhantomJS article asset) by amCharts (@amcharts) on CodePen.11061

Let's build a PhantomJS script that will grab that (or any other) page:

var page = require( 'webpage' ).create();
page.open( 'http://s.codepen.io/amcharts/debug/cd2e8ce27e3a96f43bb79d5d23722d11', function( status ) {
  console.log( "Status: " + status );
  if ( status === "success" ) {
    page.render( 'example.pdf' );
  }
  phantom.exit();
} );

Now if I run this command, I get a nice copy of the our test page as an example.pdf:

phantomjs report.js

Let's enhance our script to accept two parameters: a URL of the web page to grab and output path/filename.

/**
 * Create page object
 */
var page = require( 'webpage' ).create(),
    system = require('system');

/**
 * Check for required parameters
 */
if ( system.args.length < 3 ) {
  console.log( 'Usage: report.js <some URL> <output path/filename>' );
  phantom.exit();
}

/**
 * Grab the page and output it to specified target
 */
page.open( system.args[ 1 ], function( status ) {
  console.log( "Status: " + status );

  /**
   * Output the result
   */
  if ( status === "success" ) {
    page.render( system.args[ 2 ] );
  }

  phantom.exit();
} );

By the way, PhantomJS will recognize the output format from the extension of the output file name, so if you will specify something like report.png, it will product a nice PNG image. Cool, huh?

So, now to produce PDF of the page, we're going to use this line:

phantomjs report.js http://s.codepen.io/amcharts/debug/cd2e8ce27e3a96f43bb report.pdf

And, when we're in the mood for some PNG:

phantomjs report.js http://s.codepen.io/amcharts/debug/cd2e8ce27e3a96f43bb report.png
report generated with PhantomJS
And… voila!

Dealing with Chart Animations

So far the script we were using did a good job of producing the output. There's one caveat, though: the export kicks in as soon as the page loads. Which means that if your charts are set up to play animations, they will be exported before animations finish, resulting in incomplete appearance.

Workaround for chart animation
Oh noes!

We could delay our rendering operation in PhantomJS script by a few seconds. However, we're not going to do that since we don't want our exports to take that long. Instead we're going to disable all animations before they have a chance to execute.

AmCharts.addInitHandler is the perfect place for that. We're going to couple it with PhantomJS page.onResourceReceived event to add amCharts-wide overrides to reset animation settings.

/**
 * Create page object
 */
var page = require( 'webpage' ).create(),
  system = require( 'system' );

/**
 * Check for required parameters
 */
if ( system.args.length < 3 ) {
  console.log( 'Usage: report.js <some URL> <output path/filename>' );
  phantom.exit();
}

/**
 * Check when amCharts main library is loaded. Add overrides then.
 */
page.onResourceReceived = function() {
  if ( arguments[ 0 ].url.match( /(amcharts.js)|(ammap.js)/ ) ) {
    page.evaluate( function() {
      setTimeout( function() {
        if ( AmCharts === undefined )
          return;
        AmCharts.addInitHandler( function( chart ) {
          if ( chart.type == "stock" )
            chart.panelsSettings.startDuration = 0;
          else if ( chart.type == "map" )
            chart.zoomDuration = 0;
          else
            chart.startDuration = 0;
        } );
      } );
    }, 100 );
  }
}

/**
 * Grab the page and output it to specified target
 */
page.open( system.args[ 1 ], function( status ) {
  console.log( "Status: " + status );
  if ( status === "success" ) {
    page.render( system.args[ 2 ] );
  }
  phantom.exit();
} );

Conclusion

PhantomJS can be powerful server-side automation tool. Remember, anything you can do with JavaScript, you can do with PhantomJS.

Here's more information about it:

http://phantomjs.org/page-automation.html

Please note, that PhantomJS JavaScript code runs in it's own scope. You can't reference web page objects and code directly from PhantomJS code and vice versa.

If you would need to execute some code in the scope of the web page you are requesting, you need to use PhantomJS's page.render function:

http://phantomjs.org/api/webpage/method/evaluate.html

Assets

Download the final PhantomJS configuration script used in this article:

amcharts_phantomjs.zip