You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

620 lines
18 KiB

9 years ago
  1. /**
  2. * @license Highcharts JS v3.0.1 (2012-11-02)
  3. *
  4. * (c) 20013-2014
  5. *
  6. * Author: Gert Vaartjes
  7. *
  8. * License: www.highcharts.com/license
  9. *
  10. * version: 2.0.1
  11. */
  12. /*jslint white: true */
  13. /*global window, require, phantom, console, $, document, Image, Highcharts, clearTimeout, clearInterval, options, cb, globalOptions, dataOptions, customCode */
  14. (function () {
  15. "use strict";
  16. var config = {
  17. /* define locations of mandatory javascript files.
  18. * Depending on purchased license change the HIGHCHARTS property to
  19. * highcharts.js or highstock.js
  20. */
  21. files: {
  22. highcharts: {
  23. JQUERY: 'jquery.1.9.1.min.js',
  24. HIGHCHARTS: 'highcharts.js',
  25. HIGHCHARTS_MORE: 'highcharts-more.js',
  26. HIGHCHARTS_DATA: 'data.js',
  27. HIGHCHARTS_DRILLDOWN: 'drilldown.js',
  28. HIGHCHARTS_FUNNEL: 'funnel.js',
  29. HIGHCHARTS_HEATMAP: 'heatmap.js',
  30. HIGHCHARTS_TREEMAP: 'treemap.js',
  31. HIGHCHARTS_3D: 'highcharts-3d.js',
  32. HIGHCHARTS_NODATA: 'no-data-to-display.js',
  33. // Uncomment below if you have both Highcharts and Highmaps license
  34. // HIGHCHARTS_MAP: 'map.js',
  35. HIGHCHARTS_SOLID_GAUGE: 'solid-gauge.js',
  36. BROKEN_AXIS: 'broken-axis.js'
  37. },
  38. highstock: {
  39. JQUERY: 'jquery.1.9.1.min.js',
  40. HIGHCHARTS: 'highcharts.js',
  41. HIGHCHARTS_MORE: 'highcharts-more.js',
  42. HIGHCHARTS_DATA: 'data.js',
  43. HIGHCHARTS_DRILLDOWN: 'drilldown.js',
  44. HIGHCHARTS_FUNNEL: 'funnel.js',
  45. HIGHCHARTS_HEATMAP: 'heatmap.js',
  46. HIGHCHARTS_TREEMAP: 'treemap.js',
  47. HIGHCHARTS_3D: 'highcharts-3d.js',
  48. HIGHCHARTS_NODATA: 'no-data-to-display.js',
  49. // Uncomment below if you have both Highstock and Highmaps license
  50. // HIGHCHARTS_MAP: 'map.js',
  51. HIGHCHARTS_SOLID_GAUGE: 'solid-gauge.js',
  52. BROKEN_AXIS: 'broken-axis.js'
  53. },
  54. highmaps: {
  55. JQUERY: 'jquery.1.9.1.min.js',
  56. HIGHCHARTS: 'highmaps.js',
  57. HIGHCHARTS_DATA: 'data.js',
  58. HIGHCHARTS_DRILLDOWN: 'drilldown.js',
  59. HIGHCHARTS_HEATMAP: 'heatmap.js',
  60. HIGHCHARTS_NODATA: 'no-data-to-display.js'
  61. }
  62. },
  63. TIMEOUT: 5000 /* 5 seconds timout for loading images */
  64. },
  65. mapCLArguments,
  66. render,
  67. startServer = false,
  68. args,
  69. pick,
  70. 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\">',
  71. dpiCorrection = 1.4,
  72. system = require('system'),
  73. fs = require('fs'),
  74. serverMode = false;
  75. pick = function () {
  76. var args = arguments, i, arg, length = args.length;
  77. for (i = 0; i < length; i += 1) {
  78. arg = args[i];
  79. if (arg !== undefined && arg !== null && arg !== 'null' && arg != '0') {
  80. return arg;
  81. }
  82. }
  83. };
  84. mapCLArguments = function () {
  85. var map = {},
  86. i,
  87. key;
  88. if (system.args.length < 1) {
  89. console.log('Commandline Usage: highcharts-convert.js -infile URL -outfile filename -scale 2.5 -width 300 -constr Chart -callback callback.js');
  90. console.log(', or run PhantomJS as server: highcharts-convert.js -host 127.0.0.1 -port 1234');
  91. }
  92. for (i = 0; i < system.args.length; i += 1) {
  93. if (system.args[i].charAt(0) === '-') {
  94. key = system.args[i].substr(1, i.length);
  95. if (key === 'infile' || key === 'callback' || key === 'dataoptions' || key === 'globaloptions' || key === 'customcode') {
  96. // get string from file
  97. try {
  98. map[key] = fs.read(system.args[i + 1]).replace(/^\s+/, '');
  99. } catch (e) {
  100. console.log('Error: cannot find file, ' + system.args[i + 1]);
  101. phantom.exit();
  102. }
  103. } else {
  104. map[key] = system.args[i + 1];
  105. }
  106. }
  107. }
  108. return map;
  109. };
  110. render = function (params, exitCallback) {
  111. var page = require('webpage').create(),
  112. messages = {},
  113. scaleAndClipPage,
  114. loadChart,
  115. createChart,
  116. input,
  117. constr,
  118. callback,
  119. width,
  120. output,
  121. outType,
  122. timer,
  123. renderSVG,
  124. convert,
  125. exit,
  126. interval,
  127. counter,
  128. imagesLoaded = false;
  129. messages.optionsParsed = 'Highcharts.options.parsed';
  130. messages.callbackParsed = 'Highcharts.cb.parsed';
  131. window.optionsParsed = false;
  132. window.callbackParsed = false;
  133. page.onConsoleMessage = function (msg) {
  134. console.log(msg);
  135. /*
  136. * Ugly hack, but only way to get messages out of the 'page.evaluate()'
  137. * sandbox. If any, please contribute with improvements on this!
  138. */
  139. /* to check options or callback are properly parsed */
  140. if (msg === messages.optionsParsed) {
  141. window.optionsParsed = true;
  142. }
  143. if (msg === messages.callbackParsed) {
  144. window.callbackParsed = true;
  145. }
  146. };
  147. page.onAlert = function (msg) {
  148. console.log(msg);
  149. };
  150. /* scale and clip the page */
  151. scaleAndClipPage = function (svg) {
  152. /* param: svg: The scg configuration object
  153. */
  154. var zoom = 1,
  155. pageWidth = pick(params.width, svg.width),
  156. clipwidth,
  157. clipheight;
  158. if (parseInt(pageWidth, 10) == pageWidth) {
  159. zoom = pageWidth / svg.width;
  160. }
  161. /* set this line when scale factor has a higher precedence
  162. scale has precedence : page.zoomFactor = params.scale ? zoom * params.scale : zoom;*/
  163. /* params.width has a higher precedence over scaling, to not break backover compatibility */
  164. page.zoomFactor = params.scale && params.width == undefined ? zoom * params.scale : zoom;
  165. clipwidth = svg.width * page.zoomFactor;
  166. clipheight = svg.height * page.zoomFactor;
  167. /* define the clip-rectangle */
  168. /* ignored for PDF, see https://github.com/ariya/phantomjs/issues/10465 */
  169. page.clipRect = {
  170. top: 0,
  171. left: 0,
  172. width: clipwidth,
  173. height: clipheight
  174. };
  175. /* for pdf we need a bit more paperspace in some cases for example (w:600,h:400), I don't know why.*/
  176. if (outType === 'pdf') {
  177. // changed to a multiplication with 1.333 to correct systems dpi setting
  178. clipwidth = clipwidth * dpiCorrection;
  179. clipheight = clipheight * dpiCorrection;
  180. // redefine the viewport
  181. page.viewportSize = { width: clipwidth, height: clipheight};
  182. // make the paper a bit larger than the viewport
  183. page.paperSize = { width: clipwidth + 2 , height: clipheight + 2 };
  184. }
  185. };
  186. exit = function (result) {
  187. if (serverMode) {
  188. //Calling page.close(), may stop the increasing heap allocation
  189. page.close();
  190. }
  191. exitCallback(result);
  192. };
  193. convert = function (svg) {
  194. var base64;
  195. scaleAndClipPage(svg);
  196. if (outType === 'pdf' || output !== undefined || !serverMode) {
  197. if (output === undefined) {
  198. // in case of pdf files
  199. output = config.tmpDir + '/chart.' + outType;
  200. }
  201. page.render(output);
  202. exit(output);
  203. } else {
  204. base64 = page.renderBase64(outType);
  205. exit(base64);
  206. }
  207. };
  208. function decrementImgCounter() {
  209. counter -= 1;
  210. if (counter < 1) {
  211. imagesLoaded = true;
  212. }
  213. }
  214. function loadImages(imgUrls) {
  215. var i, img;
  216. counter = imgUrls.length;
  217. for (i = 0; i < imgUrls.length; i += 1) {
  218. img = new Image();
  219. /* onload decrements the counter, also when error (perhaps 404), don't wait for this image to be loaded */
  220. img.onload = img.onerror = decrementImgCounter;
  221. /* force loading of images by setting the src attr.*/
  222. img.src = imgUrls[i];
  223. }
  224. }
  225. renderSVG = function (svg) {
  226. var svgFile;
  227. // From this point we have 'loaded' or 'created' a SVG
  228. // Do we have to load images?
  229. if (svg.imgUrls.length > 0) {
  230. loadImages(svg.imgUrls);
  231. } else {
  232. // no images present, no loading, no waiting
  233. imagesLoaded = true;
  234. }
  235. try {
  236. if (outType.toLowerCase() === 'svg') {
  237. // output svg
  238. svg = svg.html.replace(/<svg /, '<svg xmlns:xlink="http://www.w3.org/1999/xlink" ').replace(/ href=/g, ' xlink:href=').replace(/<\/svg>.*?$/, '</svg>');
  239. // add xml doc type
  240. svg = SVG_DOCTYPE + svg;
  241. if (output !== undefined) {
  242. // write the file
  243. svgFile = fs.open(output, "w");
  244. svgFile.write(svg);
  245. svgFile.close();
  246. exit(output);
  247. } else {
  248. // return the svg as a string
  249. exit(svg);
  250. }
  251. } else {
  252. // output binary images or pdf
  253. if (!imagesLoaded) {
  254. // render with interval, waiting for all images loaded
  255. interval = window.setInterval(function () {
  256. if (imagesLoaded) {
  257. clearTimeout(timer);
  258. clearInterval(interval);
  259. convert(svg);
  260. }
  261. }, 50);
  262. // we have a 5 second timeframe..
  263. timer = window.setTimeout(function () {
  264. clearInterval(interval);
  265. exitCallback('ERROR: While rendering, there\'s is a timeout reached');
  266. }, config.TIMEOUT);
  267. } else {
  268. // images are loaded, render rightaway
  269. convert(svg);
  270. }
  271. }
  272. } catch (e) {
  273. console.log('ERROR: While rendering, ' + e);
  274. }
  275. };
  276. loadChart = function (input, outputType) {
  277. var nodeIter, nodes, elem, opacity, svgElem, imgs, imgUrls, imgIndex;
  278. document.body.style.margin = '0px';
  279. document.body.innerHTML = input;
  280. if (outputType === 'jpeg') {
  281. document.body.style.backgroundColor = 'white';
  282. }
  283. nodes = document.querySelectorAll('*[stroke-opacity]');
  284. for (nodeIter = 0; nodeIter < nodes.length; nodeIter += 1) {
  285. elem = nodes[nodeIter];
  286. opacity = elem.getAttribute('stroke-opacity');
  287. elem.removeAttribute('stroke-opacity');
  288. elem.setAttribute('opacity', opacity);
  289. }
  290. svgElem = document.getElementsByTagName('svg')[0];
  291. imgs = document.getElementsByTagName('image');
  292. imgUrls = [];
  293. for (imgIndex = 0; imgIndex < imgs.length; imgIndex = imgIndex + 1) {
  294. imgUrls.push(imgs[imgIndex].href.baseVal);
  295. }
  296. return {
  297. html: document.body.innerHTML,
  298. width: svgElem.getAttribute("width"),
  299. height: svgElem.getAttribute("height"),
  300. imgUrls: imgUrls
  301. };
  302. };
  303. createChart = function (constr, input, globalOptionsArg, dataOptionsArg, customCodeArg, outputType, callback, messages) {
  304. var $container, chart, nodes, nodeIter, elem, opacity, imgIndex, imgs, imgUrls;
  305. // dynamic script insertion
  306. function loadScript(varStr, codeStr) {
  307. var $script = $('<script>').attr('type', 'text/javascript');
  308. $script.html('var ' + varStr + ' = ' + codeStr);
  309. document.getElementsByTagName("head")[0].appendChild($script[0]);
  310. if (window[varStr] !== undefined) {
  311. console.log('Highcharts.' + varStr + '.parsed');
  312. }
  313. }
  314. function parseData(completeHandler, chartOptions, dataConfig) {
  315. try {
  316. dataConfig.complete = completeHandler;
  317. Highcharts.data(dataConfig, chartOptions);
  318. } catch (error) {
  319. completeHandler(undefined);
  320. }
  321. }
  322. if (input !== 'undefined') {
  323. loadScript('options', input);
  324. }
  325. if (callback !== 'undefined') {
  326. loadScript('cb', callback);
  327. }
  328. if (globalOptionsArg !== 'undefined') {
  329. loadScript('globalOptions', globalOptionsArg);
  330. }
  331. if (dataOptionsArg !== 'undefined') {
  332. loadScript('dataOptions', dataOptionsArg);
  333. }
  334. if (customCodeArg !== 'undefined') {
  335. loadScript('customCode', customCodeArg);
  336. }
  337. $(document.body).css('margin', '0px');
  338. if (outputType === 'jpeg') {
  339. $(document.body).css('backgroundColor', 'white');
  340. }
  341. $container = $('<div>').appendTo(document.body);
  342. $container.attr('id', 'container');
  343. // disable animations
  344. Highcharts.SVGRenderer.prototype.Element.prototype.animate = Highcharts.SVGRenderer.prototype.Element.prototype.attr;
  345. Highcharts.setOptions({
  346. plotOptions: {
  347. series: {
  348. animation: false
  349. }
  350. }
  351. });
  352. if (!options.chart) {
  353. options.chart = {};
  354. }
  355. options.chart.renderTo = $container[0];
  356. // check if witdh is set. Order of precedence:
  357. // args.width, options.chart.width and 600px
  358. // OLD. options.chart.width = width || options.chart.width || 600;
  359. // Notice we don't use commandline parameter width here. Commandline parameter width is used for scaling.
  360. options.chart.width = (options.exporting && options.exporting.sourceWidth) || options.chart.width || 600;
  361. options.chart.height = (options.exporting && options.exporting.sourceHeight) || options.chart.height || 400;
  362. // Load globalOptions
  363. if (globalOptions) {
  364. Highcharts.setOptions(globalOptions);
  365. }
  366. // Load data
  367. if (dataOptions) {
  368. parseData(function completeHandler(opts) {
  369. // Merge series configs
  370. if (options.series) {
  371. Highcharts.each(options.series, function (series, i) {
  372. options.series[i] = Highcharts.merge(series, opts.series[i]);
  373. });
  374. }
  375. var mergedOptions = Highcharts.merge(opts, options);
  376. // Run customCode
  377. if (customCode) {
  378. customCode(mergedOptions);
  379. }
  380. chart = new Highcharts[constr](mergedOptions, cb);
  381. }, options, dataOptions);
  382. } else {
  383. chart = new Highcharts[constr](options, cb);
  384. }
  385. /* remove stroke-opacity paths, used by mouse-trackers, they turn up as
  386. * as fully opaque in the PDF
  387. */
  388. nodes = document.querySelectorAll('*[stroke-opacity]');
  389. for (nodeIter = 0; nodeIter < nodes.length; nodeIter += 1) {
  390. elem = nodes[nodeIter];
  391. opacity = elem.getAttribute('stroke-opacity');
  392. elem.removeAttribute('stroke-opacity');
  393. elem.setAttribute('opacity', opacity);
  394. }
  395. imgs = document.getElementsByTagName('image');
  396. imgUrls = [];
  397. for (imgIndex = 0; imgIndex < imgs.length; imgIndex = imgIndex + 1) {
  398. imgUrls.push(imgs[imgIndex].href.baseVal);
  399. }
  400. return {
  401. html: $('div.highcharts-container')[0].innerHTML,
  402. width: chart.chartWidth,
  403. height: chart.chartHeight,
  404. imgUrls: imgUrls
  405. };
  406. };
  407. if (params.length < 1) {
  408. exit("Error: Insufficient parameters");
  409. } else {
  410. input = params.infile;
  411. output = params.outfile;
  412. if (output !== undefined) {
  413. outType = pick(output.split('.').pop(),'png');
  414. } else {
  415. outType = pick(params.type,'png');
  416. }
  417. constr = pick(params.constr, 'Chart');
  418. callback = params.callback;
  419. width = params.width;
  420. if (input === undefined || input.length === 0) {
  421. exit('Error: Insuficient or wrong parameters for rendering');
  422. }
  423. page.open('about:blank', function (status) {
  424. var svg,
  425. globalOptions = params.globaloptions,
  426. dataOptions = params.dataoptions,
  427. customCode = 'function customCode(options) {\n' + params.customcode + '}\n',
  428. jsFile,
  429. jsFiles;
  430. /* Decide if we have to generate a svg first before rendering */
  431. if (input.substring(0, 4).toLowerCase() === "<svg" || input.substring(0, 5).toLowerCase() === "<?xml"
  432. || input.substring(0, 9).toLowerCase() === "<!doctype") {
  433. //render page directly from svg file
  434. svg = page.evaluate(loadChart, input, outType);
  435. page.viewportSize = { width: svg.width, height: svg.height };
  436. renderSVG(svg);
  437. } else {
  438. /**
  439. * We have a js file, let's render serverside from Highcharts options and grab the svg from it
  440. */
  441. // load our javascript dependencies based on the constructor
  442. if (constr === 'Map') {
  443. jsFiles = config.files.highmaps;
  444. } else if (constr === 'StockChart')
  445. jsFiles = config.files.highstock;
  446. else {
  447. jsFiles = config.files.highcharts;
  448. }
  449. // load necessary libraries
  450. for (jsFile in jsFiles) {
  451. if (jsFiles.hasOwnProperty(jsFile)) {
  452. page.injectJs(jsFiles[jsFile]);
  453. }
  454. }
  455. // load chart in page and return svg height and width
  456. svg = page.evaluate(createChart, constr, input, globalOptions, dataOptions, customCode, outType, callback, messages);
  457. if (!window.optionsParsed) {
  458. exit('ERROR: the options variable was not available or couldn\'t be parsed, does the infile contain an syntax error? Input used:' + input);
  459. }
  460. if (callback !== undefined && !window.callbackParsed) {
  461. exit('ERROR: the callback variable was not available, does the callback contain an syntax error? Callback used: ' + callback);
  462. }
  463. renderSVG(svg);
  464. }
  465. });
  466. }
  467. };
  468. startServer = function (host, port) {
  469. var server = require('webserver').create();
  470. server.listen(host + ':' + port,
  471. function (request, response) {
  472. var jsonStr = request.postRaw || request.post,
  473. params,
  474. msg;
  475. try {
  476. params = JSON.parse(jsonStr);
  477. if (params.status) {
  478. // for server health validation
  479. response.statusCode = 200;
  480. response.write('OK');
  481. response.close();
  482. } else {
  483. render(params, function (result) {
  484. response.statusCode = 200;
  485. response.write(result);
  486. response.close();
  487. });
  488. }
  489. } catch (e) {
  490. msg = "Failed rendering: \n" + e;
  491. response.statusCode = 500;
  492. response.setHeader('Content-Type', 'text/plain');
  493. response.setHeader('Content-Length', msg.length);
  494. response.write(msg);
  495. response.close();
  496. }
  497. }); // end server.listen
  498. // switch to serverMode
  499. serverMode = true;
  500. console.log("OK, PhantomJS is ready.");
  501. };
  502. args = mapCLArguments();
  503. // set tmpDir, for output temporary files.
  504. if (args.tmpdir === undefined) {
  505. config.tmpDir = fs.workingDirectory + '/tmp';
  506. } else {
  507. config.tmpDir = args.tmpdir;
  508. }
  509. // exists tmpDir and is it writable?
  510. if (!fs.exists(config.tmpDir)) {
  511. try{
  512. fs.makeDirectory(config.tmpDir);
  513. } catch (e) {
  514. console.log('ERROR: Cannot create temp directory for ' + config.tmpDir);
  515. }
  516. }
  517. if (args.host !== undefined && args.port !== undefined) {
  518. startServer(args.host, args.port);
  519. } else {
  520. // presume commandline usage
  521. render(args, function (msg) {
  522. console.log(msg);
  523. phantom.exit();
  524. });
  525. }
  526. }());