2
* @license Highcharts JS v3.0.1 (2012-11-02)
6
* Author: Gert Vaartjes
8
* License: www.highcharts.com/license
13
/*jslint white: true */
14
/*global window, require, phantom, console, $, document, Image, Highcharts, clearTimeout, clearInterval, options, cb, globalOptions, dataOptions, customCode */
21
/* define locations of mandatory javascript files */
22
HIGHCHARTS: 'highstock.js',
23
HIGHCHARTS_MORE: 'highcharts-more.js',
24
HIGHCHARTS_DATA: 'data.js',
25
JQUERY: 'jquery.1.9.1.min.js',
26
TIMEOUT: 2000 /* 2 seconds timout for loading images */
33
SVG_DOCTYPE = '<?xml version=\"1.0" standalone=\"no\"?><!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">',
35
system = require('system'),
40
var args = arguments, i, arg, length = args.length;
41
for (i = 0; i < length; i += 1) {
43
if (arg !== undefined && arg !== null && arg !== 'null' && arg != '0') {
49
mapCLArguments = function () {
54
if (system.args.length < 1) {
55
console.log('Commandline Usage: highcharts-convert.js -infile URL -outfile filename -scale 2.5 -width 300 -constr Chart -callback callback.js');
56
console.log(', or run PhantomJS as server: highcharts-convert.js -host 127.0.0.1 -port 1234');
59
for (i = 0; i < system.args.length; i += 1) {
60
if (system.args[i].charAt(0) === '-') {
61
key = system.args[i].substr(1, i.length);
62
if (key === 'infile' || key === 'callback' || key === 'dataoptions' || key === 'globaloptions' || key === 'customcode') {
63
// get string from file
65
map[key] = fs.read(system.args[i + 1]).replace(/^\s+/, '');
67
console.log('Error: cannot find file, ' + system.args[i + 1]);
71
map[key] = system.args[i + 1];
78
render = function (params, exitCallback) {
80
var page = require('webpage').create(),
97
messages.imagesLoaded = 'Highcharts.images.loaded';
98
messages.optionsParsed = 'Highcharts.options.parsed';
99
messages.callbackParsed = 'Highcharts.cb.parsed';
100
window.imagesLoaded = false;
101
window.optionsParsed = false;
102
window.callbackParsed = false;
104
page.onConsoleMessage = function (msg) {
108
* Ugly hack, but only way to get messages out of the 'page.evaluate()'
109
* sandbox. If any, please contribute with improvements on this!
112
if (msg === messages.imagesLoaded) {
113
window.imagesLoaded = true;
115
/* more ugly hacks, to check options or callback are properly parsed */
116
if (msg === messages.optionsParsed) {
117
window.optionsParsed = true;
120
if (msg === messages.callbackParsed) {
121
window.callbackParsed = true;
125
page.onAlert = function (msg) {
129
/* scale and clip the page */
130
scaleAndClipPage = function (svg) {
131
/* param: svg: The scg configuration object
135
pageWidth = pick(params.width, svg.width),
139
if (parseInt(pageWidth, 10) == pageWidth) {
140
zoom = pageWidth / svg.width;
143
/* set this line when scale factor has a higher precedence
144
scale has precedence : page.zoomFactor = params.scale ? zoom * params.scale : zoom;*/
146
/* params.width has a higher precedence over scaling, to not break backover compatibility */
147
page.zoomFactor = params.scale && params.width == undefined ? zoom * params.scale : zoom;
149
clipwidth = svg.width * page.zoomFactor;
150
clipheight = svg.height * page.zoomFactor;
152
/* define the clip-rectangle */
153
/* ignored for PDF, see https://github.com/ariya/phantomjs/issues/10465 */
161
/* for pdf we need a bit more paperspace in some cases for example (w:600,h:400), I don't know why.*/
162
if (outType === 'pdf') {
163
// changed to a multiplication with 1.333 to correct systems dpi setting
164
clipwidth = clipwidth * dpiCorrection;
165
clipheight = clipheight * dpiCorrection;
166
// redefine the viewport
167
page.viewportSize = { width: clipwidth, height: clipheight};
168
// make the paper a bit larger than the viewport
169
page.paperSize = { width: clipwidth + 2 , height: clipheight + 2 };
173
exit = function (result) {
175
//Calling page.close(), may stop the increasing heap allocation
178
exitCallback(result);
181
convert = function (svg) {
183
scaleAndClipPage(svg);
184
if (outType === 'pdf' || output !== undefined || !serverMode) {
185
if (output === undefined) {
186
// in case of pdf files
187
output = config.tmpDir + '/chart.' + outType;
192
base64 = page.renderBase64(outType);
197
renderSVG = function (svg) {
199
// From this point we have loaded/or created a SVG
201
if (outType.toLowerCase() === 'svg') {
203
svg = svg.html.replace(/<svg /, '<svg xmlns:xlink="http://www.w3.org/1999/xlink" ').replace(/ href=/g, ' xlink:href=').replace(/<\/svg>.*?$/, '</svg>');
205
svg = SVG_DOCTYPE + svg;
207
if (output !== undefined) {
209
svgFile = fs.open(output, "w");
214
// return the svg as a string
219
// output binary images or pdf
220
if (!window.imagesLoaded) {
221
// render with interval, waiting for all images loaded
222
interval = window.setInterval(function () {
223
console.log('waiting');
224
if (window.imagesLoaded) {
226
clearInterval(interval);
231
// we have a 3 second timeframe..
232
timer = window.setTimeout(function () {
233
clearInterval(interval);
234
exitCallback('ERROR: While rendering, there\'s is a timeout reached');
237
// images are loaded, render rightaway
242
console.log('ERROR: While rendering, ' + e);
246
loadChart = function (input, outputType, messages) {
247
var nodeIter, nodes, elem, opacity, counter, svgElem;
249
document.body.style.margin = '0px';
250
document.body.innerHTML = input;
252
function decrementImgCounter() {
255
console.log(messages.imagesLoaded);
259
function loadImages() {
260
var images = document.getElementsByTagName('image'), i, img;
262
if (images.length > 0) {
264
counter = images.length;
266
for (i = 0; i < images.length; i += 1) {
268
/* onload decremnts the counter, also when error (perhaps 404), then we wont wait for this image to be loaded */
269
img.onload = img.onerror = decrementImgCounter;
270
/* force loading of images by setting the src attr.*/
271
img.src = images[i].href.baseVal;
274
// no images set property to:imagesLoaded = true
275
console.log(messages.imagesLoaded);
279
if (outputType === 'jpeg') {
280
document.body.style.backgroundColor = 'white';
284
nodes = document.querySelectorAll('*[stroke-opacity]');
286
for (nodeIter = 0; nodeIter < nodes.length; nodeIter += 1) {
287
elem = nodes[nodeIter];
288
opacity = elem.getAttribute('stroke-opacity');
289
elem.removeAttribute('stroke-opacity');
290
elem.setAttribute('opacity', opacity);
293
// ensure all image are loaded
296
svgElem = document.getElementsByTagName('svg')[0];
299
html: document.body.innerHTML,
300
width: svgElem.getAttribute("width"),
301
height: svgElem.getAttribute("height")
305
createChart = function (width, constr, input, globalOptionsArg, dataOptionsArg, customCodeArg, outputType, callback, messages) {
307
var $container, chart, nodes, nodeIter, elem, opacity, counter;
309
// dynamic script insertion
310
function loadScript(varStr, codeStr) {
311
var $script = $('<script>').attr('type', 'text/javascript');
312
$script.html('var ' + varStr + ' = ' + codeStr);
313
document.getElementsByTagName("head")[0].appendChild($script[0]);
314
if (window[varStr] !== undefined) {
315
console.log('Highcharts.' + varStr + '.parsed');
319
function decrementImgCounter() {
322
console.log(messages.imagesLoaded);
326
function loadImages() {
327
var $images = $('svg image'), i, img;
329
if ($images.length > 0) {
331
counter = $images.length;
333
for (i = 0; i < $images.length; i += 1) {
335
/* onload decremnts the counter, also when error (perhaps 404), then we wont wait for this image to be loaded */
336
img.onload = img.onerror = decrementImgCounter;
337
/* force loading of images by setting the src attr.*/
338
img.src = $images[i].getAttribute('href');
341
// no images set property to:imagesLoaded = true
342
console.log(messages.imagesLoaded);
346
function parseData(completeHandler, chartOptions, dataConfig) {
348
dataConfig.complete = completeHandler;
349
Highcharts.data(dataConfig, chartOptions);
351
completeHandler(undefined);
355
if (input !== 'undefined') {
356
loadScript('options', input);
359
if (callback !== 'undefined') {
360
loadScript('cb', callback);
363
if (globalOptionsArg !== 'undefined') {
364
loadScript('globalOptions', globalOptionsArg);
367
if (dataOptionsArg !== 'undefined') {
368
loadScript('dataOptions', dataOptionsArg);
371
if (customCodeArg !== 'undefined') {
372
loadScript('customCode', customCodeArg);
375
$(document.body).css('margin', '0px');
377
if (outputType === 'jpeg') {
378
$(document.body).css('backgroundColor', 'white');
381
$container = $('<div>').appendTo(document.body);
382
$container.attr('id', 'container');
384
// disable animations
385
Highcharts.SVGRenderer.prototype.Element.prototype.animate = Highcharts.SVGRenderer.prototype.Element.prototype.attr;
387
if (!options.chart) {
391
options.chart.renderTo = $container[0];
393
// check if witdh is set. Order of precedence:
394
// args.width, options.chart.width and 600px
396
// OLD. options.chart.width = width || options.chart.width || 600;
397
// Notice we don't use commandline parameter width here. Commandline parameter width is used for scaling.
399
options.chart.width = (options.exporting && options.exporting.sourceWidth) || options.chart.width || 600;
400
options.chart.height = (options.exporting && options.exporting.sourceHeight) || options.chart.height || 400;
402
// Load globalOptions
404
Highcharts.setOptions(globalOptions);
409
parseData(function completeHandler(opts) {
410
// Merge series configs
411
if (options.series) {
412
Highcharts.each(options.series, function (series, i) {
413
options.series[i] = Highcharts.merge(series, opts.series[i]);
417
var mergedOptions = Highcharts.merge(opts, options);
421
customCode(mergedOptions);
424
chart = new Highcharts[constr](mergedOptions, cb);
426
// ensure images are all loaded
428
}, options, dataOptions);
430
chart = new Highcharts[constr](options, cb);
432
// ensure images are all loaded
436
/* remove stroke-opacity paths, used by mouse-trackers, they turn up as
437
* as fully opaque in the PDF
439
nodes = document.querySelectorAll('*[stroke-opacity]');
441
for (nodeIter = 0; nodeIter < nodes.length; nodeIter += 1) {
442
elem = nodes[nodeIter];
443
opacity = elem.getAttribute('stroke-opacity');
444
elem.removeAttribute('stroke-opacity');
445
elem.setAttribute('opacity', opacity);
449
//html: $container[0].firstChild.innerHTML,
450
html: $('div.highcharts-container')[0].innerHTML,
451
width: chart.chartWidth,
452
height: chart.chartHeight
456
if (params.length < 1) {
457
exit("Error: Insuficient parameters");
459
input = params.infile;
460
output = params.outfile;
462
if (output !== undefined) {
463
outType = pick(output.split('.').pop(),'png');
465
outType = pick(params.type,'png');
468
constr = pick(params.constr, 'Chart');
469
callback = params.callback;
470
width = params.width;
472
if (input === undefined || input.length === 0) {
473
exit('Error: Insuficient or wrong parameters for rendering');
476
page.open('about:blank', function (status) {
478
globalOptions = params.globaloptions,
479
dataOptions = params.dataoptions,
480
customCode = 'function customCode(options) {\n' + params.customcode + '}\n';
482
/* Decide if we have to generate a svg first before rendering */
483
if (input.substring(0, 4).toLowerCase() === "<svg" || input.substring(0, 5).toLowerCase() === "<?xml"
484
|| input.substring(0, 9).toLowerCase() === "<!doctype") {
485
//render page directly from svg file
486
svg = page.evaluate(loadChart, input, outType, messages);
487
page.viewportSize = { width: svg.width, height: svg.height };
490
// We have a js file, let highcharts create the chart first and grab the svg
492
// load necessary libraries
493
page.injectJs(config.JQUERY);
494
page.injectJs(config.HIGHCHARTS);
495
page.injectJs(config.HIGHCHARTS_MORE);
496
page.injectJs(config.HIGHCHARTS_DATA);
498
// load chart in page and return svg height and width
499
svg = page.evaluate(createChart, width, constr, input, globalOptions, dataOptions, customCode, outType, callback, messages);
501
if (!window.optionsParsed) {
502
exit('ERROR: the options variable was not available, contains the infile an syntax error? see' + input);
505
if (callback !== undefined && !window.callbackParsed) {
506
exit('ERROR: the callback variable was not available, contains the callbackfile an syntax error? see' + callback);
514
startServer = function (host, port) {
515
var server = require('webserver').create();
517
server.listen(host + ':' + port,
518
function (request, response) {
519
var jsonStr = request.post,
523
params = JSON.parse(jsonStr);
525
// for server health validation
526
response.statusCode = 200;
527
response.write('OK');
530
render(params, function (result) {
531
response.statusCode = 200;
532
response.write(result);
537
msg = "Failed rendering: \n" + e;
538
response.statusCode = 500;
539
response.setHeader('Content-Type', 'text/plain');
540
response.setHeader('Content-Length', msg.length);
544
}); // end server.listen
546
// switch to serverMode
549
console.log("OK, PhantomJS is ready.");
552
args = mapCLArguments();
554
// set tmpDir, for output temporary files.
555
if (args.tmpdir === undefined) {
556
config.tmpDir = fs.workingDirectory + '/tmp';
558
config.tmpDir = args.tmpdir;
561
// exists tmpDir and is it writable?
562
if (!fs.exists(config.tmpDir)) {
564
fs.makeDirectory(config.tmpDir);
566
console.log('ERROR: Cannot make temp directory');
571
if (args.host !== undefined && args.port !== undefined) {
572
startServer(args.host, args.port);
574
// presume commandline usage
575
render(args, function (msg) {