Subversion-Projekte lars-tiefland.ci

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

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