1/*
2Plugin Name: amCharts Export
3Description: Adds export capabilities to amCharts products
4Author: Benjamin Maertz, amCharts
5Version: 1.4.13
6Author URI: http://www.amcharts.com/
7
8Copyright 2015 amCharts
9
10Licensed under the Apache License, Version 2.0 (the "License");
11you may not use this file except in compliance with the License.
12You may obtain a copy of the License at
13
14	http://www.apache.org/licenses/LICENSE-2.0
15
16Unless required by applicable law or agreed to in writing, software
17distributed under the License is distributed on an "AS IS" BASIS,
18WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19See the License for the specific language governing permissions and
20limitations under the License.
21
22Please note that the above license covers only this plugin. It by all means does
23not apply to any other amCharts products that are covered by different licenses.
24*/
25
26/*
27 ** Polyfill translation
28 */
29if ( !AmCharts.translations[ "export" ] ) {
30	AmCharts.translations[ "export" ] = {}
31}
32if ( !AmCharts.translations[ "export" ][ "en" ] ) {
33	AmCharts.translations[ "export" ][ "en" ] = {
34		"fallback.save.text": "CTRL + C to copy the data into the clipboard.",
35		"fallback.save.image": "Rightclick -> Save picture as... to save the image.",
36
37		"capturing.delayed.menu.label": "{{duration}}",
38		"capturing.delayed.menu.title": "Click to cancel",
39
40		"menu.label.print": "Print",
41		"menu.label.undo": "Undo",
42		"menu.label.redo": "Redo",
43		"menu.label.cancel": "Cancel",
44
45		"menu.label.save.image": "Download as ...",
46		"menu.label.save.data": "Save as ...",
47
48		"menu.label.draw": "Annotate ...",
49		"menu.label.draw.change": "Change ...",
50		"menu.label.draw.add": "Add ...",
51		"menu.label.draw.shapes": "Shape ...",
52		"menu.label.draw.colors": "Color ...",
53		"menu.label.draw.widths": "Size ...",
54		"menu.label.draw.opacities": "Opacity ...",
55		"menu.label.draw.text": "Text",
56
57		"menu.label.draw.modes": "Mode ...",
58		"menu.label.draw.modes.pencil": "Pencil",
59		"menu.label.draw.modes.line": "Line",
60		"menu.label.draw.modes.arrow": "Arrow"
61	}
62}
63
64/*
65 ** Polyfill export class
66 */
67( function() {
68	AmCharts[ "export" ] = function( chart, config ) {
69		var _this = {
70			name: "export",
71			version: "1.4.13",
72			libs: {
73				async: true,
74				autoLoad: true,
75				reload: false,
76				resources: [ {
77					"pdfmake/pdfmake.js": [ "pdfmake/vfs_fonts.js" ],
78					"jszip/jszip.js": [ "xlsx/xlsx.js" ]
79				}, "fabric.js/fabric.js", "FileSaver.js/FileSaver.js" ],
80				namespaces: {
81					"pdfmake.js": "pdfMake",
82					"jszip.js": "JSZip",
83					"xlsx.js": "XLSX",
84					"fabric.js": "fabric",
85					"FileSaver.js": "saveAs"
86				}
87			},
88			config: {},
89			setup: {
90				chart: chart,
91				hasBlob: false,
92				wrapper: false
93			},
94			drawing: {
95				enabled: false,
96				undos: [],
97				redos: [],
98				buffer: {
99					position: {
100						x1: 0,
101						y1: 0,
102						x2: 0,
103						y2: 0,
104						xD: 0,
105						yD: 0
106					}
107				},
108				handler: {
109					undo: function( options, skipped ) {
110						var item = _this.drawing.undos.pop();
111						if ( item ) {
112							item.selectable = true;
113							_this.drawing.redos.push( item );
114
115							if ( item.action == "added" ) {
116								_this.setup.fabric.remove( item.target );
117							}
118
119							var state = JSON.parse( item.state );
120							item.target.set( state );
121
122							if ( item.target instanceof fabric.Group ) {
123								_this.drawing.handler.change( {
124									color: state.cfg.color,
125									width: state.cfg.width,
126									opacity: state.cfg.opacity
127								}, true, item.target );
128							}
129
130							_this.setup.fabric.renderAll();
131
132							// RECALL
133							if ( item.state == item.target.recentState && !skipped ) {
134								_this.drawing.handler.undo( item, true );
135							}
136						}
137					},
138					redo: function( options, skipped ) {
139						var item = _this.drawing.redos.pop();
140						if ( item ) {
141							item.selectable = true;
142							_this.drawing.undos.push( item );
143
144							if ( item.action == "added" ) {
145								_this.setup.fabric.add( item.target );
146							}
147
148							var state = JSON.parse( item.state );
149							item.target.recentState = item.state;
150							item.target.set( state );
151
152							if ( item.target instanceof fabric.Group ) {
153								_this.drawing.handler.change( {
154									color: state.cfg.color,
155									width: state.cfg.width,
156									opacity: state.cfg.opacity
157								}, true, item.target );
158							}
159
160							_this.setup.fabric.renderAll();
161
162							// RECALL
163							if ( item.action == "addified" ) {
164								_this.drawing.handler.redo();
165							}
166						}
167					},
168					done: function( options ) {
169						_this.drawing.buffer.enabled = false;
170						_this.drawing.undos = [];
171						_this.drawing.redos = [];
172						_this.createMenu( _this.config.menu );
173						_this.setup.fabric.deactivateAll();
174
175						if ( _this.setup.wrapper ) {
176							_this.setup.chart.containerDiv.removeChild( _this.setup.wrapper );
177							_this.setup.wrapper = false;
178						}
179					},
180					add: function( options ) {
181						var cfg = _this.deepMerge( {
182							top: _this.setup.fabric.height / 2,
183							left: _this.setup.fabric.width / 2
184						}, options || {} );
185						var method = cfg.url.indexOf( ".svg" ) != -1 ? fabric.loadSVGFromURL : fabric.Image.fromURL;
186
187						method( cfg.url, function( objects, options ) {
188							var group = options !== undefined ? fabric.util.groupSVGElements( objects, options ) : objects;
189							var ratio = false;
190
191							// RESCALE ONLY IF IT EXCEEDS THE CANVAS
192							if ( group.height > _this.setup.fabric.height || group.width > _this.setup.fabric.width ) {
193								ratio = ( _this.setup.fabric.height / 2 ) / group.height;
194							}
195
196							if ( cfg.top > _this.setup.fabric.height ) {
197								cfg.top = _this.setup.fabric.height / 2;
198							}
199
200							if ( cfg.left > _this.setup.fabric.width ) {
201								cfg.left = _this.setup.fabric.width / 2;
202							}
203
204							group.set( {
205								originX: "center",
206								originY: "center",
207								top: cfg.top,
208								left: cfg.left,
209								width: ratio ? group.width * ratio : group.width,
210								height: ratio ? group.height * ratio : group.height,
211								fill: _this.drawing.color
212							} );
213							_this.setup.fabric.add( group );
214						} );
215					},
216					change: function( options, skipped, target ) {
217						var cfg = _this.deepMerge( {}, options || {} );
218						var state, i1, rgba;
219						var current = target || _this.drawing.buffer.target;
220						var objects = current ? current._objects ? current._objects : [ current ] : null;
221
222						// UPDATE DRAWING OBJECT
223						if ( cfg.mode ) {
224							_this.drawing.mode = cfg.mode;
225						}
226						if ( cfg.width ) {
227							_this.drawing.width = cfg.width;
228							_this.drawing.fontSize = cfg.width * 3;
229						}
230						if ( cfg.fontSize ) {
231							_this.drawing.fontSize = cfg.fontSize;
232						}
233						if ( cfg.color ) {
234							_this.drawing.color = cfg.color;
235						}
236						if ( cfg.opacity ) {
237							_this.drawing.opacity = cfg.opacity;
238						}
239
240						// APPLY OPACITY ON CURRENT COLOR
241						rgba = new fabric.Color( _this.drawing.color ).getSource();
242						rgba.pop();
243						rgba.push( _this.drawing.opacity );
244						_this.drawing.color = "rgba(" + rgba.join() + ")";
245						_this.setup.fabric.freeDrawingBrush.color = _this.drawing.color;
246						_this.setup.fabric.freeDrawingBrush.width = _this.drawing.width;
247
248						// UPDATE CURRENT SELECTION
249						if ( current ) {
250							state = JSON.parse( current.recentState ).cfg;
251
252							// UPDATE GIVE OPTIONS ONLY
253							if ( state ) {
254								cfg.color = cfg.color || state.color;
255								cfg.width = cfg.width || state.width;
256								cfg.opacity = cfg.opacity || state.opacity;
257								cfg.fontSize = cfg.fontSize || cfg.width * 3;
258
259								rgba = new fabric.Color( cfg.color ).getSource();
260								rgba.pop();
261								rgba.push( cfg.opacity );
262								cfg.color = "rgba(" + rgba.join() + ")";
263							}
264
265							// UPDATE OBJECTS
266							for ( i1 = 0; i1 < objects.length; i1++ ) {
267								if (
268									objects[ i1 ] instanceof fabric.Text ||
269									objects[ i1 ] instanceof fabric.PathGroup ||
270									objects[ i1 ] instanceof fabric.Triangle
271								) {
272									if ( cfg.color || cfg.opacity ) {
273										objects[ i1 ].set( {
274											fill: cfg.color
275										} );
276									}
277									if ( cfg.fontSize ) {
278										objects[ i1 ].set( {
279											fontSize: cfg.fontSize
280										} );
281									}
282								} else if (
283									objects[ i1 ] instanceof fabric.Path ||
284									objects[ i1 ] instanceof fabric.Line
285								) {
286									if ( current instanceof fabric.Group ) {
287										if ( cfg.color || cfg.opacity ) {
288											objects[ i1 ].set( {
289												stroke: cfg.color
290											} );
291										}
292									} else {
293										if ( cfg.color || cfg.opacity ) {
294											objects[ i1 ].set( {
295												stroke: cfg.color
296											} );
297										}
298										if ( cfg.width ) {
299											objects[ i1 ].set( {
300												strokeWidth: cfg.width
301											} );
302										}
303									}
304								}
305							}
306
307							// ADD UNDO
308							if ( !skipped ) {
309								state = JSON.stringify( _this.deepMerge( current.saveState().originalState, {
310									cfg: {
311										color: cfg.color,
312										width: cfg.width,
313										opacity: cfg.opacity
314									}
315								} ) );
316								current.recentState = state;
317								_this.drawing.redos = [];
318								_this.drawing.undos.push( {
319									action: "modified",
320									target: current,
321									state: state
322								} );
323							}
324
325							_this.setup.fabric.renderAll();
326						}
327					},
328					text: function( options ) {
329						var cfg = _this.deepMerge( {
330							text: _this.i18l( "menu.label.draw.text" ),
331							top: _this.setup.fabric.height / 2,
332							left: _this.setup.fabric.width / 2,
333							fontSize: _this.drawing.fontSize,
334							fontFamily: _this.setup.chart.fontFamily || "Verdana",
335							fill: _this.drawing.color
336						}, options || {} );
337
338						cfg.click = function() {};
339
340						var text = new fabric.IText( cfg.text, cfg );
341
342						_this.setup.fabric.add( text );
343						_this.setup.fabric.setActiveObject( text );
344
345						text.selectAll();
346						text.enterEditing();
347
348						return text;
349					},
350					line: function( options ) {
351						var cfg = _this.deepMerge( {
352							x1: ( _this.setup.fabric.width / 2 ) - ( _this.setup.fabric.width / 10 ),
353							x2: ( _this.setup.fabric.width / 2 ) + ( _this.setup.fabric.width / 10 ),
354							y1: ( _this.setup.fabric.height / 2 ),
355							y2: ( _this.setup.fabric.height / 2 ),
356							angle: 90,
357							strokeLineCap: _this.drawing.lineCap,
358							arrow: _this.drawing.arrow,
359							color: _this.drawing.color,
360							width: _this.drawing.width,
361							group: [],
362						}, options || {} );
363						var i1, arrow, arrowTop, arrowLeft;
364						var line = new fabric.Line( [ cfg.x1, cfg.y1, cfg.x2, cfg.y2 ], {
365							stroke: cfg.color,
366							strokeWidth: cfg.width,
367							strokeLineCap: cfg.strokeLineCap
368						} );
369
370						cfg.group.push( line );
371
372						if ( cfg.arrow ) {
373							cfg.angle = cfg.angle ? cfg.angle : _this.getAngle( cfg.x1, cfg.y1, cfg.x2, cfg.y2 );
374
375							if ( cfg.arrow == "start" ) {
376								arrowTop = cfg.y1 + ( cfg.width / 2 );
377								arrowLeft = cfg.x1 + ( cfg.width / 2 );
378							} else if ( cfg.arrow == "middle" ) {
379								arrowTop = cfg.y2 + ( cfg.width / 2 ) - ( ( cfg.y2 - cfg.y1 ) / 2 );
380								arrowLeft = cfg.x2 + ( cfg.width / 2 ) - ( ( cfg.x2 - cfg.x1 ) / 2 );
381							} else { // arrow: end
382								arrowTop = cfg.y2 + ( cfg.width / 2 );
383								arrowLeft = cfg.x2 + ( cfg.width / 2 );
384							}
385
386							arrow = new fabric.Triangle( {
387								top: arrowTop,
388								left: arrowLeft,
389								fill: cfg.color,
390								height: cfg.width * 7,
391								width: cfg.width * 7,
392								angle: cfg.angle,
393								originX: "center",
394								originY: "bottom"
395							} );
396							cfg.group.push( arrow );
397						}
398
399						if ( cfg.action != "config" ) {
400							if ( cfg.arrow ) {
401								var group = new fabric.Group( cfg.group );
402								group.set( {
403									cfg: cfg,
404									fill: cfg.color,
405									action: cfg.action,
406									selectable: true,
407									known: cfg.action == "change"
408								} );
409								if ( cfg.action == "change" ) {
410									_this.setup.fabric.setActiveObject( group );
411								}
412								_this.setup.fabric.add( group );
413								return group;
414							} else {
415								_this.setup.fabric.add( line );
416								return line;
417							}
418						} else {
419							for ( i1 = 0; i1 < cfg.group.length; i1++ ) {
420								cfg.group[ i1 ].noUndo = true;
421								_this.setup.fabric.add( cfg.group[ i1 ] );
422							}
423						}
424						return cfg;
425					}
426				}
427			},
428			defaults: {
429				position: "top-right",
430				fileName: "amCharts",
431				action: "download",
432				overflow: true,
433				path: ( ( chart.path || "" ) + "plugins/export/" ),
434				formats: {
435					JPG: {
436						mimeType: "image/jpg",
437						extension: "jpg",
438						capture: true
439					},
440					PNG: {
441						mimeType: "image/png",
442						extension: "png",
443						capture: true
444					},
445					SVG: {
446						mimeType: "text/xml",
447						extension: "svg",
448						capture: true
449					},
450					PDF: {
451						mimeType: "application/pdf",
452						extension: "pdf",
453						capture: true
454					},
455					CSV: {
456						mimeType: "text/plain",
457						extension: "csv"
458					},
459					JSON: {
460						mimeType: "text/plain",
461						extension: "json"
462					},
463					XLSX: {
464						mimeType: "application/octet-stream",
465						extension: "xlsx"
466					}
467				},
468				fabric: {
469					backgroundColor: "#FFFFFF",
470					removeImages: true,
471					selection: false,
472					drawing: {
473						enabled: true,
474						arrow: "end",
475						lineCap: "butt",
476						mode: "pencil",
477						modes: [ "pencil", "line", "arrow" ],
478						color: "#000000",
479						colors: [ "#000000", "#FFFFFF", "#FF0000", "#00FF00", "#0000FF" ],
480						shapes: [ "11.svg", "14.svg", "16.svg", "17.svg", "20.svg", "27.svg" ],
481						width: 1,
482						fontSize: 11,
483						widths: [ 1, 5, 10, 15 ],
484						opacity: 1,
485						opacities: [ 1, 0.8, 0.6, 0.4, 0.2 ],
486						menu: undefined,
487						autoClose: true
488					}
489				},
490				pdfMake: {
491					pageSize: "A4",
492					pageOrientation: "portrait",
493					images: {},
494					content: [ "Saved from:", window.location.href, {
495						image: "reference",
496						fit: [ 523.28, 769.89 ]
497					} ]
498				},
499				menu: undefined,
500				divId: null,
501				menuReviver: null,
502				menuWalker: null,
503				fallback: true,
504				keyListener: true,
505				fileListener: true
506			},
507
508			/**
509			 * Returns translated message, takes english as default
510			 */
511			i18l: function( key, language ) {
512				var lang = language ? langugage : _this.setup.chart.language ? _this.setup.chart.language : "en";
513				var catalog = AmCharts.translations[ _this.name ][ lang ] || AmCharts.translations[ _this.name ][ "en" ];
514
515				return catalog[ key ] || key;
516			},
517
518			/**
519			 * Generates download file; if unsupported offers fallback to save manually
520			 */
521			download: function( data, type, filename ) {
522				// SAVE
523				if ( window.saveAs && _this.setup.hasBlob ) {
524					var blob = _this.toBlob( {
525						data: data,
526						type: type
527					}, function( data ) {
528						saveAs( data, filename );
529					} );
530
531					// FALLBACK TEXTAREA
532				} else if ( _this.config.fallback && type == "text/plain" ) {
533					var div = document.createElement( "div" );
534					var msg = document.createElement( "div" );
535					var textarea = document.createElement( "textarea" );
536
537					msg.innerHTML = _this.i18l( "fallback.save.text" );
538
539					div.appendChild( msg );
540					div.appendChild( textarea );
541					msg.setAttribute( "class", "amcharts-export-fallback-message" );
542					div.setAttribute( "class", "amcharts-export-fallback" );
543					_this.setup.chart.containerDiv.appendChild( div );
544
545					// FULFILL TEXTAREA AND PRESELECT
546					textarea.setAttribute( "readonly", "" );
547					textarea.value = data;
548					textarea.focus();
549					textarea.select();
550
551					// UPDATE MENU
552					_this.createMenu( [ {
553						"class": "export-main export-close",
554						label: "Done",
555						click: function() {
556							_this.createMenu( _this.config.menu );
557							_this.setup.chart.containerDiv.removeChild( div );
558						}
559					} ] );
560
561					// FALLBACK IMAGE
562				} else if ( _this.config.fallback && type.split( "/" )[ 0 ] == "image" ) {
563					var div = document.createElement( "div" );
564					var msg = document.createElement( "div" );
565					var img = _this.toImage( {
566						data: data
567					} );
568
569					msg.innerHTML = _this.i18l( "fallback.save.image" );
570
571					// FULFILL TEXTAREA AND PRESELECT
572					div.appendChild( msg );
573					div.appendChild( img );
574					msg.setAttribute( "class", "amcharts-export-fallback-message" );
575					div.setAttribute( "class", "amcharts-export-fallback" );
576					_this.setup.chart.containerDiv.appendChild( div );
577
578					// UPDATE MENU
579					_this.createMenu( [ {
580						"class": "export-main export-close",
581						label: "Done",
582						click: function() {
583							_this.createMenu( _this.config.menu );
584							_this.setup.chart.containerDiv.removeChild( div );
585						}
586					} ] );
587
588					// ERROR
589				} else {
590					throw new Error( "Unable to create file. Ensure saveAs (FileSaver.js) is supported." );
591				}
592				return data;
593			},
594
595			/**
596			 * Generates script, links tags and places them into the document's head
597			 * In case of reload it replaces the node to force the download
598			 */
599			loadResource: function( src, addons ) {
600				var i1, exist, node, item, check, type;
601				var url = src.indexOf( "//" ) != -1 ? src : [ _this.libs.path, src ].join( "" );
602
603				function callback() {
604					if ( addons ) {
605						for ( i1 = 0; i1 < addons.length; i1++ ) {
606							_this.loadResource( addons[ i1 ] );
607						}
608					}
609				}
610
611				if ( src.indexOf( ".js" ) != -1 ) {
612					node = document.createElement( "script" );
613					node.setAttribute( "type", "text/javascript" );
614					node.setAttribute( "src", url );
615					if ( _this.libs.async ) {
616						node.setAttribute( "async", "" );
617					}
618
619				} else if ( src.indexOf( ".css" ) != -1 ) {
620					node = document.createElement( "link" );
621					node.setAttribute( "type", "text/css" );
622					node.setAttribute( "rel", "stylesheet" );
623					node.setAttribute( "href", url );
624				}
625
626				// NODE CHECK
627				for ( i1 = 0; i1 < document.head.childNodes.length; i1++ ) {
628					item = document.head.childNodes[ i1 ];
629					check = item ? ( item.src || item.href ) : false;
630					type = item ? item.tagName : false;
631
632					if ( item && check && check.indexOf( src ) != -1 ) {
633						if ( _this.libs.reload ) {
634							document.head.removeChild( item );
635						}
636						exist = true;
637						break;
638					}
639				}
640
641				// NAMESPACE CHECK
642				for ( i1 in _this.libs.namespaces ) {
643					var namespace = _this.libs.namespaces[ i1 ];
644					var check = src.toLowerCase();
645					var item = i1.toLowerCase();
646					if ( check.indexOf( item ) != -1 && window[ namespace ] !== undefined ) {
647						exist = true;
648						break;
649					}
650				}
651
652				if ( !exist || _this.libs.reload ) {
653					node.addEventListener( "load", callback );
654					document.head.appendChild( node );
655				}
656
657			},
658
659			/**
660			 * Walker to generate the script,link tags
661			 */
662			loadDependencies: function() {
663				var i1, i2;
664				if ( _this.libs.autoLoad ) {
665					for ( i1 = 0; i1 < _this.libs.resources.length; i1++ ) {
666						if ( _this.libs.resources[ i1 ] instanceof Object ) {
667							for ( i2 in _this.libs.resources[ i1 ] ) {
668								_this.loadResource( i2, _this.libs.resources[ i1 ][ i2 ] );
669							}
670						} else {
671							_this.loadResource( _this.libs.resources[ i1 ] );
672						}
673					}
674				}
675			},
676
677			/**
678			 * Converts string to number
679			 */
680			pxToNumber: function( attr, returnUndefined ) {
681				if ( !attr && returnUndefined ) {
682					return undefined;
683				}
684				return Number( String( attr ).replace( "px", "" ) ) || 0;
685			},
686
687			/**
688			 * Converts number to string
689			 */
690			numberToPx: function( attr ) {
691				return String( attr ) + "px";
692			},
693
694			/**
695			 * Recursive method to merge the given objects together
696			 * Overwrite flag replaces the value instead to crawl through
697			 */
698			deepMerge: function( a, b, overwrite ) {
699				var i1, v, type = b instanceof Array ? "array" : "object";
700
701				for ( i1 in b ) {
702					// PREVENT METHODS
703					if ( type == "array" && isNaN( i1 ) ) {
704						continue;
705					}
706
707					v = b[ i1 ];
708
709					// NEW
710					if ( a[ i1 ] == undefined || overwrite ) {
711						if ( v instanceof Array ) {
712							a[ i1 ] = new Array();
713						} else if ( v instanceof Function ) {
714							a[ i1 ] = function() {};
715						} else if ( v instanceof Date ) {
716							a[ i1 ] = new Date();
717						} else if ( v instanceof Object ) {
718							a[ i1 ] = new Object();
719						} else if ( v instanceof Number ) {
720							a[ i1 ] = new Number();
721						} else if ( v instanceof String ) {
722							a[ i1 ] = new String();
723						}
724					}
725
726					if (
727						( a instanceof Object || a instanceof Array ) &&
728						( v instanceof Object || v instanceof Array ) &&
729						!( v instanceof Function || v instanceof Date || _this.isElement( v ) ) &&
730						i1 != "chart"
731					) {
732						_this.deepMerge( a[ i1 ], v, overwrite );
733					} else {
734						if ( a instanceof Array && !overwrite ) {
735							a.push( v );
736						} else {
737							a[ i1 ] = v;
738						}
739					}
740				}
741				return a;
742			},
743
744			/**
745			 * Checks if given argument is a valid node
746			 */
747			isElement: function( thingy ) {
748				return thingy instanceof Object && thingy && thingy.nodeType === 1;
749			},
750
751			/**
752			 * Checks if given argument contains a hashbang and returns it
753			 */
754			isHashbanged: function( thingy ) {
755				var str = String( thingy ).replace( /\"/g, "" );
756
757				return str.slice( 0, 3 ) == "url" ? str.slice( str.indexOf( "#" ) + 1, str.length - 1 ) : false;
758			},
759
760			/**
761			 * Checks if given event has been thrown with pressed click / touch
762			 */
763			isPressed: function( event ) {
764				// IE EXCEPTION
765				if ( event.type == "mousemove" && event.which === 1 ) {
766					// IGNORE
767
768					// OTHERS
769				} else if (
770					event.type == "touchmove" ||
771					event.buttons === 1 ||
772					event.button === 1 ||
773					event.which === 1
774				) {
775					_this.drawing.buffer.isPressed = true;
776				} else {
777					_this.drawing.buffer.isPressed = false;
778				}
779				return _this.drawing.buffer.isPressed;
780			},
781
782			/**
783			 * Checks if given source is within the current origin
784			 */
785			isTainted: function( source ) {
786				var origin = String( window.location.origin || window.location.protocol + "//" + window.location.hostname + ( window.location.port ? ':' + window.location.port : '' ) );
787
788				// CHECK IF TAINTED
789				if (
790					source &&
791					source.indexOf( "//" ) != -1 &&
792					source.indexOf( origin.replace( /.*:/, "" ) ) == -1
793				) {
794					return true;
795				}
796				return false;
797			},
798
799			/*
800			 ** Checks several indicators for acceptance;
801			 */
802			isSupported: function() {
803				// CHECK CONFIG
804				if ( !_this.config.enabled ) {
805					return false;
806				}
807
808				// CHECK IE; ATTEMPT TO ACCESS HEAD ELEMENT
809				if ( AmCharts.isIE && AmCharts.IEversion <= 9 ) {
810					if ( !Array.prototype.indexOf || !document.head || _this.config.fallback === false ) {
811						return false;
812					}
813				}
814				return true;
815			},
816
817
818			getAngle: function( x1, y1, x2, y2 ) {
819				var x = x2 - x1;
820				var y = y2 - y1;
821				var angle;
822				if ( x == 0 ) {
823					if ( y == 0 ) {
824						angle = 0;
825					} else if ( y > 0 ) {
826						angle = Math.PI / 2;
827					} else {
828						angle = Math.PI * 3 / 2;
829					}
830				} else if ( y == 0 ) {
831					if ( x > 0 ) {
832						angle = 0;
833					} else {
834						angle = Math.PI;
835					}
836				} else {
837					if ( x < 0 ) {
838						angle = Math.atan( y / x ) + Math.PI;
839					} else if ( y < 0 ) {
840						angle = Math.atan( y / x ) + ( 2 * Math.PI );
841					} else {
842						angle = Math.atan( y / x );
843					}
844				}
845				return angle * 180 / Math.PI;
846			},
847
848			/**
849			 * Recursive method which crawls upwards to gather the requested attribute
850			 */
851			gatherAttribute: function( elm, attr, limit, lvl ) {
852				var value, lvl = lvl ? lvl : 0,
853					limit = limit ? limit : 3;
854				if ( elm ) {
855					value = elm.getAttribute( attr );
856
857					if ( !value && lvl < limit ) {
858						return _this.gatherAttribute( elm.parentNode, attr, limit, lvl + 1 );
859					}
860				}
861				return value;
862			},
863
864			/**
865			 * Recursive method which crawls upwards to gather the requested classname
866			 */
867			gatherClassName: function( elm, className, limit, lvl ) {
868				var value, lvl = lvl ? lvl : 0,
869					limit = limit ? limit : 3;
870
871				if ( _this.isElement( elm ) ) {
872					value = ( elm.getAttribute( "class" ) || "" ).split( " " ).indexOf( className ) != -1;
873
874					if ( !value && lvl < limit ) {
875						return _this.gatherClassName( elm.parentNode, className, limit, lvl + 1 );
876					} else if ( value ) {
877						value = elm;
878					}
879				}
880				return value;
881			},
882
883			/**
884			 * Collects the clip-paths and patterns
885			 */
886			gatherElements: function( group, cfg, images ) {
887				var i1, i2;
888				for ( i1 = 0; i1 < group.children.length; i1++ ) {
889					var childNode = group.children[ i1 ];
890
891					// CLIPPATH
892					if ( childNode.tagName == "clipPath" ) {
893						var bbox = {};
894						var transform = fabric.parseTransformAttribute( _this.gatherAttribute( childNode, "transform" ) );
895
896						// HIDE SIBLINGS; GATHER IT'S DIMENSIONS
897						for ( i2 = 0; i2 < childNode.childNodes.length; i2++ ) {
898							childNode.childNodes[ i2 ].setAttribute( "fill", "transparent" );
899							bbox = {
900								x: _this.pxToNumber( childNode.childNodes[ i2 ].getAttribute( "x" ) ),
901								y: _this.pxToNumber( childNode.childNodes[ i2 ].getAttribute( "y" ) ),
902								width: _this.pxToNumber( childNode.childNodes[ i2 ].getAttribute( "width" ) ),
903								height: _this.pxToNumber( childNode.childNodes[ i2 ].getAttribute( "height" ) )
904							}
905						}
906
907						group.clippings[ childNode.id ] = {
908							svg: childNode,
909							bbox: bbox,
910							transform: transform
911						};
912
913						// PATTERN
914					} else if ( childNode.tagName == "pattern" ) {
915						var props = {
916							node: childNode,
917							source: childNode.getAttribute( "xlink:href" ),
918							width: Number( childNode.getAttribute( "width" ) ),
919							height: Number( childNode.getAttribute( "height" ) ),
920							repeat: "repeat"
921						}
922
923						// GATHER BACKGROUND COLOR
924						for ( i2 = 0; i2 < childNode.childNodes.length; i2++ ) {
925							if ( childNode.childNodes[ i2 ].tagName == "rect" ) {
926								props.fill = childNode.childNodes[ i2 ].getAttribute( "fill" );
927							}
928						}
929
930						// TAINTED
931						if ( cfg.removeImages && _this.isTainted( props.source ) ) {
932							group.patterns[ childNode.id ] = props.fill ? props.fill : "transparent";
933						} else {
934							images.included++;
935
936							group.patterns[ props.node.id ] = props;
937						}
938
939						// IMAGES
940					} else if ( childNode.tagName == "image" ) {
941						images.included++;
942
943						// LOAD IMAGE MANUALLY; TO RERENDER THE CANVAS
944						fabric.Image.fromURL( childNode.getAttribute( "xlink:href" ), function( img ) {
945							images.loaded++;
946						} );
947					}
948				}
949				return group;
950			},
951
952			/*
953			 ** GATHER MOUSE POSITION;
954			 */
955			gatherPosition: function( event, type ) {
956				var ref = _this.drawing.buffer.position;
957				var ivt = fabric.util.invertTransform( _this.setup.fabric.viewportTransform );
958				var pos;
959
960				if ( event.type == "touchmove" ) {
961					if ( "touches" in event ) {
962						event = event.touches[ 0 ];
963					} else if ( "changedTouches" in event ) {
964						event = event.changedTouches[ 0 ];
965					}
966				}
967
968				pos = fabric.util.transformPoint( _this.setup.fabric.getPointer( event, true ), ivt );
969
970				if ( type == 1 ) {
971					ref.x1 = pos.x;
972					ref.y1 = pos.y;
973				}
974
975				ref.x2 = pos.x;
976				ref.y2 = pos.y;
977				ref.xD = ( ref.x1 - ref.x2 ) < 0 ? ( ref.x1 - ref.x2 ) * -1 : ( ref.x1 - ref.x2 );
978				ref.yD = ( ref.y1 - ref.y2 ) < 0 ? ( ref.y1 - ref.y2 ) * -1 : ( ref.y1 - ref.y2 );
979
980				return ref;
981			},
982
983			/**
984			 * Method to capture the current state of the chart
985			 */
986			capture: function( options, callback ) {
987				var i1;
988				var cfg = _this.deepMerge( _this.deepMerge( {}, _this.config.fabric ), options || {} );
989				var groups = [];
990				var offset = {
991					x: 0,
992					y: 0,
993					pX: 0,
994					pY: 0,
995					width: _this.setup.chart.divRealWidth,
996					height: _this.setup.chart.divRealHeight
997				};
998				var images = {
999					loaded: 0,
1000					included: 0
1001				}
1002
1003				fabric.ElementsParser.prototype.resolveGradient = function( obj, property ) {
1004
1005					var instanceFillValue = obj.get( property );
1006					if ( !( /^url\(/ ).test( instanceFillValue ) ) {
1007						return;
1008					}
1009					var gradientId = instanceFillValue.slice( instanceFillValue.indexOf( "#" ) + 1, instanceFillValue.length - 1 );
1010					if ( fabric.gradientDefs[ this.svgUid ][ gradientId ] ) {
1011						obj.set( property, fabric.Gradient.fromElement( fabric.gradientDefs[ this.svgUid ][ gradientId ], obj ) );
1012					}
1013				};
1014
1015				// BEFORE CAPTURING
1016				_this.handleCallback( cfg.beforeCapture, cfg );
1017
1018				// GATHER SVGS
1019				var svgs = _this.setup.chart.containerDiv.getElementsByTagName( "svg" );
1020				for ( i1 = 0; i1 < svgs.length; i1++ ) {
1021					var group = {
1022						svg: svgs[ i1 ],
1023						parent: svgs[ i1 ].parentNode,
1024						children: svgs[ i1 ].getElementsByTagName( "*" ),
1025						offset: {
1026							x: 0,
1027							y: 0
1028						},
1029						patterns: {},
1030						clippings: {}
1031					}
1032
1033					// GATHER ELEMENTS
1034					group = _this.gatherElements( group, cfg, images );
1035
1036					// APPEND GROUP
1037					groups.push( group );
1038				}
1039
1040				// GATHER EXTERNAL LEGEND
1041				if ( _this.config.legend && _this.setup.chart.legend && _this.setup.chart.legend.position == "outside" ) {
1042					var group = {
1043						svg: _this.setup.chart.legend.container.container,
1044						parent: _this.setup.chart.legend.container.container.parentNode,
1045						children: _this.setup.chart.legend.container.container.getElementsByTagName( "*" ),
1046						offset: {
1047							x: 0,
1048							y: 0
1049						},
1050						legend: {
1051							type: [ "top", "left" ].indexOf( _this.config.legend.position ) != -1 ? "unshift" : "push",
1052							position: _this.config.legend.position,
1053							width: _this.config.legend.width ? _this.config.legend.width : _this.setup.chart.legend.container.width,
1054							height: _this.config.legend.height ? _this.config.legend.height : _this.setup.chart.legend.container.height
1055						},
1056						patterns: {},
1057						clippings: {}
1058					}
1059
1060					// ADAPT CANVAS DIMENSIONS
1061					if ( [ "left", "right" ].indexOf( group.legend.position ) != -1 ) {
1062						offset.width += group.legend.width;
1063						offset.height = group.legend.height > offset.height ? group.legend.height : offset.height;
1064					} else if ( [ "top", "bottom" ].indexOf( group.legend.position ) != -1 ) {
1065						offset.height += group.legend.height;
1066					}
1067
1068					// GATHER ELEMENTS
1069					group = _this.gatherElements( group, cfg, images );
1070
1071					// PRE/APPEND SVG
1072					groups[ group.legend.type ]( group );
1073				}
1074
1075				// CLEAR IF EXIST
1076				_this.drawing.buffer.enabled = cfg.action == "draw";
1077
1078				_this.setup.wrapper = document.createElement( "div" );
1079				_this.setup.wrapper.setAttribute( "class", _this.setup.chart.classNamePrefix + "-export-canvas" );
1080				_this.setup.chart.containerDiv.appendChild( _this.setup.wrapper );
1081
1082				// STOCK CHART; SELECTOR OFFSET
1083				if ( _this.setup.chart.type == "stock" ) {
1084					var padding = {
1085						top: 0,
1086						right: 0,
1087						bottom: 0,
1088						left: 0
1089					}
1090					if ( _this.setup.chart.leftContainer ) {
1091						offset.width -= _this.setup.chart.leftContainer.offsetWidth;
1092						padding.left = _this.setup.chart.leftContainer.offsetWidth + ( _this.setup.chart.panelsSettings.panelSpacing * 2 );
1093					}
1094					if ( _this.setup.chart.rightContainer ) {
1095						offset.width -= _this.setup.chart.rightContainer.offsetWidth;
1096						padding.right = _this.setup.chart.rightContainer.offsetWidth + ( _this.setup.chart.panelsSettings.panelSpacing * 2 );
1097					}
1098					if ( _this.setup.chart.periodSelector && [ "top", "bottom" ].indexOf( _this.setup.chart.periodSelector.position ) != -1 ) {
1099						offset.height -= _this.setup.chart.periodSelector.offsetHeight + _this.setup.chart.panelsSettings.panelSpacing;
1100						padding[ _this.setup.chart.periodSelector.position ] += _this.setup.chart.periodSelector.offsetHeight + _this.setup.chart.panelsSettings.panelSpacing;
1101					}
1102					if ( _this.setup.chart.dataSetSelector && [ "top", "bottom" ].indexOf( _this.setup.chart.dataSetSelector.position ) != -1 ) {
1103						offset.height -= _this.setup.chart.dataSetSelector.offsetHeight;
1104						padding[ _this.setup.chart.dataSetSelector.position ] += _this.setup.chart.dataSetSelector.offsetHeight;
1105					}
1106
1107					// APPLY OFFSET ON WRAPPER
1108					_this.setup.wrapper.style.paddingTop = _this.numberToPx( padding.top );
1109					_this.setup.wrapper.style.paddingRight = _this.numberToPx( padding.right );
1110					_this.setup.wrapper.style.paddingBottom = _this.numberToPx( padding.bottom );
1111					_this.setup.wrapper.style.paddingLeft = _this.numberToPx( padding.left );
1112				}
1113
1114				// CREATE CANVAS
1115				_this.setup.canvas = document.createElement( "canvas" );
1116				_this.setup.wrapper.appendChild( _this.setup.canvas );
1117				_this.setup.fabric = new fabric.Canvas( _this.setup.canvas, _this.deepMerge( {
1118					width: offset.width,
1119					height: offset.height,
1120					isDrawingMode: true
1121				}, cfg ) );
1122
1123				// REAPPLY FOR SOME REASON
1124				_this.deepMerge( _this.setup.fabric, cfg );
1125				_this.deepMerge( _this.setup.fabric.freeDrawingBrush, cfg.drawing );
1126
1127				// RELIABLE VARIABLES; UPDATE DRAWING
1128				_this.deepMerge( _this.drawing, cfg.drawing );
1129				_this.drawing.handler.change( cfg.drawing );
1130
1131				// OBSERVE MOUSE EVENTS
1132				_this.setup.fabric.on( "mouse:down", function( e ) {
1133					var p = _this.gatherPosition( e.e, 1 );
1134					_this.drawing.buffer.pressedTS = Number( new Date() );
1135					_this.isPressed( e.e );
1136				} );
1137				_this.setup.fabric.on( "mouse:move", function( e ) {
1138					var p = _this.gatherPosition( e.e, 2 );
1139					_this.isPressed( e.e );
1140
1141					// CREATE INITIAL LINE / ARROW; JUST ON LEFT CLICK
1142					if ( _this.drawing.buffer.isPressed && !_this.drawing.buffer.line ) {
1143						if ( !_this.drawing.buffer.isSelected && _this.drawing.mode != "pencil" && ( p.xD > 5 || p.xD > 5 ) ) {
1144							_this.drawing.buffer.hasLine = true;
1145							_this.setup.fabric.isDrawingMode = false;
1146							_this.setup.fabric._onMouseUpInDrawingMode( e );
1147							_this.drawing.buffer.line = _this.drawing.handler.line( {
1148								x1: p.x1,
1149								y1: p.y1,
1150								x2: p.x2,
1151								y2: p.y2,
1152								arrow: _this.drawing.mode == "line" ? false : _this.drawing.arrow,
1153								action: "config"
1154							} );
1155						}
1156					}
1157
1158					// UPDATE LINE / ARROW
1159					if ( _this.drawing.buffer.line ) {
1160						var obj, top, left;
1161						var l = _this.drawing.buffer.line;
1162
1163						l.x2 = p.x2;
1164						l.y2 = p.y2;
1165
1166						for ( i1 = 0; i1 < l.group.length; i1++ ) {
1167							obj = l.group[ i1 ];
1168
1169							if ( obj instanceof fabric.Line ) {
1170								obj.set( {
1171									x2: l.x2,
1172									y2: l.y2
1173								} );
1174							} else if ( obj instanceof fabric.Triangle ) {
1175								l.angle = ( _this.getAngle( l.x1, l.y1, l.x2, l.y2 ) + 90 );
1176
1177								if ( l.arrow == "start" ) {
1178									top = l.y1 + ( l.width / 2 );
1179									left = l.x1 + ( l.width / 2 );
1180								} else if ( l.arrow == "middle" ) {
1181									top = l.y2 + ( l.width / 2 ) - ( ( l.y2 - l.y1 ) / 2 );
1182									left = l.x2 + ( l.width / 2 ) - ( ( l.x2 - l.x1 ) / 2 );
1183								} else { // arrow: end
1184									top = l.y2 + ( l.width / 2 );
1185									left = l.x2 + ( l.width / 2 );
1186								}
1187
1188								obj.set( {
1189									top: top,
1190									left: left,
1191									angle: l.angle
1192								} );
1193							}
1194						}
1195						_this.setup.fabric.renderAll();
1196					}
1197				} );
1198				_this.setup.fabric.on( "mouse:up", function( e ) {
1199					// SELECT TARGET
1200					if ( Number( new Date() ) - _this.drawing.buffer.pressedTS < 200 ) {
1201						var target = _this.setup.fabric.findTarget( e.e );
1202						if ( target && target.selectable ) {
1203							_this.setup.fabric.setActiveObject( target );
1204						}
1205					}
1206
1207					// UPDATE LINE / ARROW
1208					if ( _this.drawing.buffer.line ) {
1209						for ( i1 = 0; i1 < _this.drawing.buffer.line.group.length; i1++ ) {
1210							_this.drawing.buffer.line.group[ i1 ].remove();
1211						}
1212						delete _this.drawing.buffer.line.action;
1213						delete _this.drawing.buffer.line.group;
1214						_this.drawing.handler.line( _this.drawing.buffer.line );
1215					}
1216					_this.drawing.buffer.line = false;
1217					_this.drawing.buffer.hasLine = false;
1218					_this.drawing.buffer.isPressed = false;
1219				} );
1220
1221				// OBSERVE OBJECT SELECTION
1222				_this.setup.fabric.on( "object:selected", function( e ) {
1223					_this.drawing.buffer.isSelected = true;
1224					_this.drawing.buffer.target = e.target;
1225					_this.setup.fabric.isDrawingMode = false;
1226				} );
1227				_this.setup.fabric.on( "selection:cleared", function( e ) {
1228					_this.drawing.buffer.onMouseDown = _this.setup.fabric.freeDrawingBrush.onMouseDown;
1229					_this.drawing.buffer.target = false;
1230
1231					// FREEHAND WORKAROUND
1232					if ( _this.drawing.buffer.isSelected ) {
1233						_this.setup.fabric._isCurrentlyDrawing = false;
1234						_this.setup.fabric.freeDrawingBrush.onMouseDown = function() {};
1235					}
1236
1237					// DELAYED DESELECTION TO PREVENT DRAWING
1238					setTimeout( function() {
1239						_this.drawing.buffer.isSelected = false;
1240						_this.setup.fabric.isDrawingMode = true;
1241						_this.setup.fabric.freeDrawingBrush.onMouseDown = _this.drawing.buffer.onMouseDown;
1242					}, 10 );
1243				} );
1244				_this.setup.fabric.on( "path:created", function( e ) {
1245					var item = e.path;
1246					if ( Number( new Date() ) - _this.drawing.buffer.pressedTS < 200 || _this.drawing.buffer.hasLine ) {
1247						_this.setup.fabric.remove( item );
1248						_this.setup.fabric.renderAll();
1249						return;
1250					}
1251				} );
1252
1253				// OBSERVE OBJECT MODIFICATIONS
1254				_this.setup.fabric.on( "object:added", function( e ) {
1255					var item = e.target;
1256					var state = _this.deepMerge( item.saveState().originalState, {
1257						cfg: {
1258							color: _this.drawing.color,
1259							width: _this.drawing.width,
1260							opacity: _this.drawing.opacity,
1261							fontSize: _this.drawing.fontSize
1262						}
1263					} );
1264
1265					if ( Number( new Date() ) - _this.drawing.buffer.pressedTS < 200 && !item.noUndo ) {
1266						_this.setup.fabric.remove( item );
1267						_this.setup.fabric.renderAll();
1268						return;
1269					}
1270
1271					state = JSON.stringify( state );
1272					item.recentState = state;
1273
1274					if ( item.selectable && !item.known && !item.noUndo ) {
1275						_this.drawing.undos.push( {
1276							action: "added",
1277							target: item,
1278							state: state
1279						} );
1280						_this.drawing.undos.push( {
1281							action: "addified",
1282							target: item,
1283							state: state
1284						} );
1285						_this.drawing.redos = [];
1286					}
1287
1288					item.known = true;
1289					_this.setup.fabric.isDrawingMode = true;
1290				} );
1291				_this.setup.fabric.on( "object:modified", function( e ) {
1292					var item = e.target;
1293					var recentState = JSON.parse( item.recentState );
1294					var state = _this.deepMerge( item.saveState().originalState, {
1295						cfg: recentState.cfg
1296					} );
1297
1298					state = JSON.stringify( state );
1299					item.recentState = state;
1300
1301					_this.drawing.undos.push( {
1302						action: "modified",
1303						target: item,
1304						state: state
1305					} );
1306
1307					_this.drawing.redos = [];
1308				} );
1309				_this.setup.fabric.on( "text:changed", function( e ) {
1310					var item = e.target;
1311					clearTimeout( item.timer );
1312					item.timer = setTimeout( function() {
1313						var state = JSON.stringify( item.saveState().originalState );
1314
1315						item.recentState = state;
1316
1317						_this.drawing.redos = [];
1318						_this.drawing.undos.push( {
1319							action: "modified",
1320							target: item,
1321							state: state
1322						} );
1323					}, 250 );
1324				} );
1325
1326				// DRAWING
1327				if ( _this.drawing.buffer.enabled ) {
1328					_this.setup.wrapper.setAttribute( "class", _this.setup.chart.classNamePrefix + "-export-canvas active" );
1329					_this.setup.wrapper.style.backgroundColor = cfg.backgroundColor;
1330					_this.setup.wrapper.style.display = "block";
1331
1332				} else {
1333					_this.setup.wrapper.setAttribute( "class", _this.setup.chart.classNamePrefix + "-export-canvas" );
1334					_this.setup.wrapper.style.display = "none";
1335				}
1336
1337				for ( i1 = 0; i1 < groups.length; i1++ ) {
1338					var group = groups[ i1 ];
1339					var isLegend = _this.gatherClassName( group.parent, _this.setup.chart.classNamePrefix + "-legend-div", 1 );
1340					var isPanel = _this.gatherClassName( group.parent, _this.setup.chart.classNamePrefix + "-stock-panel-div" );
1341					var isScrollbar = _this.gatherClassName( group.parent, _this.setup.chart.classNamePrefix + "-scrollbar-chart-div" );
1342
1343					// STOCK CHART; SVG OFFSET;; SVG OFFSET
1344					if ( _this.setup.chart.type == "stock" && _this.setup.chart.legendSettings.position ) {
1345
1346						// TOP / BOTTOM
1347						if ( [ "top", "bottom" ].indexOf( _this.setup.chart.legendSettings.position ) != -1 ) {
1348
1349							// POSITION; ABSOLUTE
1350							if ( group.parent.style.top && group.parent.style.left ) {
1351								group.offset.y = _this.pxToNumber( group.parent.style.top );
1352								group.offset.x = _this.pxToNumber( group.parent.style.left );
1353
1354								// POSITION; RELATIVE
1355							} else {
1356								group.offset.x = offset.x;
1357								group.offset.y = offset.y;
1358								offset.y += _this.pxToNumber( group.parent.style.height );
1359
1360								// LEGEND; OFFSET
1361								if ( isPanel ) {
1362									offset.pY = _this.pxToNumber( isPanel.style.marginTop );
1363									group.offset.y += offset.pY;
1364
1365									// SCROLLBAR; OFFSET
1366								} else if ( isScrollbar ) {
1367									group.offset.y += offset.pY;
1368								}
1369							}
1370
1371							// LEFT / RIGHT
1372						} else if ( [ "left", "right" ].indexOf( _this.setup.chart.legendSettings.position ) != -1 ) {
1373							group.offset.y = _this.pxToNumber( group.parent.style.top ) + offset.pY;
1374							group.offset.x = _this.pxToNumber( group.parent.style.left ) + offset.pX;
1375
1376							// LEGEND; OFFSET
1377							if ( isLegend ) {
1378								offset.pY += _this.pxToNumber( isPanel.style.height ) + _this.setup.chart.panelsSettings.panelSpacing;
1379
1380								// SCROLLBAR; OFFSET
1381							} else if ( isScrollbar ) {
1382								group.offset.y -= _this.setup.chart.panelsSettings.panelSpacing;
1383							}
1384						}
1385
1386						// REGULAR CHARTS; SVG OFFSET
1387					} else {
1388						// POSITION; ABSOLUTE
1389						if ( group.parent.style.position == "absolute" ) {
1390							group.offset.absolute = true;
1391							group.offset.top = _this.pxToNumber( group.parent.style.top );
1392							group.offset.right = _this.pxToNumber( group.parent.style.right, true );
1393							group.offset.bottom = _this.pxToNumber( group.parent.style.bottom, true );
1394							group.offset.left = _this.pxToNumber( group.parent.style.left );
1395							group.offset.width = _this.pxToNumber( group.parent.style.width );
1396							group.offset.height = _this.pxToNumber( group.parent.style.height );
1397
1398							// POSITION; RELATIVE
1399						} else if ( group.parent.style.top && group.parent.style.left ) {
1400							group.offset.y = _this.pxToNumber( group.parent.style.top );
1401							group.offset.x = _this.pxToNumber( group.parent.style.left );
1402
1403							// POSITION; GENERIC
1404						} else {
1405
1406							// EXTERNAL LEGEND
1407							if ( group.legend ) {
1408								if ( group.legend.position == "left" ) {
1409									offset.x += group.legend.width;
1410								} else if ( group.legend.position == "right" ) {
1411									group.offset.x += offset.width - group.legend.width;
1412								} else if ( group.legend.position == "top" ) {
1413									offset.y += group.legend.height;
1414								} else if ( group.legend.position == "bottom" ) {
1415									group.offset.y += offset.height - group.legend.height; // OFFSET.Y
1416								}
1417
1418								// NORMAL
1419							} else {
1420								group.offset.x = offset.x;
1421								group.offset.y = offset.y + offset.pY;
1422								offset.y += _this.pxToNumber( group.parent.style.height );
1423							}
1424						}
1425
1426						// PANEL
1427						if ( isLegend && isPanel && isPanel.style.marginTop ) {
1428							offset.y += _this.pxToNumber( isPanel.style.marginTop );
1429							group.offset.y += _this.pxToNumber( isPanel.style.marginTop );
1430						}
1431					}
1432
1433					// ADD TO CANVAS
1434					fabric.parseSVGDocument( group.svg, ( function( group ) {
1435						return function( objects, options ) {
1436							var i1;
1437							var g = fabric.util.groupSVGElements( objects, options );
1438							var paths = [];
1439							var tmp = {
1440								selectable: false
1441							};
1442
1443							// GROUP OFFSET; ABSOLUTE
1444							if ( group.offset.absolute ) {
1445								if ( group.offset.bottom !== undefined ) {
1446									tmp.top = offset.height - group.offset.height - group.offset.bottom;
1447								} else {
1448									tmp.top = group.offset.top;
1449								}
1450
1451								if ( group.offset.right !== undefined ) {
1452									tmp.left = offset.width - group.offset.width - group.offset.right;
1453								} else {
1454									tmp.left = group.offset.left;
1455								}
1456
1457								// GROUP OFFSET; REGULAR
1458							} else {
1459								tmp.top = group.offset.y;
1460								tmp.left = group.offset.x;
1461							}
1462
1463							// WALKTHROUGH ELEMENTS
1464							for ( i1 = 0; i1 < g.paths.length; i1++ ) {
1465								var PID = null;
1466
1467								// OPACITY; TODO: DISTINGUISH OPACITY TYPES
1468								if ( g.paths[ i1 ] ) {
1469
1470									// CHECK ORIGIN; REMOVE TAINTED
1471									if ( cfg.removeImages && _this.isTainted( g.paths[ i1 ][ "xlink:href" ] ) ) {
1472										continue;
1473									}
1474
1475									// SET OPACITY
1476									if ( g.paths[ i1 ].fill instanceof Object ) {
1477
1478										// MISINTERPRETATION OF FABRIC
1479										if ( g.paths[ i1 ].fill.type == "radial" ) {
1480
1481											// PIE EXCEPTION
1482											if ( _this.setup.chart.type == "pie" ) {
1483												var tmp_n = g.paths[ i1 ];
1484												var tmp_c = tmp_n.getCenterPoint();
1485												var tmp_cp = tmp_n.group.getCenterPoint();
1486												var tmp_cd = {
1487													x: tmp_n.pathOffset.x - tmp_cp.x,
1488													y: tmp_n.pathOffset.y - tmp_cp.y
1489												};
1490
1491												g.paths[ i1 ].fill.gradientTransform[ 4 ] = tmp_n.pathOffset.x - tmp_cd.x;
1492												g.paths[ i1 ].fill.gradientTransform[ 5 ] = tmp_n.pathOffset.y - tmp_cd.y;
1493
1494												// OTHERS
1495											} else {
1496												g.paths[ i1 ].fill.coords.r2 = g.paths[ i1 ].fill.coords.r1 * -1;
1497												g.paths[ i1 ].fill.coords.r1 = 0;
1498												g.paths[ i1 ].set( {
1499													opacity: g.paths[ i1 ].fillOpacity
1500												} );
1501											}
1502										}
1503
1504										// FILLING; TODO: DISTINGUISH OPACITY TYPES
1505									} else if ( PID = _this.isHashbanged( g.paths[ i1 ].fill ) ) {
1506
1507										// PATTERN
1508										if ( group.patterns && group.patterns[ PID ] ) {
1509
1510											var props = group.patterns[ PID ];
1511
1512											// LOAD IMAGE MANUALLY; TO RERENDER THE CANVAS
1513											fabric.Image.fromURL( props.source, ( function( props, i1 ) {
1514												return function( img ) {
1515													images.loaded++;
1516
1517													var pattern = null;
1518													var patternSourceCanvas = new fabric.StaticCanvas( undefined, {
1519														backgroundColor: props.fill
1520													} );
1521													patternSourceCanvas.add( img );
1522
1523													pattern = new fabric.Pattern( {
1524														source: function() {
1525															patternSourceCanvas.setDimensions( {
1526																width: props.width,
1527																height: props.height
1528															} );
1529															return patternSourceCanvas.getElement();
1530														},
1531														repeat: 'repeat'
1532													} );
1533
1534													g.paths[ i1 ].set( {
1535														fill: pattern,
1536														opacity: g.paths[ i1 ].fillOpacity
1537													} );
1538												}
1539											} )( props, i1 ) );
1540										}
1541									}
1542
1543									// CLIPPATH;
1544									if ( PID = _this.isHashbanged( g.paths[ i1 ].clipPath ) ) {
1545
1546										if ( group.clippings && group.clippings[ PID ] ) {
1547
1548											// TODO: WAIT UNTIL FABRICJS HANDLES CLIPPATH FOR SVG OUTPUT
1549											( function( i1, PID ) {
1550												var toSVG = g.paths[ i1 ].toSVG;
1551
1552												g.paths[ i1 ].toSVG = function( original_reviver ) {
1553													return toSVG.apply(this, [ function( string ) {
1554														return original_reviver( string, group.clippings[ PID ] );
1555													} ] );
1556												}
1557											} )( i1, PID );
1558
1559											g.paths[ i1 ].set( {
1560												clipTo: ( function( i1, PID ) {
1561													return function( ctx ) {
1562														var cp = group.clippings[ PID ];
1563														var tm = this.transformMatrix || [ 1, 0, 0, 1, 0, 0 ];
1564														var dim = {
1565															top: ( cp.bbox.y - tm[ 5 ] ) + cp.transform[ 5 ],
1566															left: ( cp.bbox.x - tm[ 4 ] ) + cp.transform[ 4 ],
1567															width: cp.bbox.width,
1568															height: cp.bbox.height
1569														}
1570
1571														ctx.rect( dim.left, dim.top, dim.width, dim.height );
1572													}
1573												} )( i1, PID )
1574											} );
1575										}
1576									}
1577
1578									// TODO; WAIT FOR TSPAN SUPPORT FROM FABRICJS SIDE
1579									if ( g.paths[ i1 ].TSPANWORKAROUND ) {
1580										var parsedAttributes = fabric.parseAttributes( g.paths[ i1 ].svg, fabric.Text.ATTRIBUTE_NAMES );
1581										var options = fabric.util.object.extend( {}, parsedAttributes );
1582
1583										// CREATE NEW SET
1584										var tmpBuffer = [];
1585										for ( var i = 0; i < g.paths[ i1 ].svg.childNodes.length; i++ ) {
1586											var textNode = g.paths[ i1 ].svg.childNodes[ i ];
1587											var textElement = fabric.Text.fromElement( textNode, options );
1588
1589											textElement.set( {
1590												left: 0
1591											} );
1592
1593											tmpBuffer.push( textElement );
1594										}
1595
1596										// HIDE ORIGINAL ELEMENT
1597										g.paths[ i1 ].set( {
1598											opacity: 0
1599										} );
1600
1601										// REPLACE BY GROUP AND CANCEL FIRST OFFSET
1602										var tmpGroup = new fabric.Group( tmpBuffer, {
1603											top: g.paths[ i1 ].top * -1
1604										} );
1605										g.paths[ i1 ] = tmpGroup;
1606									}
1607								}
1608								paths.push( g.paths[ i1 ] );
1609							}
1610
1611							// REPLACE WITH WHITELIST
1612							g.paths = paths;
1613
1614							// SET PROPS
1615							g.set( tmp );
1616
1617							// ADD TO CANVAS
1618							_this.setup.fabric.add( g );
1619
1620							// ADD BALLOONS
1621							if ( group.svg.parentNode && group.svg.parentNode.getElementsByTagName ) {
1622								var balloons = group.svg.parentNode.getElementsByClassName( _this.setup.chart.classNamePrefix + "-balloon-div" );
1623								for ( i1 = 0; i1 < balloons.length; i1++ ) {
1624									if ( cfg.balloonFunction instanceof Function ) {
1625										cfg.balloonFunction.apply( _this, [ balloons[ i1 ], group ] );
1626									} else {
1627										var elm_parent = balloons[ i1 ];
1628										var style_parent = fabric.parseStyleAttribute( elm_parent );
1629										var style_text = fabric.parseStyleAttribute( elm_parent.childNodes[ 0 ] );
1630										var fabric_label = new fabric.Text( elm_parent.innerText || elm_parent.textContent || elm_parent.innerHTML, {
1631											selectable: false,
1632											top: style_parent.top + group.offset.y,
1633											left: style_parent.left + group.offset.x,
1634											fill: style_text[ "color" ],
1635											fontSize: style_text[ "fontSize" ],
1636											fontFamily: style_text[ "fontFamily" ],
1637											textAlign: style_text[ "text-align" ]
1638										} );
1639
1640										_this.setup.fabric.add( fabric_label );
1641									}
1642								}
1643							}
1644
1645							if ( group.svg.nextSibling && group.svg.nextSibling.tagName == "A" ) {
1646								var elm_parent = group.svg.nextSibling;
1647								var style_parent = fabric.parseStyleAttribute( elm_parent );
1648								var fabric_label = new fabric.Text( elm_parent.innerText || elm_parent.textContent || elm_parent.innerHTML, {
1649									selectable: false,
1650									top: style_parent.top + group.offset.y,
1651									left: style_parent.left + group.offset.x,
1652									fill: style_parent[ "color" ],
1653									fontSize: style_parent[ "fontSize" ],
1654									fontFamily: style_parent[ "fontFamily" ],
1655									opacity: style_parent[ "opacity" ]
1656								} );
1657
1658								_this.setup.fabric.add( fabric_label );
1659							}
1660
1661							groups.pop();
1662
1663							// TRIGGER CALLBACK WITH SAFETY DELAY
1664							if ( !groups.length ) {
1665								var timer = setInterval( function() {
1666									if ( images.loaded == images.included ) {
1667										clearTimeout( timer );
1668										_this.handleCallback( cfg.afterCapture, cfg );
1669										_this.setup.fabric.renderAll();
1670										_this.handleCallback( callback, cfg );
1671									}
1672								}, AmCharts.updateRate );
1673							}
1674						}
1675
1676						// IDENTIFY ELEMENTS THROUGH CLASSNAMES
1677					} )( group ), function( svg, obj ) {
1678						var i1;
1679						var className = _this.gatherAttribute( svg, "class" );
1680						var visibility = _this.gatherAttribute( svg, "visibility" );
1681						var clipPath = _this.gatherAttribute( svg, "clip-path" );
1682
1683						obj.className = String( className );
1684						obj.classList = String( className ).split( " " );
1685						obj.clipPath = clipPath;
1686						obj.svg = svg;
1687
1688						// TODO; WAIT FOR TSPAN SUPPORT FROM FABRICJS SIDE
1689						if ( svg.tagName == "text" && svg.childNodes.length > 1 ) {
1690							obj.TSPANWORKAROUND = true;
1691						}
1692
1693						// HIDE HIDDEN ELEMENTS; TODO: FIND A BETTER WAY TO HANDLE THAT
1694						if ( visibility == "hidden" ) {
1695							obj.opacity = 0;
1696
1697							// WALKTHROUGH ELEMENTS
1698						} else {
1699
1700							// TRANSPORT FILL/STROKE OPACITY
1701							var attrs = [ "fill", "stroke" ];
1702							for ( i1 = 0; i1 < attrs.length; i1++ ) {
1703								var attr = attrs[ i1 ]
1704								var attrVal = String( svg.getAttribute( attr ) || "" );
1705								var attrOpacity = Number( svg.getAttribute( attr + "-opacity" ) || "1" );
1706								var attrRGBA = fabric.Color.fromHex( attrVal ).getSource();
1707
1708								// EXCEPTION
1709								if ( obj.classList.indexOf( _this.setup.chart.classNamePrefix + "-guide-fill" ) != -1 && !attrVal ) {
1710									attrOpacity = 0;
1711									attrRGBA = fabric.Color.fromHex( "#000000" ).getSource();
1712								}
1713
1714								if ( attrRGBA ) {
1715									attrRGBA.pop();
1716									attrRGBA.push( attrOpacity )
1717									obj[ attr ] = "rgba(" + attrRGBA.join() + ")";
1718									obj[ attr + _this.capitalize( "opacity" ) ] = attrOpacity;
1719								}
1720							}
1721						}
1722
1723						// REVIVER
1724						_this.handleCallback( cfg.reviver, obj, svg );
1725					} );
1726				}
1727			},
1728
1729			/**
1730			 * Returns the current canvas
1731			 */
1732			toCanvas: function( options, callback ) {
1733				var cfg = _this.deepMerge( {
1734					// NUFFIN
1735				}, options || {} );
1736				var data = _this.setup.canvas;
1737
1738				_this.handleCallback( callback, data );
1739
1740				return data;
1741			},
1742
1743			/**
1744			 * Returns an image; by default PNG
1745			 */
1746			toImage: function( options, callback ) {
1747				var cfg = _this.deepMerge( {
1748					format: "png",
1749					quality: 1,
1750					multiplier: 1
1751				}, options || {} );
1752				var data = cfg.data;
1753				var img = document.createElement( "img" );
1754
1755				if ( !cfg.data ) {
1756					if ( cfg.lossless || cfg.format == "svg" ) {
1757						data = _this.toSVG( _this.deepMerge( cfg, {
1758							getBase64: true
1759						} ) );
1760					} else {
1761						data = _this.setup.fabric.toDataURL( cfg );
1762					}
1763				}
1764
1765				img.setAttribute( "src", data );
1766
1767				_this.handleCallback( callback, img );
1768
1769				return img;
1770			},
1771
1772			/**
1773			 * Generates a blob instance image; returns base64 datastring
1774			 */
1775			toBlob: function( options, callback ) {
1776				var cfg = _this.deepMerge( {
1777					data: "empty",
1778					type: "text/plain"
1779				}, options || {} );
1780				var data;
1781				var isBase64 = /^data:.+;base64,(.*)$/.exec( cfg.data );
1782
1783				// GATHER BODY
1784				if ( isBase64 ) {
1785					cfg.data = isBase64[ 0 ];
1786					cfg.type = cfg.data.slice( 5, cfg.data.indexOf( "," ) - 7 );
1787					cfg.data = _this.toByteArray( {
1788						data: cfg.data.slice( cfg.data.indexOf( "," ) + 1, cfg.data.length )
1789					} );
1790				}
1791
1792				if ( cfg.getByteArray ) {
1793					data = cfg.data;
1794				} else {
1795					data = new Blob( [ cfg.data ], {
1796						type: cfg.type
1797					} );
1798				}
1799
1800				_this.handleCallback( callback, data );
1801
1802				return data;
1803			},
1804
1805			/**
1806			 * Generates JPG image; returns base64 datastring
1807			 */
1808			toJPG: function( options, callback ) {
1809				var cfg = _this.deepMerge( {
1810					format: "jpeg",
1811					quality: 1,
1812					multiplier: 1
1813				}, options || {} );
1814				cfg.format = cfg.format.toLowerCase();
1815				var data = _this.setup.fabric.toDataURL( cfg );
1816
1817				_this.handleCallback( callback, data );
1818
1819				return data;
1820			},
1821
1822			/**
1823			 * Generates PNG image; returns base64 datastring
1824			 */
1825			toPNG: function( options, callback ) {
1826				var cfg = _this.deepMerge( {
1827					format: "png",
1828					quality: 1,
1829					multiplier: 1
1830				}, options || {} );
1831				var data = _this.setup.fabric.toDataURL( cfg );
1832
1833				_this.handleCallback( callback, data );
1834
1835				return data;
1836			},
1837
1838			/**
1839			 * Generates SVG image; returns base64 datastring
1840			 */
1841			toSVG: function( options, callback ) {
1842				var clipPaths = [];
1843				var cfg = _this.deepMerge( {
1844					reviver: function( string, clipPath ) {
1845						var matcher = new RegExp( /\bstyle=(['"])(.*?)\1/ );
1846						var match = matcher.exec( string )[ 0 ].slice( 7, -1 );
1847						var styles = match.split( ";" );
1848						var replacement = [];
1849
1850						// BEAUTIFY STYLES
1851						for ( i1 = 0; i1 < styles.length; i1++ ) {
1852							if ( styles[ i1 ] ) {
1853								var pair = styles[ i1 ].replace( /\s/g, "" ).split( ":" );
1854								var key = pair[ 0 ];
1855								var value = pair[ 1 ];
1856
1857								if ( [ "fill", "stroke" ].indexOf( key ) != -1 ) {
1858									value = fabric.Color.fromRgba( value );
1859									if ( value && value._source ) {
1860										var color = "#" + value.toHex();
1861										var opacity = value._source[ 3 ];
1862
1863										replacement.push( [ key, color ].join( ":" ) );
1864										replacement.push( [ key + "-opacity", opacity ].join( ":" ) );
1865									} else {
1866										replacement.push( styles[ i1 ] );
1867									}
1868								} else if ( key != "opactiy" ) {
1869									replacement.push( styles[ i1 ] );
1870								}
1871							}
1872						}
1873						string = string.replace( match, replacement.join( ";" ) );
1874
1875						// TODO: WAIT UNTIL FABRICJS HANDLES CLIPPATH FOR SVG OUTPUT
1876						if ( clipPath ) {
1877							var sliceOffset = 2;
1878							var end = string.slice( - sliceOffset);
1879
1880							if ( end != "/>" ) {
1881								sliceOffset = 3;
1882								end = string.slice( - sliceOffset);
1883							}
1884
1885							var start = string.slice(0,string.length - sliceOffset);
1886							var clipPathAttr = " clip-path=\"url(#"+ clipPath.svg.id +")\" ";
1887							var clipPathString = new XMLSerializer().serializeToString(clipPath.svg);
1888
1889							string = start + clipPathAttr + end;
1890
1891							clipPaths.push(clipPathString);
1892						}
1893
1894						return string;
1895					}
1896				}, options || {} );
1897				var data = _this.setup.fabric.toSVG( cfg, cfg.reviver );
1898
1899				// TODO: WAIT UNTIL FABRICJS HANDLES CLIPPATH FOR SVG OUTPUT
1900				if ( clipPaths.length ) {
1901					var start = data.slice(0,data.length-6);
1902					var end = data.slice(-6);
1903					data = start + clipPaths.join("") + end;
1904				}
1905
1906				if ( cfg.getBase64 ) {
1907					data = "data:image/svg+xml;base64," + btoa( data );
1908				}
1909
1910				_this.handleCallback( callback, data );
1911
1912				return data;
1913			},
1914
1915			/**
1916			 * Generates PDF; returns base64 datastring
1917			 */
1918			toPDF: function( options, callback ) {
1919				var cfg = _this.deepMerge( _this.deepMerge( {
1920					multiplier: 2
1921				}, _this.config.pdfMake ), options || {}, true );
1922				cfg.images.reference = _this.toPNG( cfg );
1923				var data = new pdfMake.createPdf( cfg );
1924
1925				if ( callback ) {
1926					data.getDataUrl( ( function( callback ) {
1927						return function() {
1928							callback.apply( _this, arguments );
1929						}
1930					} )( callback ) );
1931				}
1932
1933				return data;
1934			},
1935
1936			/**
1937			 * Generates an image; hides all elements on page to trigger native print method
1938			 */
1939			toPRINT: function( options, callback ) {
1940				var i1;
1941				var cfg = _this.deepMerge( {
1942					delay: 1,
1943					lossless: false
1944				}, options || {} );
1945				var data = _this.toImage( cfg );
1946				var states = [];
1947				var items = document.body.childNodes;
1948
1949				data.setAttribute( "style", "width: 100%; max-height: 100%;" );
1950
1951				for ( i1 = 0; i1 < items.length; i1++ ) {
1952					if ( _this.isElement( items[ i1 ] ) ) {
1953						states[ i1 ] = items[ i1 ].style.display;
1954						items[ i1 ].style.display = "none";
1955					}
1956				}
1957
1958				document.body.appendChild( data );
1959				window.print();
1960
1961				setTimeout( function() {
1962					for ( i1 = 0; i1 < items.length; i1++ ) {
1963						if ( _this.isElement( items[ i1 ] ) ) {
1964							items[ i1 ].style.display = states[ i1 ];
1965						}
1966					}
1967					document.body.removeChild( data );
1968					_this.handleCallback( callback, data );
1969				}, cfg.delay );
1970
1971				return data;
1972			},
1973
1974			/**
1975			 * Generates JSON string
1976			 */
1977			toJSON: function( options, callback ) {
1978				var cfg = _this.deepMerge( {
1979					dateFormat: _this.config.dateFormat || "dateObject",
1980				}, options || {}, true );
1981				cfg.data = cfg.data ? cfg.data : _this.getChartData( cfg );
1982				var data = JSON.stringify( cfg.data, undefined, "\t" );
1983
1984				_this.handleCallback( callback, data );
1985
1986				return data;
1987			},
1988
1989			/**
1990			 * Generates CSV string
1991			 */
1992			toCSV: function( options, callback ) {
1993				var row, col;
1994				var cfg = _this.deepMerge( {
1995					data: _this.getChartData( options ),
1996					delimiter: ",",
1997					quotes: true,
1998					escape: true,
1999					withHeader: true
2000				}, options || {}, true );
2001				var data = "";
2002				var cols = [];
2003				var buffer = [];
2004
2005				function enchant( value, column ) {
2006
2007					// WRAP IN QUOTES
2008					if ( typeof value === "string" ) {
2009						if ( cfg.escape ) {
2010							value = value.replace( '"', '""' );
2011						}
2012						if ( cfg.quotes ) {
2013							value = [ '"', value, '"' ].join( "" );
2014						}
2015					}
2016
2017					return value;
2018				}
2019
2020				// HEADER
2021				for ( value in cfg.data[ 0 ] ) {
2022					buffer.push( enchant( value ) );
2023					cols.push( value );
2024				}
2025				if ( cfg.withHeader ) {
2026					data += buffer.join( cfg.delimiter ) + "\n";
2027				}
2028
2029				// BODY
2030				for ( row in cfg.data ) {
2031					buffer = [];
2032					if ( !isNaN( row ) ) {
2033						for ( col in cols ) {
2034							if ( !isNaN( col ) ) {
2035								var column = cols[ col ];
2036								var value = cfg.data[ row ][ column ];
2037
2038								buffer.push( enchant( value, column ) );
2039							}
2040						}
2041						data += buffer.join( cfg.delimiter ) + "\n";
2042					}
2043				}
2044
2045				_this.handleCallback( callback, data );
2046
2047				return data;
2048			},
2049
2050			/**
2051			 * Generates excel sheet; returns base64 datastring
2052			 */
2053			toXLSX: function( options, callback ) {
2054				var cfg = _this.deepMerge( {
2055					name: "amCharts",
2056					dateFormat: _this.config.dateFormat || "dateObject",
2057					withHeader: true,
2058					stringify: false
2059				}, options || {}, true );
2060				var data = "";
2061				var wb = {
2062					SheetNames: [],
2063					Sheets: {}
2064				}
2065
2066				cfg.data = cfg.data ? cfg.data : _this.getChartData( cfg );
2067
2068				function datenum( v, date1904 ) {
2069					if ( date1904 ) v += 1462;
2070					var epoch = Date.parse( v );
2071					return ( epoch - new Date( Date.UTC( 1899, 11, 30 ) ) ) / ( 24 * 60 * 60 * 1000 );
2072				}
2073
2074				function sheet_from_array_of_arrays( data, opts ) {
2075					var ws = {};
2076					var range = {
2077						s: {
2078							c: 10000000,
2079							r: 10000000
2080						},
2081						e: {
2082							c: 0,
2083							r: 0
2084						}
2085					};
2086					for ( var R = 0; R != data.length; ++R ) {
2087						for ( var C = 0; C != data[ R ].length; ++C ) {
2088							if ( range.s.r > R ) range.s.r = R;
2089							if ( range.s.c > C ) range.s.c = C;
2090							if ( range.e.r < R ) range.e.r = R;
2091							if ( range.e.c < C ) range.e.c = C;
2092							var cell = {
2093								v: data[ R ][ C ]
2094							};
2095							if ( cell.v == null ) continue;
2096							var cell_ref = XLSX.utils.encode_cell( {
2097								c: C,
2098								r: R
2099							} );
2100
2101							if ( typeof cell.v === "number" ) cell.t = "n";
2102							else if ( typeof cell.v === "boolean" ) cell.t = "b";
2103							else if ( cell.v instanceof Date ) {
2104								cell.t = "n";
2105								cell.z = XLSX.SSF._table[ 14 ];
2106								cell.v = datenum( cell.v );
2107							} else cell.t = "s";
2108
2109							ws[ cell_ref ] = cell;
2110						}
2111					}
2112					if ( range.s.c < 10000000 ) ws[ "!ref" ] = XLSX.utils.encode_range( range );
2113					return ws;
2114				}
2115
2116				wb.SheetNames.push( cfg.name );
2117				wb.Sheets[ cfg.name ] = sheet_from_array_of_arrays( _this.toArray( cfg ) );
2118
2119				data = XLSX.write( wb, {
2120					bookType: "xlsx",
2121					bookSST: true,
2122					type: "base64"
2123				} );
2124
2125				data = "data:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;base64," + data;
2126
2127				_this.handleCallback( callback, data );
2128
2129				return data;
2130			},
2131
2132			/**
2133			 * Generates an array of arrays
2134			 */
2135			toArray: function( options, callback ) {
2136				var row, col;
2137				var cfg = _this.deepMerge( {
2138					data: _this.getChartData( options ),
2139					withHeader: false,
2140					stringify: true
2141				}, options || {}, true );
2142				var data = [];
2143				var cols = [];
2144
2145				// HEADER
2146				for ( col in cfg.data[ 0 ] ) {
2147					cols.push( col );
2148				}
2149				if ( cfg.withHeader ) {
2150					data.push( cols );
2151				}
2152
2153				// BODY
2154				for ( row in cfg.data ) {
2155					var buffer = [];
2156					if ( !isNaN( row ) ) {
2157						for ( col in cols ) {
2158							if ( !isNaN( col ) ) {
2159								var col = cols[ col ];
2160								var value = cfg.data[ row ][ col ];
2161								if ( value == null ) {
2162									value = "";
2163								} else if ( cfg.stringify ) {
2164									value = String( value );
2165								} else {
2166									value = value;
2167								}
2168								buffer.push( value );
2169							}
2170						}
2171						data.push( buffer );
2172					}
2173				}
2174
2175				_this.handleCallback( callback, data );
2176
2177				return data;
2178			},
2179
2180			/**
2181			 * Generates byte array with given base64 datastring; returns byte array
2182			 */
2183			toByteArray: function( options, callback ) {
2184				var cfg = _this.deepMerge( {
2185					// NUFFIN
2186				}, options || {} );
2187				var Arr = ( typeof Uint8Array !== 'undefined' ) ? Uint8Array : Array
2188				var PLUS = '+'.charCodeAt( 0 )
2189				var SLASH = '/'.charCodeAt( 0 )
2190				var NUMBER = '0'.charCodeAt( 0 )
2191				var LOWER = 'a'.charCodeAt( 0 )
2192				var UPPER = 'A'.charCodeAt( 0 )
2193				var data = b64ToByteArray( cfg.data );
2194
2195				function decode( elt ) {
2196					var code = elt.charCodeAt( 0 )
2197					if ( code === PLUS )
2198						return 62 // '+'
2199					if ( code === SLASH )
2200						return 63 // '/'
2201					if ( code < NUMBER )
2202						return -1 //no match
2203					if ( code < NUMBER + 10 )
2204						return code - NUMBER + 26 + 26
2205					if ( code < UPPER + 26 )
2206						return code - UPPER
2207					if ( code < LOWER + 26 )
2208						return code - LOWER + 26
2209				}
2210
2211				function b64ToByteArray( b64 ) {
2212					var i, j, l, tmp, placeHolders, arr
2213
2214					if ( b64.length % 4 > 0 ) {
2215						throw new Error( 'Invalid string. Length must be a multiple of 4' )
2216					}
2217
2218					// THE NUMBER OF EQUAL SIGNS (PLACE HOLDERS)
2219					// IF THERE ARE TWO PLACEHOLDERS, THAN THE TWO CHARACTERS BEFORE IT
2220					// REPRESENT ONE BYTE
2221					// IF THERE IS ONLY ONE, THEN THE THREE CHARACTERS BEFORE IT REPRESENT 2 BYTES
2222					// THIS IS JUST A CHEAP HACK TO NOT DO INDEXOF TWICE
2223					var len = b64.length
2224					placeHolders = '=' === b64.charAt( len - 2 ) ? 2 : '=' === b64.charAt( len - 1 ) ? 1 : 0
2225
2226					// BASE64 IS 4/3 + UP TO TWO CHARACTERS OF THE ORIGINAL DATA
2227					arr = new Arr( b64.length * 3 / 4 - placeHolders )
2228
2229					// IF THERE ARE PLACEHOLDERS, ONLY GET UP TO THE LAST COMPLETE 4 CHARS
2230					l = placeHolders > 0 ? b64.length - 4 : b64.length
2231
2232					var L = 0
2233
2234					function push( v ) {
2235						arr[ L++ ] = v
2236					}
2237
2238					for ( i = 0, j = 0; i < l; i += 4, j += 3 ) {
2239						tmp = ( decode( b64.charAt( i ) ) << 18 ) | ( decode( b64.charAt( i + 1 ) ) << 12 ) | ( decode( b64.charAt( i + 2 ) ) << 6 ) | decode( b64.charAt( i + 3 ) )
2240						push( ( tmp & 0xFF0000 ) >> 16 )
2241						push( ( tmp & 0xFF00 ) >> 8 )
2242						push( tmp & 0xFF )
2243					}
2244
2245					if ( placeHolders === 2 ) {
2246						tmp = ( decode( b64.charAt( i ) ) << 2 ) | ( decode( b64.charAt( i + 1 ) ) >> 4 )
2247						push( tmp & 0xFF )
2248					} else if ( placeHolders === 1 ) {
2249						tmp = ( decode( b64.charAt( i ) ) << 10 ) | ( decode( b64.charAt( i + 1 ) ) << 4 ) | ( decode( b64.charAt( i + 2 ) ) >> 2 )
2250						push( ( tmp >> 8 ) & 0xFF )
2251						push( tmp & 0xFF )
2252					}
2253
2254					return arr
2255				}
2256
2257				_this.handleCallback( callback, data );
2258
2259				return data;
2260			},
2261
2262			/**
2263			 * Callback handler; injects additional arguments to callback
2264			 */
2265			handleCallback: function( callback ) {
2266				var i1, data = Array();
2267				if ( callback && callback instanceof Function ) {
2268					for ( i1 = 0; i1 < arguments.length; i1++ ) {
2269						if ( i1 > 0 ) {
2270							data.push( arguments[ i1 ] );
2271						}
2272					}
2273					callback.apply( _this, data );
2274				}
2275			},
2276
2277			/**
2278			 * Handles drag/drop events; loads given imagery
2279			 */
2280			handleDropbox: function( e ) {
2281				if ( _this.drawing.buffer.enabled ) {
2282					e.preventDefault();
2283					e.stopPropagation();
2284
2285					// DRAG OVER
2286					if ( e.type == "dragover" ) {
2287						_this.setup.wrapper.setAttribute( "class", _this.setup.chart.classNamePrefix + "-export-canvas active dropbox" );
2288
2289						// DRAGLEAVE; DROP
2290					} else {
2291						_this.setup.wrapper.setAttribute( "class", _this.setup.chart.classNamePrefix + "-export-canvas active" );
2292
2293						if ( e.type == "drop" && e.dataTransfer.files.length ) {
2294							for ( var i1 = 0; i1 < e.dataTransfer.files.length; i1++ ) {
2295								var reader = new FileReader();
2296								reader.onloadend = ( function( index ) {
2297									return function() {
2298										_this.drawing.handler.add( {
2299											url: reader.result,
2300											top: e.layerY - ( index * 10 ),
2301											left: e.layerX - ( index * 10 )
2302										} );
2303									}
2304								} )( i1 );
2305								reader.readAsDataURL( e.dataTransfer.files[ i1 ] );
2306							}
2307						}
2308					}
2309				}
2310			},
2311
2312			/**
2313			 * Gathers chart data according to its type
2314			 */
2315			getChartData: function( options ) {
2316				var cfg = _this.deepMerge( {
2317					data: [],
2318					titles: {},
2319					dateFields: [],
2320					dataFields: [],
2321					dataFieldsMap: {},
2322					exportTitles: _this.config.exportTitles,
2323					exportFields: _this.config.exportFields,
2324					exportSelection: _this.config.exportSelection,
2325					columnNames: _this.config.columnNames
2326				}, options || {}, true );
2327				var uid, i1, i2, i3;
2328				var lookupFields = [ "valueField", "openField", "closeField", "highField", "lowField", "xField", "yField" ];
2329
2330				// HANDLE FIELDS
2331				function addField( field, title, type ) {
2332
2333					function checkExistance( field, type ) {
2334						if ( cfg.dataFields.indexOf( field ) != -1 ) {
2335							return checkExistance( [ field, ".", type ].join( "" ) );
2336						}
2337						return field;
2338					}
2339
2340					if ( field && cfg.exportTitles && _this.setup.chart.type != "gantt" ) {
2341						uid = checkExistance( field, type );
2342						cfg.dataFieldsMap[ uid ] = field;
2343						cfg.dataFields.push( uid );
2344						cfg.titles[ uid ] = title || uid;
2345					}
2346				}
2347
2348				if ( cfg.data.length == 0 ) {
2349					// STOCK DATA; GATHER COMPARED GRAPHS
2350					if ( _this.setup.chart.type == "stock" ) {
2351						cfg.data = _this.setup.chart.mainDataSet.dataProvider;
2352
2353						// CATEGORY AXIS
2354						addField( _this.setup.chart.mainDataSet.categoryField );
2355						cfg.dateFields.push( _this.setup.chart.mainDataSet.categoryField );
2356
2357						// WALKTHROUGH GRAPHS
2358						for ( i1 = 0; i1 < _this.setup.chart.mainDataSet.fieldMappings.length; i1++ ) {
2359							var fieldMap = _this.setup.chart.mainDataSet.fieldMappings[ i1 ];
2360							for ( i2 = 0; i2 < _this.setup.chart.panels.length; i2++ ) {
2361								var panel = _this.setup.chart.panels[ i2 ]
2362								for ( i3 = 0; i3 < panel.stockGraphs.length; i3++ ) {
2363									var graph = panel.stockGraphs[ i3 ];
2364
2365									for ( i4 = 0; i4 < lookupFields.length; i4++ ) {
2366										if ( graph[ lookupFields[ i4 ] ] == fieldMap.toField ) {
2367											addField( fieldMap.fromField, graph.title, lookupFields[ i4 ] );
2368										}
2369									}
2370								}
2371							}
2372						}
2373
2374						// WALKTHROUGH COMPARISON AND MERGE IT'S DATA
2375						for ( i1 = 0; i1 < _this.setup.chart.comparedGraphs.length; i1++ ) {
2376							var graph = _this.setup.chart.comparedGraphs[ i1 ];
2377							for ( i2 = 0; i2 < graph.dataSet.dataProvider.length; i2++ ) {
2378								for ( i3 = 0; i3 < graph.dataSet.fieldMappings.length; i3++ ) {
2379									var fieldMap = graph.dataSet.fieldMappings[ i3 ];
2380									var uid = graph.dataSet.id + "_" + fieldMap.toField;
2381
2382									if ( i2 < cfg.data.length ) {
2383										cfg.data[ i2 ][ uid ] = graph.dataSet.dataProvider[ i2 ][ fieldMap.fromField ];
2384
2385										if ( !cfg.titles[ uid ] ) {
2386											addField( uid, graph.dataSet.title )
2387										}
2388									}
2389								}
2390							}
2391						}
2392
2393						// GANTT DATA; FLATTEN SEGMENTS
2394					} else if ( _this.setup.chart.type == "gantt" ) {
2395						// CATEGORY AXIS
2396						addField( _this.setup.chart.categoryField );
2397						cfg.dateFields.push( _this.setup.chart.categoryField );
2398
2399						var field = _this.setup.chart.segmentsField;
2400						for ( i1 = 0; i1 < _this.setup.chart.dataProvider.length; i1++ ) {
2401							var dataItem = _this.setup.chart.dataProvider[ i1 ];
2402							if ( dataItem[ field ] ) {
2403								for ( i2 = 0; i2 < dataItem[ field ].length; i2++ ) {
2404									dataItem[ field ][ i2 ][ _this.setup.chart.categoryField ] = dataItem[ _this.setup.chart.categoryField ];
2405									cfg.data.push( dataItem[ field ][ i2 ] );
2406								}
2407							}
2408						}
2409
2410						// GRAPHS
2411						for ( i1 = 0; i1 < _this.setup.chart.graphs.length; i1++ ) {
2412							var graph = _this.setup.chart.graphs[ i1 ];
2413
2414							for ( i2 = 0; i2 < lookupFields.length; i2++ ) {
2415								var dataField = lookupFields[ i2 ];
2416								var graphField = graph[ dataField ];
2417								var title = graph.title;
2418
2419								addField( graphField, graph.title, dataField );
2420							}
2421						}
2422
2423						// PIE/FUNNEL DATA;
2424					} else if ( [ "pie", "funnel" ].indexOf( _this.setup.chart.type ) != -1 ) {
2425						cfg.data = _this.setup.chart.dataProvider;
2426
2427						// CATEGORY AXIS
2428						addField( _this.setup.chart.titleField );
2429						cfg.dateFields.push( _this.setup.chart.titleField );
2430
2431						// VALUE
2432						addField( _this.setup.chart.valueField );
2433
2434						// DEFAULT DATA;
2435					} else if ( _this.setup.chart.type != "map" ) {
2436						cfg.data = _this.setup.chart.dataProvider;
2437
2438						// CATEGORY AXIS
2439						if ( _this.setup.chart.categoryAxis ) {
2440							addField( _this.setup.chart.categoryField, _this.setup.chart.categoryAxis.title );
2441							if ( _this.setup.chart.categoryAxis.parseDates !== false ) {
2442								cfg.dateFields.push( _this.setup.chart.categoryField );
2443							}
2444						}
2445
2446						// GRAPHS
2447						for ( i1 = 0; i1 < _this.setup.chart.graphs.length; i1++ ) {
2448							var graph = _this.setup.chart.graphs[ i1 ];
2449
2450							for ( i2 = 0; i2 < lookupFields.length; i2++ ) {
2451								var dataField = lookupFields[ i2 ];
2452								var graphField = graph[ dataField ];
2453
2454								addField( graphField, graph.title, dataField );
2455							}
2456						}
2457					}
2458				}
2459				return _this.processData( cfg );
2460			},
2461
2462			/**
2463			 * Walkthrough data to format dates and titles
2464			 */
2465			processData: function( options ) {
2466				var cfg = _this.deepMerge( {
2467					data: [],
2468					titles: {},
2469					dateFields: [],
2470					dataFields: [],
2471					dataFieldsMap: {},
2472					dataDateFormat: _this.setup.chart.dataDateFormat,
2473					dateFormat: _this.config.dateFormat || _this.setup.chart.dataDateFormat || "YYYY-MM-DD",
2474					exportTitles: _this.config.exportTitles,
2475					exportFields: _this.config.exportFields,
2476					exportSelection: _this.config.exportSelection,
2477					columnNames: _this.config.columnNames
2478				}, options || {}, true );
2479				var i1, i2;
2480
2481				if ( cfg.data.length ) {
2482					// GATHER MISSING FIELDS
2483					for ( i1 = 0; i1 < cfg.data.length; i1++ ) {
2484						for ( i2 in cfg.data[ i1 ] ) {
2485							if ( cfg.dataFields.indexOf( i2 ) == -1 ) {
2486								cfg.dataFields.push( i2 );
2487								cfg.dataFieldsMap[ i2 ] = i2;
2488							}
2489						}
2490					}
2491
2492					// REMOVE FIELDS SELECTIVELY
2493					if ( cfg.exportFields !== undefined ) {
2494						cfg.dataFields = cfg.dataFields.filter( function( n ) {
2495							return cfg.exportFields.indexOf( n ) != -1;
2496						} );
2497					}
2498
2499					// REBUILD DATA
2500					var buffer = [];
2501					for ( i1 = 0; i1 < cfg.data.length; i1++ ) {
2502						var tmp = {};
2503						var skip = false;
2504						for ( i2 = 0; i2 < cfg.dataFields.length; i2++ ) {
2505							var uniqueField = cfg.dataFields[ i2 ];
2506							var dataField = cfg.dataFieldsMap[ uniqueField ];
2507							var title = ( cfg.columnNames && cfg.columnNames[ uniqueField ] ) || cfg.titles[ uniqueField ] || uniqueField;
2508							var value = cfg.data[ i1 ][ dataField ];
2509							if ( value == null ) {
2510								value = undefined;
2511							}
2512
2513							// TITLEFY
2514							if ( cfg.exportTitles && _this.setup.chart.type != "gantt" ) {
2515								if ( title in tmp ) {
2516									title += [ "( ", uniqueField, " )" ].join( "" );
2517								}
2518							}
2519
2520							// PROCESS CATEGORY
2521							if ( cfg.dateFields.indexOf( dataField ) != -1 ) {
2522
2523								// CONVERT DATESTRING TO DATE OBJECT
2524								if ( cfg.dataDateFormat && ( value instanceof String || typeof value == "string" ) ) {
2525									value = AmCharts.stringToDate( value, cfg.dataDateFormat );
2526
2527									// CONVERT TIMESTAMP TO DATE OBJECT
2528								} else if ( cfg.dateFormat && ( value instanceof Number || typeof value == "number" ) ) {
2529									value = new Date( value );
2530								}
2531
2532								// CATEGORY RANGE
2533								if ( cfg.exportSelection ) {
2534									if ( value instanceof Date ) {
2535										if ( value < chart.startDate || value > chart.endDate ) {
2536											skip = true;
2537										}
2538
2539									} else if ( i1 < chart.startIndex || i1 > chart.endIndex ) {
2540										skip = true;
2541									}
2542								}
2543
2544								// CATEGORY FORMAT
2545								if ( cfg.dateFormat && cfg.dateFormat != "dateObject" && value instanceof Date ) {
2546									value = AmCharts.formatDate( value, cfg.dateFormat );
2547								}
2548							}
2549
2550							tmp[ title ] = value;
2551						}
2552						if ( !skip ) {
2553							buffer.push( tmp );
2554						}
2555					}
2556					cfg.data = buffer;
2557				}
2558				return cfg.data;
2559			},
2560
2561			/**
2562			 * Prettifies string
2563			 */
2564			capitalize: function( string ) {
2565				return string.charAt( 0 ).toUpperCase() + string.slice( 1 ).toLowerCase();
2566			},
2567
2568			/**
2569			 * Generates export menu; returns UL node
2570			 */
2571			createMenu: function( list, container ) {
2572				var div;
2573
2574				function buildList( list, container ) {
2575					var i1, i2, ul = document.createElement( "ul" );
2576					for ( i1 = 0; i1 < list.length; i1++ ) {
2577						var item = typeof list[ i1 ] === "string" ? {
2578							format: list[ i1 ]
2579						} : list[ i1 ];
2580						var li = document.createElement( "li" );
2581						var a = document.createElement( "a" );
2582						var img = document.createElement( "img" );
2583						var span = document.createElement( "span" );
2584						var action = String( item.action ? item.action : item.format ).toLowerCase();
2585
2586						item.format = String( item.format ).toUpperCase();
2587
2588						// MERGE WITH GIVEN FORMAT
2589						if ( _this.config.formats[ item.format ] ) {
2590							item = _this.deepMerge( {
2591								label: item.icon ? "" : item.format,
2592								format: item.format,
2593								mimeType: _this.config.formats[ item.format ].mimeType,
2594								extension: _this.config.formats[ item.format ].extension,
2595								capture: _this.config.formats[ item.format ].capture,
2596								action: _this.config.action,
2597								fileName: _this.config.fileName
2598							}, item );
2599						} else if ( !item.label ) {
2600							item.label = item.label ? item.label : _this.i18l( "menu.label." + action );
2601						}
2602
2603						// FILTER; TOGGLE FLAG
2604						if ( [ "CSV", "JSON", "XLSX" ].indexOf( item.format ) != -1 && [ "map", "gauge" ].indexOf( _this.setup.chart.type ) != -1 ) {
2605							continue;
2606
2607							// BLOB EXCEPTION
2608						} else if ( !_this.setup.hasBlob && item.format != "UNDEFINED" ) {
2609							if ( item.mimeType && item.mimeType.split( "/" )[ 0 ] != "image" && item.mimeType != "text/plain" ) {
2610								continue;
2611							}
2612						}
2613
2614						// DRAWING
2615						if ( item.action == "draw" ) {
2616							if ( _this.config.fabric.drawing.enabled ) {
2617								item.menu = item.menu ? item.menu : _this.config.fabric.drawing.menu;
2618								item.click = ( function( item ) {
2619									return function() {
2620										this.capture( item, function() {
2621											this.createMenu( item.menu );
2622										} );
2623									}
2624								} )( item );
2625							} else {
2626								item.menu = [];
2627							}
2628
2629							// DRAWING CHOICES
2630						} else if ( !item.populated && item.action && item.action.indexOf( "draw." ) != -1 ) {
2631							var type = item.action.split( "." )[ 1 ];
2632							var items = item[ type ] || _this.config.fabric.drawing[ type ] || [];
2633
2634							item.menu = [];
2635							item.populated = true;
2636
2637							for ( i2 = 0; i2 < items.length; i2++ ) {
2638								var tmp = {
2639									"label": items[ i2 ]
2640								}
2641
2642								if ( type == "shapes" ) {
2643									var io = items[ i2 ].indexOf( "//" ) == -1;
2644									var url = ( io ? _this.config.path + "shapes/" : "" ) + items[ i2 ];
2645
2646									tmp.action = "add";
2647									tmp.url = url;
2648									tmp.icon = url;
2649									tmp.ignore = io;
2650									tmp[ "class" ] = "export-drawing-shape";
2651
2652								} else if ( type == "colors" ) {
2653									tmp.style = "background-color: " + items[ i2 ];
2654									tmp.action = "change";
2655									tmp.color = items[ i2 ];
2656									tmp[ "class" ] = "export-drawing-color";
2657
2658								} else if ( type == "widths" ) {
2659									tmp.action = "change";
2660									tmp.width = items[ i2 ];
2661									tmp.label = document.createElement( "span" );
2662
2663									tmp.label.style.width = _this.numberToPx( items[ i2 ] );
2664									tmp.label.style.height = _this.numberToPx( items[ i2 ] );
2665									tmp[ "class" ] = "export-drawing-width";
2666								} else if ( type == "opacities" ) {
2667									tmp.style = "opacity: " + items[ i2 ];
2668									tmp.action = "change";
2669									tmp.opacity = items[ i2 ];
2670									tmp.label = ( items[ i2 ] * 100 ) + "%";
2671									tmp[ "class" ] = "export-drawing-opacity";
2672								} else if ( type == "modes" ) {
2673									tmp.label = _this.i18l( "menu.label.draw.modes." + items[ i2 ] );
2674									tmp.click = ( function( mode ) {
2675										return function() {
2676											_this.drawing.mode = mode;
2677										}
2678									} )( items[ i2 ] );
2679									tmp[ "class" ] = "export-drawing-mode";
2680								}
2681
2682								item.menu.push( tmp );
2683							}
2684
2685							// ADD CLICK HANDLER
2686						} else if ( !item.click && !item.menu && !item.items ) {
2687							// DRAWING METHODS
2688							if ( _this.drawing.handler[ action ] instanceof Function ) {
2689								item.action = action;
2690								item.click = ( function( item ) {
2691									return function() {
2692										this.drawing.handler[ item.action ]( item );
2693									}
2694								} )( item );
2695
2696								// DRAWING
2697							} else if ( _this.drawing.buffer.enabled ) {
2698								item.click = ( function( item ) {
2699									return function() {
2700										if ( this.config.drawing.autoClose ) {
2701											this.drawing.handler.done();
2702										}
2703										this[ "to" + item.format ]( item, function( data ) {
2704											if ( item.action == "download" ) {
2705												this.download( data, item.mimeType, [ item.fileName, item.extension ].join( "." ) );
2706											}
2707										} );
2708									}
2709								} )( item );
2710
2711								// REGULAR
2712							} else if ( item.format != "UNDEFINED" ) {
2713								item.click = ( function( item ) {
2714									return function() {
2715										if ( item.capture || item.action == "print" || item.format == "PRINT" ) {
2716											this.capture( item, function() {
2717												if ( this.config.drawing.autoClose ) {
2718													this.drawing.handler.done();
2719												}
2720												this[ "to" + item.format ]( item, function( data ) {
2721													if ( item.action == "download" ) {
2722														this.download( data, item.mimeType, [ item.fileName, item.extension ].join( "." ) );
2723													}
2724												} );
2725											} )
2726
2727										} else if ( this[ "to" + item.format ] ) {
2728											this[ "to" + item.format ]( item, function( data ) {
2729												this.download( data, item.mimeType, [ item.fileName, item.extension ].join( "." ) );
2730											} );
2731										} else {
2732											throw new Error( 'Invalid format. Could not determine output type.' );
2733										}
2734									}
2735								} )( item );
2736							}
2737						}
2738
2739						// HIDE EMPTY ONES
2740						if ( item.menu !== undefined && !item.menu.length ) {
2741							continue;
2742						}
2743
2744						// ADD LINK ATTR
2745						a.setAttribute( "href", "#" );
2746						a.addEventListener( "click", ( function( callback, item ) {
2747							return function( e ) {
2748								e.preventDefault();
2749								var args = [ e, item ];
2750
2751								// DELAYED
2752								if ( ( item.action == "draw" || item.format == "PRINT" || ( item.format != "UNDEFINED" && item.capture ) ) && !_this.drawing.enabled ) {
2753									item.delay = item.delay ? item.delay : _this.config.delay;
2754									if ( item.delay ) {
2755										_this.delay( item, callback );
2756										return;
2757									}
2758								}
2759
2760								callback.apply( _this, args );
2761							}
2762						} )( item.click || function( e ) {
2763							e.preventDefault();
2764						}, item ) );
2765						li.appendChild( a );
2766
2767						// ADD LABEL
2768						if ( _this.isElement( item.label ) ) {
2769							span.appendChild( item.label );
2770						} else {
2771							span.innerHTML = item.label;
2772						}
2773
2774						// APPEND ITEMS
2775						if ( item[ "class" ] ) {
2776							li.className = item[ "class" ];
2777						}
2778
2779						if ( item.style ) {
2780							li.setAttribute( "style", item.style );
2781						}
2782
2783						if ( item.icon ) {
2784							img.setAttribute( "src", ( !item.ignore && item.icon.slice( 0, 10 ).indexOf( "//" ) == -1 ? chart.pathToImages : "" ) + item.icon );
2785							a.appendChild( img );
2786						}
2787						if ( item.label ) {
2788							a.appendChild( span );
2789						}
2790						if ( item.title ) {
2791							a.setAttribute( "title", item.title );
2792						}
2793
2794						// CALLBACK; REVIVER FOR MENU ITEMS
2795						if ( _this.config.menuReviver ) {
2796							li = _this.config.menuReviver.apply( _this, [ item, li ] );
2797						}
2798
2799						// ADD ELEMENTS FOR EASY ACCESS
2800						item.elements = {
2801							li: li,
2802							a: a,
2803							img: img,
2804							span: span
2805						}
2806
2807						// ADD SUBLIST; JUST WITH ENTRIES
2808						if ( ( item.menu || item.items ) && item.action != "draw" ) {
2809							if ( buildList( item.menu || item.items, li ).childNodes.length ) {
2810								ul.appendChild( li );
2811							}
2812						} else {
2813							ul.appendChild( li );
2814						}
2815					}
2816
2817					// JUST ADD THOSE WITH ENTRIES
2818					if ( ul.childNodes.length ) {
2819						container.appendChild( ul );
2820					}
2821
2822					return ul;
2823				}
2824
2825				// DETERMINE CONTAINER
2826				if ( !container ) {
2827					if ( typeof _this.config.divId == "string" ) {
2828						_this.config.divId = container = document.getElementById( _this.config.divId );
2829					} else if ( _this.isElement( _this.config.divId ) ) {
2830						container = _this.config.divId;
2831					} else {
2832						container = _this.setup.chart.containerDiv;
2833					}
2834				}
2835
2836				// CREATE / RESET MENU CONTAINER
2837				if ( _this.isElement( _this.setup.menu ) ) {
2838					_this.setup.menu.innerHTML = "";
2839				} else {
2840					_this.setup.menu = document.createElement( "div" );
2841				}
2842				_this.setup.menu.setAttribute( "class", _this.setup.chart.classNamePrefix + "-export-menu " + _this.setup.chart.classNamePrefix + "-export-menu-" + _this.config.position + " amExportButton" );
2843
2844				// CALLBACK; REPLACES THE MENU WALKER
2845				if ( _this.config.menuWalker ) {
2846					buildList = _this.config.menuWalker;
2847				}
2848				buildList.apply( this, [ list, _this.setup.menu ] );
2849
2850				// JUST ADD THOSE WITH ENTRIES
2851				if ( _this.setup.menu.childNodes.length ) {
2852					container.appendChild( _this.setup.menu );
2853				}
2854
2855				return _this.setup.menu;
2856			},
2857
2858			/**
2859			 * Method to trigger the callback delayed
2860			 */
2861			delay: function( options, callback ) {
2862				var cfg = _this.deepMerge( {
2863					delay: 3,
2864					precision: 2
2865				}, options || {} );
2866				var t1, t2, start = Number( new Date() );
2867				var menu = _this.createMenu( [ {
2868					label: _this.i18l( "capturing.delayed.menu.label" ).replace( "{{duration}}", AmCharts.toFixed( cfg.delay, cfg.precision ) ),
2869					title: _this.i18l( "capturing.delayed.menu.title" ),
2870					"class": "export-delayed-capturing",
2871					click: function() {
2872						clearTimeout( t1 );
2873						clearTimeout( t2 );
2874						_this.createMenu( _this.config.menu );
2875					}
2876				} ] );
2877				var label = menu.getElementsByTagName( "a" )[ 0 ];
2878
2879				// MENU UPDATE
2880				t1 = setInterval( function() {
2881					var diff = cfg.delay - ( Number( new Date() ) - start ) / 1000;
2882					if ( diff <= 0 ) {
2883						clearTimeout( t1 );
2884						if ( cfg.action != "draw" ) {
2885							_this.createMenu( _this.config.menu );
2886						}
2887					} else if ( label ) {
2888						label.innerHTML = _this.i18l( "capturing.delayed.menu.label" ).replace( "{{duration}}", AmCharts.toFixed( diff, 2 ) );
2889					}
2890				}, 10 );
2891
2892				// CALLBACK
2893				t2 = setTimeout( function() {
2894					callback.apply( _this, arguments );
2895				}, cfg.delay * 1000 );
2896			},
2897
2898			/**
2899			 * Migration method to support old export setup
2900			 */
2901			migrateSetup: function( setup ) {
2902				var cfg = {
2903					enabled: true,
2904					migrated: true,
2905					libs: {
2906						autoLoad: true
2907					},
2908					menu: []
2909				};
2910
2911				function crawler( object ) {
2912					var key;
2913					for ( key in object ) {
2914						var value = object[ key ];
2915
2916						if ( key.slice( 0, 6 ) == "export" && value ) {
2917							cfg.menu.push( key.slice( 6 ) );
2918						} else if ( key == "userCFG" ) {
2919							crawler( value );
2920						} else if ( key == "menuItems" ) {
2921							cfg.menu = value;
2922						} else if ( key == "libs" ) {
2923							cfg.libs = value;
2924						} else if ( typeof key == "string" ) {
2925							cfg[ key ] = value;
2926						}
2927					}
2928				}
2929
2930				crawler( setup );
2931
2932				return cfg;
2933			},
2934
2935			/*
2936			 ** Add event listener
2937			 */
2938			loadListeners: function() {
2939				function handleClone( clone ) {
2940					if ( clone ) {
2941						clone.set( {
2942							top: clone.top + 10,
2943							left: clone.left + 10
2944						} );
2945						_this.setup.fabric.add( clone );
2946					}
2947				}
2948
2949				// OBSERVE; KEY LISTENER; DRAWING FEATURES
2950				if ( _this.config.keyListener && _this.config.keyListener != "attached" ) {
2951					_this.config.keyListener = "attached";
2952					document.addEventListener( "keydown", function( e ) {
2953						var current = _this.drawing.buffer.target;
2954
2955						// REMOVE; key: BACKSPACE / DELETE
2956						if ( ( e.keyCode == 8 || e.keyCode == 46 ) && current ) {
2957							e.preventDefault();
2958							_this.setup.fabric.remove( current );
2959
2960							// ESCAPE DRAWIN MODE; key: escape
2961						} else if ( e.keyCode == 27 && _this.drawing.enabled ) {
2962							e.preventDefault();
2963							_this.drawing.handler.done();
2964
2965							// COPY; key: C
2966						} else if ( e.keyCode == 67 && ( e.metaKey || e.ctrlKey ) && current ) {
2967							_this.drawing.buffer.copy = current;
2968
2969							// CUT; key: X
2970						} else if ( e.keyCode == 88 && ( e.metaKey || e.ctrlKey ) && current ) {
2971							_this.drawing.buffer.copy = current;
2972							_this.setup.fabric.remove( current );
2973
2974							// PASTE; key: V
2975						} else if ( e.keyCode == 86 && ( e.metaKey || e.ctrlKey ) ) {
2976							if ( _this.drawing.buffer.copy ) {
2977								handleClone( _this.drawing.buffer.copy.clone( handleClone ) )
2978							}
2979
2980							// UNDO / REDO; key: Z
2981						} else if ( e.keyCode == 90 && ( e.metaKey || e.ctrlKey ) ) {
2982							e.preventDefault();
2983							if ( e.shiftKey ) {
2984								_this.drawing.handler.redo();
2985							} else {
2986								_this.drawing.handler.undo();
2987							}
2988						}
2989					} );
2990				}
2991
2992				// OBSERVE; DRAG AND DROP LISTENER; DRAWING FEATURE
2993				if ( _this.config.fileListener ) {
2994					_this.setup.chart.containerDiv.addEventListener( "dragover", _this.handleDropbox );
2995					_this.setup.chart.containerDiv.addEventListener( "dragleave", _this.handleDropbox );
2996					_this.setup.chart.containerDiv.addEventListener( "drop", _this.handleDropbox );
2997				}
2998			},
2999
3000			/**
3001			 * Initiate export menu; waits for chart container to place menu
3002			 */
3003			init: function() {
3004				clearTimeout( _this.timer );
3005				_this.timer = setInterval( function() {
3006					if ( _this.setup.chart.containerDiv ) {
3007						clearTimeout( _this.timer );
3008
3009						if ( _this.config.enabled ) {
3010							// CREATE REFERENCE
3011							_this.setup.chart.AmExport = _this;
3012
3013							// OVERWRITE PARENT OVERFLOW
3014							if ( _this.config.overflow ) {
3015								_this.setup.chart.div.style.overflow = "visible";
3016							}
3017
3018							// ATTACH EVENTS
3019							_this.loadListeners();
3020
3021							// CREATE MENU
3022							_this.createMenu( _this.config.menu );
3023						}
3024					}
3025				}, AmCharts.updateRate );
3026
3027			},
3028
3029			/**
3030			 * Initiates export instance; merges given config; attaches event listener
3031			 */
3032			construct: function() {
3033				// ANNOTATION; MAP "DONE"
3034				_this.drawing.handler.cancel = _this.drawing.handler.done;
3035
3036				// CHECK BLOB CONSTRUCTOR
3037				try {
3038					_this.setup.hasBlob = !!new Blob;
3039				} catch ( e ) {}
3040
3041				// WORK AROUND TO BYPASS FILESAVER CHECK TRYING TO OPEN THE BLOB URL IN SAFARI BROWSER
3042				window.safari = window.safari ? window.safari : {};
3043
3044				// OVERTAKE CHART FONTSIZE IF GIVEN
3045				_this.defaults.fabric.drawing.fontSize = _this.setup.chart.fontSize || 11;
3046
3047				// MERGE SETTINGS
3048				_this.config.drawing = _this.deepMerge( _this.defaults.fabric.drawing, _this.config.drawing || {}, true );
3049				_this.deepMerge( _this.defaults.fabric, _this.config, true );
3050				_this.deepMerge( _this.defaults.fabric, _this.config.fabric || {}, true );
3051				_this.deepMerge( _this.defaults.pdfMake, _this.config, true );
3052				_this.deepMerge( _this.defaults.pdfMake, _this.config.pdfMake || {}, true );
3053				_this.deepMerge( _this.libs, _this.config.libs || {}, true );
3054
3055				// UPDATE CONFIG
3056				_this.config.drawing = _this.defaults.fabric.drawing;
3057				_this.config.fabric = _this.defaults.fabric;
3058				_this.config.pdfMake = _this.defaults.pdfMake;
3059				_this.config = _this.deepMerge( _this.defaults, _this.config, true );
3060
3061				// MERGE; SETUP DRAWING MENU
3062				if ( _this.config.fabric.drawing.enabled ) {
3063					if ( _this.config.fabric.drawing.menu === undefined ) {
3064						_this.config.fabric.drawing.menu = [];
3065						_this.deepMerge( _this.config.fabric.drawing.menu, [ {
3066							"class": "export-drawing",
3067							menu: [ {
3068								label: _this.i18l( "menu.label.draw.add" ),
3069								menu: [ {
3070									label: _this.i18l( "menu.label.draw.shapes" ),
3071									action: "draw.shapes"
3072								}, {
3073									label: _this.i18l( "menu.label.draw.text" ),
3074									action: "text"
3075								} ]
3076							}, {
3077								label: _this.i18l( "menu.label.draw.change" ),
3078								menu: [ {
3079									label: _this.i18l( "menu.label.draw.modes" ),
3080									action: "draw.modes"
3081								}, {
3082									label: _this.i18l( "menu.label.draw.colors" ),
3083									action: "draw.colors"
3084								}, {
3085									label: _this.i18l( "menu.label.draw.widths" ),
3086									action: "draw.widths"
3087								}, {
3088									label: _this.i18l( "menu.label.draw.opacities" ),
3089									action: "draw.opacities"
3090								}, "UNDO", "REDO" ]
3091							}, {
3092								label: _this.i18l( "menu.label.save.image" ),
3093								menu: [ "PNG", "JPG", "SVG", "PDF" ]
3094							}, "PRINT", "CANCEL" ]
3095						} ] );
3096					}
3097				}
3098
3099				// MERGE; SETUP MAIN MENU
3100				if ( _this.config.menu === undefined ) {
3101					_this.config.menu = [];
3102					// PARENT MENU
3103					_this.deepMerge( _this.config, {
3104						menu: [ {
3105							"class": "export-main",
3106							menu: [ {
3107								label: _this.i18l( "menu.label.save.image" ),
3108								menu: [ "PNG", "JPG", "SVG", "PDF" ]
3109							}, {
3110								label: _this.i18l( "menu.label.save.data" ),
3111								menu: [ "CSV", "XLSX", "JSON" ]
3112							}, {
3113								label: _this.i18l( "menu.label.draw" ),
3114								action: "draw",
3115								menu: _this.config.fabric.drawing.menu
3116							}, {
3117								format: "PRINT",
3118								label: _this.i18l( "menu.label.print" )
3119							} ]
3120						} ]
3121					} );
3122				}
3123
3124				// ADD MISSING PATH
3125				if ( !_this.libs.path ) {
3126					_this.libs.path = _this.config.path + "libs/";
3127				}
3128
3129				// CHECK ACCEPTANCE
3130				if ( _this.isSupported() ) {
3131					// LOAD DEPENDENCIES
3132					_this.loadDependencies( _this.libs.resources, _this.libs.reload );
3133					// ADD CLASSNAMES
3134					_this.setup.chart.addClassNames = true;
3135					// REFERENCE
3136					_this.setup.chart[ _this.name ] = _this;
3137					// INIT MENU; WAIT FOR CHART INSTANCE
3138					_this.init();
3139				}
3140			}
3141		}
3142
3143		// USE GIVEN CONFIG
3144		if ( config ) {
3145			_this.config = config;
3146
3147			// USE CHART EXPORT CONFIG
3148		} else if ( _this.setup.chart[ _this.name ] ) {
3149			_this.config = _this.setup.chart[ _this.name ];
3150
3151			// MIGRATE OLD EXPORT CHART CONFIG
3152		} else if ( _this.setup.chart.amExport || _this.setup.chart.exportConfig ) {
3153			_this.config = _this.migrateSetup( _this.setup.chart.amExport || _this.setup.chart.exportConfig );
3154
3155			// EXIT; NO CONFIG
3156		} else {
3157			return;
3158		}
3159
3160		// CONSTRUCT INSTANCE
3161		_this.construct();
3162
3163		// EXPORT SCOPE
3164		return _this.deepMerge( this, _this );
3165	}
3166} )();
3167
3168/**
3169 * Set init handler
3170 */
3171AmCharts.addInitHandler( function( chart ) {
3172	new AmCharts[ "export" ]( chart );
3173
3174}, [ "pie", "serial", "xy", "funnel", "radar", "gauge", "stock", "map", "gantt" ] );