/**
|
|
* @license Highcharts JS v3.0.1 (2012-11-02)
|
|
*
|
|
* (c) 20013-2014
|
|
*
|
|
* Author: Gert Vaartjes
|
|
*
|
|
* License: www.highcharts.com/license
|
|
*
|
|
* version: 2.0.1
|
|
*/
|
|
|
|
/*jslint white: true */
|
|
/*global window, require, phantom, console, $, document, Image, Highcharts, clearTimeout, clearInterval, options, cb, globalOptions, dataOptions, customCode */
|
|
|
|
|
|
(function () {
|
|
"use strict";
|
|
|
|
var config = {
|
|
/* define locations of mandatory javascript files.
|
|
* Depending on purchased license change the HIGHCHARTS property to
|
|
* highcharts.js or highstock.js
|
|
*/
|
|
|
|
files: {
|
|
highcharts: {
|
|
JQUERY: 'jquery.1.9.1.min.js',
|
|
HIGHCHARTS: 'highcharts.js',
|
|
HIGHCHARTS_MORE: 'highcharts-more.js',
|
|
HIGHCHARTS_DATA: 'data.js',
|
|
HIGHCHARTS_DRILLDOWN: 'drilldown.js',
|
|
HIGHCHARTS_FUNNEL: 'funnel.js',
|
|
HIGHCHARTS_HEATMAP: 'heatmap.js',
|
|
HIGHCHARTS_TREEMAP: 'treemap.js',
|
|
HIGHCHARTS_3D: 'highcharts-3d.js',
|
|
HIGHCHARTS_NODATA: 'no-data-to-display.js',
|
|
// Uncomment below if you have both Highcharts and Highmaps license
|
|
// HIGHCHARTS_MAP: 'map.js',
|
|
HIGHCHARTS_SOLID_GAUGE: 'solid-gauge.js',
|
|
BROKEN_AXIS: 'broken-axis.js'
|
|
},
|
|
highstock: {
|
|
JQUERY: 'jquery.1.9.1.min.js',
|
|
HIGHCHARTS: 'highcharts.js',
|
|
HIGHCHARTS_MORE: 'highcharts-more.js',
|
|
HIGHCHARTS_DATA: 'data.js',
|
|
HIGHCHARTS_DRILLDOWN: 'drilldown.js',
|
|
HIGHCHARTS_FUNNEL: 'funnel.js',
|
|
HIGHCHARTS_HEATMAP: 'heatmap.js',
|
|
HIGHCHARTS_TREEMAP: 'treemap.js',
|
|
HIGHCHARTS_3D: 'highcharts-3d.js',
|
|
HIGHCHARTS_NODATA: 'no-data-to-display.js',
|
|
// Uncomment below if you have both Highstock and Highmaps license
|
|
// HIGHCHARTS_MAP: 'map.js',
|
|
HIGHCHARTS_SOLID_GAUGE: 'solid-gauge.js',
|
|
BROKEN_AXIS: 'broken-axis.js'
|
|
},
|
|
highmaps: {
|
|
JQUERY: 'jquery.1.9.1.min.js',
|
|
HIGHCHARTS: 'highmaps.js',
|
|
HIGHCHARTS_DATA: 'data.js',
|
|
HIGHCHARTS_DRILLDOWN: 'drilldown.js',
|
|
HIGHCHARTS_HEATMAP: 'heatmap.js',
|
|
HIGHCHARTS_NODATA: 'no-data-to-display.js'
|
|
}
|
|
},
|
|
TIMEOUT: 5000 /* 5 seconds timout for loading images */
|
|
},
|
|
mapCLArguments,
|
|
render,
|
|
startServer = false,
|
|
args,
|
|
pick,
|
|
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\">',
|
|
dpiCorrection = 1.4,
|
|
system = require('system'),
|
|
fs = require('fs'),
|
|
serverMode = false;
|
|
|
|
pick = function () {
|
|
var args = arguments, i, arg, length = args.length;
|
|
for (i = 0; i < length; i += 1) {
|
|
arg = args[i];
|
|
if (arg !== undefined && arg !== null && arg !== 'null' && arg != '0') {
|
|
return arg;
|
|
}
|
|
}
|
|
};
|
|
|
|
mapCLArguments = function () {
|
|
var map = {},
|
|
i,
|
|
key;
|
|
|
|
if (system.args.length < 1) {
|
|
console.log('Commandline Usage: highcharts-convert.js -infile URL -outfile filename -scale 2.5 -width 300 -constr Chart -callback callback.js');
|
|
console.log(', or run PhantomJS as server: highcharts-convert.js -host 127.0.0.1 -port 1234');
|
|
}
|
|
|
|
for (i = 0; i < system.args.length; i += 1) {
|
|
if (system.args[i].charAt(0) === '-') {
|
|
key = system.args[i].substr(1, i.length);
|
|
if (key === 'infile' || key === 'callback' || key === 'dataoptions' || key === 'globaloptions' || key === 'customcode') {
|
|
// get string from file
|
|
try {
|
|
map[key] = fs.read(system.args[i + 1]).replace(/^\s+/, '');
|
|
} catch (e) {
|
|
console.log('Error: cannot find file, ' + system.args[i + 1]);
|
|
phantom.exit();
|
|
}
|
|
} else {
|
|
map[key] = system.args[i + 1];
|
|
}
|
|
}
|
|
}
|
|
return map;
|
|
};
|
|
|
|
render = function (params, exitCallback) {
|
|
|
|
var page = require('webpage').create(),
|
|
messages = {},
|
|
scaleAndClipPage,
|
|
loadChart,
|
|
createChart,
|
|
input,
|
|
constr,
|
|
callback,
|
|
width,
|
|
output,
|
|
outType,
|
|
timer,
|
|
renderSVG,
|
|
convert,
|
|
exit,
|
|
interval,
|
|
counter,
|
|
imagesLoaded = false;
|
|
|
|
messages.optionsParsed = 'Highcharts.options.parsed';
|
|
messages.callbackParsed = 'Highcharts.cb.parsed';
|
|
|
|
window.optionsParsed = false;
|
|
window.callbackParsed = false;
|
|
|
|
page.onConsoleMessage = function (msg) {
|
|
console.log(msg);
|
|
|
|
/*
|
|
* Ugly hack, but only way to get messages out of the 'page.evaluate()'
|
|
* sandbox. If any, please contribute with improvements on this!
|
|
*/
|
|
|
|
/* to check options or callback are properly parsed */
|
|
if (msg === messages.optionsParsed) {
|
|
window.optionsParsed = true;
|
|
}
|
|
|
|
if (msg === messages.callbackParsed) {
|
|
window.callbackParsed = true;
|
|
}
|
|
};
|
|
|
|
page.onAlert = function (msg) {
|
|
console.log(msg);
|
|
};
|
|
|
|
/* scale and clip the page */
|
|
scaleAndClipPage = function (svg) {
|
|
/* param: svg: The scg configuration object
|
|
*/
|
|
|
|
var zoom = 1,
|
|
pageWidth = pick(params.width, svg.width),
|
|
clipwidth,
|
|
clipheight;
|
|
|
|
if (parseInt(pageWidth, 10) == pageWidth) {
|
|
zoom = pageWidth / svg.width;
|
|
}
|
|
|
|
/* set this line when scale factor has a higher precedence
|
|
scale has precedence : page.zoomFactor = params.scale ? zoom * params.scale : zoom;*/
|
|
|
|
/* params.width has a higher precedence over scaling, to not break backover compatibility */
|
|
page.zoomFactor = params.scale && params.width == undefined ? zoom * params.scale : zoom;
|
|
|
|
clipwidth = svg.width * page.zoomFactor;
|
|
clipheight = svg.height * page.zoomFactor;
|
|
|
|
/* define the clip-rectangle */
|
|
/* ignored for PDF, see https://github.com/ariya/phantomjs/issues/10465 */
|
|
page.clipRect = {
|
|
top: 0,
|
|
left: 0,
|
|
width: clipwidth,
|
|
height: clipheight
|
|
};
|
|
|
|
/* for pdf we need a bit more paperspace in some cases for example (w:600,h:400), I don't know why.*/
|
|
if (outType === 'pdf') {
|
|
// changed to a multiplication with 1.333 to correct systems dpi setting
|
|
clipwidth = clipwidth * dpiCorrection;
|
|
clipheight = clipheight * dpiCorrection;
|
|
// redefine the viewport
|
|
page.viewportSize = { width: clipwidth, height: clipheight};
|
|
// make the paper a bit larger than the viewport
|
|
page.paperSize = { width: clipwidth + 2 , height: clipheight + 2 };
|
|
}
|
|
};
|
|
|
|
exit = function (result) {
|
|
if (serverMode) {
|
|
//Calling page.close(), may stop the increasing heap allocation
|
|
page.close();
|
|
}
|
|
exitCallback(result);
|
|
};
|
|
|
|
convert = function (svg) {
|
|
var base64;
|
|
scaleAndClipPage(svg);
|
|
if (outType === 'pdf' || output !== undefined || !serverMode) {
|
|
if (output === undefined) {
|
|
// in case of pdf files
|
|
output = config.tmpDir + '/chart.' + outType;
|
|
}
|
|
page.render(output);
|
|
exit(output);
|
|
} else {
|
|
base64 = page.renderBase64(outType);
|
|
exit(base64);
|
|
}
|
|
};
|
|
|
|
function decrementImgCounter() {
|
|
counter -= 1;
|
|
if (counter < 1) {
|
|
imagesLoaded = true;
|
|
}
|
|
}
|
|
|
|
function loadImages(imgUrls) {
|
|
var i, img;
|
|
counter = imgUrls.length;
|
|
for (i = 0; i < imgUrls.length; i += 1) {
|
|
img = new Image();
|
|
/* onload decrements the counter, also when error (perhaps 404), don't wait for this image to be loaded */
|
|
img.onload = img.onerror = decrementImgCounter;
|
|
/* force loading of images by setting the src attr.*/
|
|
img.src = imgUrls[i];
|
|
}
|
|
}
|
|
|
|
renderSVG = function (svg) {
|
|
var svgFile;
|
|
// From this point we have 'loaded' or 'created' a SVG
|
|
|
|
// Do we have to load images?
|
|
if (svg.imgUrls.length > 0) {
|
|
loadImages(svg.imgUrls);
|
|
} else {
|
|
// no images present, no loading, no waiting
|
|
imagesLoaded = true;
|
|
}
|
|
|
|
try {
|
|
if (outType.toLowerCase() === 'svg') {
|
|
// output svg
|
|
svg = svg.html.replace(/<svg /, '<svg xmlns:xlink="http://www.w3.org/1999/xlink" ').replace(/ href=/g, ' xlink:href=').replace(/<\/svg>.*?$/, '</svg>');
|
|
// add xml doc type
|
|
svg = SVG_DOCTYPE + svg;
|
|
|
|
if (output !== undefined) {
|
|
// write the file
|
|
svgFile = fs.open(output, "w");
|
|
svgFile.write(svg);
|
|
svgFile.close();
|
|
exit(output);
|
|
} else {
|
|
// return the svg as a string
|
|
exit(svg);
|
|
}
|
|
|
|
} else {
|
|
// output binary images or pdf
|
|
if (!imagesLoaded) {
|
|
// render with interval, waiting for all images loaded
|
|
interval = window.setInterval(function () {
|
|
if (imagesLoaded) {
|
|
clearTimeout(timer);
|
|
clearInterval(interval);
|
|
convert(svg);
|
|
}
|
|
}, 50);
|
|
|
|
// we have a 5 second timeframe..
|
|
timer = window.setTimeout(function () {
|
|
clearInterval(interval);
|
|
exitCallback('ERROR: While rendering, there\'s is a timeout reached');
|
|
}, config.TIMEOUT);
|
|
} else {
|
|
// images are loaded, render rightaway
|
|
convert(svg);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.log('ERROR: While rendering, ' + e);
|
|
}
|
|
};
|
|
|
|
loadChart = function (input, outputType) {
|
|
var nodeIter, nodes, elem, opacity, svgElem, imgs, imgUrls, imgIndex;
|
|
|
|
document.body.style.margin = '0px';
|
|
document.body.innerHTML = input;
|
|
|
|
if (outputType === 'jpeg') {
|
|
document.body.style.backgroundColor = 'white';
|
|
}
|
|
|
|
nodes = document.querySelectorAll('*[stroke-opacity]');
|
|
|
|
for (nodeIter = 0; nodeIter < nodes.length; nodeIter += 1) {
|
|
elem = nodes[nodeIter];
|
|
opacity = elem.getAttribute('stroke-opacity');
|
|
elem.removeAttribute('stroke-opacity');
|
|
elem.setAttribute('opacity', opacity);
|
|
}
|
|
|
|
svgElem = document.getElementsByTagName('svg')[0];
|
|
|
|
imgs = document.getElementsByTagName('image');
|
|
imgUrls = [];
|
|
|
|
for (imgIndex = 0; imgIndex < imgs.length; imgIndex = imgIndex + 1) {
|
|
imgUrls.push(imgs[imgIndex].href.baseVal);
|
|
}
|
|
|
|
return {
|
|
html: document.body.innerHTML,
|
|
width: svgElem.getAttribute("width"),
|
|
height: svgElem.getAttribute("height"),
|
|
imgUrls: imgUrls
|
|
};
|
|
};
|
|
|
|
createChart = function (constr, input, globalOptionsArg, dataOptionsArg, customCodeArg, outputType, callback, messages) {
|
|
|
|
var $container, chart, nodes, nodeIter, elem, opacity, imgIndex, imgs, imgUrls;
|
|
|
|
// dynamic script insertion
|
|
function loadScript(varStr, codeStr) {
|
|
var $script = $('<script>').attr('type', 'text/javascript');
|
|
$script.html('var ' + varStr + ' = ' + codeStr);
|
|
document.getElementsByTagName("head")[0].appendChild($script[0]);
|
|
if (window[varStr] !== undefined) {
|
|
console.log('Highcharts.' + varStr + '.parsed');
|
|
}
|
|
}
|
|
|
|
function parseData(completeHandler, chartOptions, dataConfig) {
|
|
try {
|
|
dataConfig.complete = completeHandler;
|
|
Highcharts.data(dataConfig, chartOptions);
|
|
} catch (error) {
|
|
completeHandler(undefined);
|
|
}
|
|
}
|
|
|
|
if (input !== 'undefined') {
|
|
loadScript('options', input);
|
|
}
|
|
|
|
if (callback !== 'undefined') {
|
|
loadScript('cb', callback);
|
|
}
|
|
|
|
if (globalOptionsArg !== 'undefined') {
|
|
loadScript('globalOptions', globalOptionsArg);
|
|
}
|
|
|
|
if (dataOptionsArg !== 'undefined') {
|
|
loadScript('dataOptions', dataOptionsArg);
|
|
}
|
|
|
|
if (customCodeArg !== 'undefined') {
|
|
loadScript('customCode', customCodeArg);
|
|
}
|
|
|
|
$(document.body).css('margin', '0px');
|
|
|
|
if (outputType === 'jpeg') {
|
|
$(document.body).css('backgroundColor', 'white');
|
|
}
|
|
|
|
$container = $('<div>').appendTo(document.body);
|
|
$container.attr('id', 'container');
|
|
|
|
// disable animations
|
|
Highcharts.SVGRenderer.prototype.Element.prototype.animate = Highcharts.SVGRenderer.prototype.Element.prototype.attr;
|
|
Highcharts.setOptions({
|
|
plotOptions: {
|
|
series: {
|
|
animation: false
|
|
}
|
|
}
|
|
});
|
|
|
|
if (!options.chart) {
|
|
options.chart = {};
|
|
}
|
|
|
|
options.chart.renderTo = $container[0];
|
|
|
|
// check if witdh is set. Order of precedence:
|
|
// args.width, options.chart.width and 600px
|
|
|
|
// OLD. options.chart.width = width || options.chart.width || 600;
|
|
// Notice we don't use commandline parameter width here. Commandline parameter width is used for scaling.
|
|
|
|
options.chart.width = (options.exporting && options.exporting.sourceWidth) || options.chart.width || 600;
|
|
options.chart.height = (options.exporting && options.exporting.sourceHeight) || options.chart.height || 400;
|
|
|
|
// Load globalOptions
|
|
if (globalOptions) {
|
|
Highcharts.setOptions(globalOptions);
|
|
}
|
|
|
|
// Load data
|
|
if (dataOptions) {
|
|
parseData(function completeHandler(opts) {
|
|
// Merge series configs
|
|
if (options.series) {
|
|
Highcharts.each(options.series, function (series, i) {
|
|
options.series[i] = Highcharts.merge(series, opts.series[i]);
|
|
});
|
|
}
|
|
|
|
var mergedOptions = Highcharts.merge(opts, options);
|
|
|
|
// Run customCode
|
|
if (customCode) {
|
|
customCode(mergedOptions);
|
|
}
|
|
|
|
chart = new Highcharts[constr](mergedOptions, cb);
|
|
|
|
}, options, dataOptions);
|
|
} else {
|
|
chart = new Highcharts[constr](options, cb);
|
|
}
|
|
|
|
/* remove stroke-opacity paths, used by mouse-trackers, they turn up as
|
|
* as fully opaque in the PDF
|
|
*/
|
|
nodes = document.querySelectorAll('*[stroke-opacity]');
|
|
|
|
for (nodeIter = 0; nodeIter < nodes.length; nodeIter += 1) {
|
|
elem = nodes[nodeIter];
|
|
opacity = elem.getAttribute('stroke-opacity');
|
|
elem.removeAttribute('stroke-opacity');
|
|
elem.setAttribute('opacity', opacity);
|
|
}
|
|
|
|
imgs = document.getElementsByTagName('image');
|
|
imgUrls = [];
|
|
|
|
for (imgIndex = 0; imgIndex < imgs.length; imgIndex = imgIndex + 1) {
|
|
imgUrls.push(imgs[imgIndex].href.baseVal);
|
|
}
|
|
|
|
return {
|
|
html: $('div.highcharts-container')[0].innerHTML,
|
|
width: chart.chartWidth,
|
|
height: chart.chartHeight,
|
|
imgUrls: imgUrls
|
|
};
|
|
};
|
|
|
|
if (params.length < 1) {
|
|
exit("Error: Insufficient parameters");
|
|
} else {
|
|
input = params.infile;
|
|
output = params.outfile;
|
|
|
|
if (output !== undefined) {
|
|
outType = pick(output.split('.').pop(),'png');
|
|
} else {
|
|
outType = pick(params.type,'png');
|
|
}
|
|
|
|
constr = pick(params.constr, 'Chart');
|
|
callback = params.callback;
|
|
width = params.width;
|
|
|
|
if (input === undefined || input.length === 0) {
|
|
exit('Error: Insuficient or wrong parameters for rendering');
|
|
}
|
|
|
|
page.open('about:blank', function (status) {
|
|
var svg,
|
|
globalOptions = params.globaloptions,
|
|
dataOptions = params.dataoptions,
|
|
customCode = 'function customCode(options) {\n' + params.customcode + '}\n',
|
|
jsFile,
|
|
jsFiles;
|
|
|
|
/* Decide if we have to generate a svg first before rendering */
|
|
if (input.substring(0, 4).toLowerCase() === "<svg" || input.substring(0, 5).toLowerCase() === "<?xml"
|
|
|| input.substring(0, 9).toLowerCase() === "<!doctype") {
|
|
//render page directly from svg file
|
|
svg = page.evaluate(loadChart, input, outType);
|
|
page.viewportSize = { width: svg.width, height: svg.height };
|
|
renderSVG(svg);
|
|
} else {
|
|
/**
|
|
* We have a js file, let's render serverside from Highcharts options and grab the svg from it
|
|
*/
|
|
|
|
// load our javascript dependencies based on the constructor
|
|
if (constr === 'Map') {
|
|
jsFiles = config.files.highmaps;
|
|
} else if (constr === 'StockChart')
|
|
jsFiles = config.files.highstock;
|
|
else {
|
|
jsFiles = config.files.highcharts;
|
|
}
|
|
|
|
// load necessary libraries
|
|
for (jsFile in jsFiles) {
|
|
if (jsFiles.hasOwnProperty(jsFile)) {
|
|
page.injectJs(jsFiles[jsFile]);
|
|
}
|
|
}
|
|
|
|
// load chart in page and return svg height and width
|
|
svg = page.evaluate(createChart, constr, input, globalOptions, dataOptions, customCode, outType, callback, messages);
|
|
|
|
if (!window.optionsParsed) {
|
|
exit('ERROR: the options variable was not available or couldn\'t be parsed, does the infile contain an syntax error? Input used:' + input);
|
|
}
|
|
|
|
if (callback !== undefined && !window.callbackParsed) {
|
|
exit('ERROR: the callback variable was not available, does the callback contain an syntax error? Callback used: ' + callback);
|
|
}
|
|
renderSVG(svg);
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
startServer = function (host, port) {
|
|
var server = require('webserver').create();
|
|
|
|
server.listen(host + ':' + port,
|
|
function (request, response) {
|
|
var jsonStr = request.postRaw || request.post,
|
|
params,
|
|
msg;
|
|
try {
|
|
params = JSON.parse(jsonStr);
|
|
if (params.status) {
|
|
// for server health validation
|
|
response.statusCode = 200;
|
|
response.write('OK');
|
|
response.close();
|
|
} else {
|
|
render(params, function (result) {
|
|
response.statusCode = 200;
|
|
response.write(result);
|
|
response.close();
|
|
});
|
|
}
|
|
} catch (e) {
|
|
msg = "Failed rendering: \n" + e;
|
|
response.statusCode = 500;
|
|
response.setHeader('Content-Type', 'text/plain');
|
|
response.setHeader('Content-Length', msg.length);
|
|
response.write(msg);
|
|
response.close();
|
|
}
|
|
}); // end server.listen
|
|
|
|
// switch to serverMode
|
|
serverMode = true;
|
|
|
|
console.log("OK, PhantomJS is ready.");
|
|
};
|
|
|
|
args = mapCLArguments();
|
|
|
|
// set tmpDir, for output temporary files.
|
|
if (args.tmpdir === undefined) {
|
|
config.tmpDir = fs.workingDirectory + '/tmp';
|
|
} else {
|
|
config.tmpDir = args.tmpdir;
|
|
}
|
|
|
|
// exists tmpDir and is it writable?
|
|
if (!fs.exists(config.tmpDir)) {
|
|
try{
|
|
fs.makeDirectory(config.tmpDir);
|
|
} catch (e) {
|
|
console.log('ERROR: Cannot create temp directory for ' + config.tmpDir);
|
|
}
|
|
}
|
|
|
|
|
|
if (args.host !== undefined && args.port !== undefined) {
|
|
startServer(args.host, args.port);
|
|
} else {
|
|
// presume commandline usage
|
|
render(args, function (msg) {
|
|
console.log(msg);
|
|
phantom.exit();
|
|
});
|
|
}
|
|
}());
|