UNPKG

amcharts-jschart

Version:

A go-to library for data visualization. When you don’t have time to learn new technologies. When you need a simple yet powerful and flexible drop-in data visualization solution.

1,624 lines (1,426 loc) 125 kB
/* Plugin Name: amCharts Export Description: Adds export capabilities to amCharts products Author: Benjamin Maertz, amCharts Version: 1.4.56 Author URI: http://www.amcharts.com/ Copyright 2016 amCharts Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Please note that the above license covers only this plugin. It by all means does not apply to any other amCharts products that are covered by different licenses. */ /* ** Polyfill translation */ if ( !AmCharts.translations[ "export" ] ) { AmCharts.translations[ "export" ] = {} } if ( !AmCharts.translations[ "export" ][ "en" ] ) { AmCharts.translations[ "export" ][ "en" ] = { "fallback.save.text": "CTRL + C to copy the data into the clipboard.", "fallback.save.image": "Rightclick -> Save picture as... to save the image.", "capturing.delayed.menu.label": "{{duration}}", "capturing.delayed.menu.title": "Click to cancel", "menu.label.print": "Print", "menu.label.undo": "Undo", "menu.label.redo": "Redo", "menu.label.cancel": "Cancel", "menu.label.save.image": "Download as ...", "menu.label.save.data": "Save as ...", "menu.label.draw": "Annotate ...", "menu.label.draw.change": "Change ...", "menu.label.draw.add": "Add ...", "menu.label.draw.shapes": "Shape ...", "menu.label.draw.colors": "Color ...", "menu.label.draw.widths": "Size ...", "menu.label.draw.opacities": "Opacity ...", "menu.label.draw.text": "Text", "menu.label.draw.modes": "Mode ...", "menu.label.draw.modes.pencil": "Pencil", "menu.label.draw.modes.line": "Line", "menu.label.draw.modes.arrow": "Arrow", "label.saved.from": "Saved from: " } } /* ** Polyfill export class */ ( function() { AmCharts[ "export" ] = function( chart, config ) { var _timer; var _this = { name: "export", version: "1.4.56", libs: { async: true, autoLoad: true, reload: false, resources: [ "fabric.js/fabric.min.js", "FileSaver.js/FileSaver.min.js", { "jszip/jszip.min.js": [ "xlsx/xlsx.min.js" ], "pdfmake/pdfmake.min.js": [ "pdfmake/vfs_fonts.js" ] } ], namespaces: { "pdfmake.min.js": "pdfMake", "jszip.min.js": "JSZip", "xlsx.min.js": "XLSX", "fabric.min.js": "fabric", "FileSaver.min.js": "saveAs" }, loadTimeout: 10000 }, config: {}, setup: { chart: chart, hasBlob: false, wrapper: false, isIE: !!window.document.documentMode, IEversion: window.document.documentMode, hasTouch: typeof window.Touch == "object" }, drawing: { enabled: false, undos: [], redos: [], buffer: { position: { x1: 0, y1: 0, x2: 0, y2: 0, xD: 0, yD: 0 } }, handler: { undo: function( options, skipped ) { var item = _this.drawing.undos.pop(); if ( item ) { item.selectable = true; _this.drawing.redos.push( item ); if ( item.action == "added" ) { _this.setup.fabric.remove( item.target ); } var state = JSON.parse( item.state ); item.target.set( state ); if ( item.target instanceof fabric.Group ) { _this.drawing.handler.change( { color: state.cfg.color, width: state.cfg.width, opacity: state.cfg.opacity }, true, item.target ); } _this.setup.fabric.renderAll(); // RECALL if ( item.state == item.target.recentState && !skipped ) { _this.drawing.handler.undo( item, true ); } } }, redo: function( options, skipped ) { var item = _this.drawing.redos.pop(); if ( item ) { item.selectable = true; _this.drawing.undos.push( item ); if ( item.action == "added" ) { _this.setup.fabric.add( item.target ); } var state = JSON.parse( item.state ); item.target.recentState = item.state; item.target.set( state ); if ( item.target instanceof fabric.Group ) { _this.drawing.handler.change( { color: state.cfg.color, width: state.cfg.width, opacity: state.cfg.opacity }, true, item.target ); } _this.setup.fabric.renderAll(); // RECALL if ( item.action == "addified" ) { _this.drawing.handler.redo(); } } }, done: function( options ) { _this.drawing.enabled = false; _this.drawing.buffer.enabled = false; _this.drawing.undos = []; _this.drawing.redos = []; _this.createMenu( _this.config.menu ); _this.setup.fabric.deactivateAll(); if ( _this.setup.wrapper ) { _this.setup.chart.containerDiv.removeChild( _this.setup.wrapper ); _this.setup.wrapper = false; } }, add: function( options ) { var cfg = _this.deepMerge( { top: _this.setup.fabric.height / 2, left: _this.setup.fabric.width / 2 }, options || {} ); var method = cfg.url.indexOf( ".svg" ) != -1 ? fabric.loadSVGFromURL : fabric.Image.fromURL; method( cfg.url, function( objects, options ) { var group = options !== undefined ? fabric.util.groupSVGElements( objects, options ) : objects; var ratio = false; // RESCALE ONLY IF IT EXCEEDS THE CANVAS if ( group.height > _this.setup.fabric.height || group.width > _this.setup.fabric.width ) { ratio = ( _this.setup.fabric.height / 2 ) / group.height; } if ( cfg.top > _this.setup.fabric.height ) { cfg.top = _this.setup.fabric.height / 2; } if ( cfg.left > _this.setup.fabric.width ) { cfg.left = _this.setup.fabric.width / 2; } // SET DRAWING FLAG _this.drawing.buffer.isDrawing = true; group.set( { originX: "center", originY: "center", top: cfg.top, left: cfg.left, width: ratio ? group.width * ratio : group.width, height: ratio ? group.height * ratio : group.height, fill: _this.drawing.color } ); _this.setup.fabric.add( group ); } ); }, change: function( options, skipped, target ) { var cfg = _this.deepMerge( {}, options || {} ); var state, i1, rgba; var current = target || _this.drawing.buffer.target; var objects = current ? current._objects ? current._objects : [ current ] : null; // UPDATE DRAWING OBJECT if ( cfg.mode ) { _this.drawing.mode = cfg.mode; } if ( cfg.width ) { _this.drawing.width = cfg.width; _this.drawing.fontSize = cfg.fontSize = cfg.width * 3; // BACK TO DEFAULT if ( _this.drawing.width == 1 ) { _this.drawing.fontSize = cfg.fontSize = _this.defaults.fabric.drawing.fontSize; } } if ( cfg.fontSize ) { _this.drawing.fontSize = cfg.fontSize; } if ( cfg.color ) { _this.drawing.color = cfg.color; } if ( cfg.opacity ) { _this.drawing.opacity = cfg.opacity; } // APPLY OPACITY ON CURRENT COLOR rgba = _this.getRGBA( _this.drawing.color ); rgba.pop(); rgba.push( _this.drawing.opacity ); _this.drawing.color = "rgba(" + rgba.join() + ")"; _this.setup.fabric.freeDrawingBrush.color = _this.drawing.color; _this.setup.fabric.freeDrawingBrush.width = _this.drawing.width; // UPDATE CURRENT SELECTION if ( current ) { state = JSON.parse( current.recentState ).cfg; // UPDATE GIVE OPTIONS ONLY if ( state ) { cfg.color = cfg.color || state.color; cfg.width = cfg.width || state.width; cfg.opacity = cfg.opacity || state.opacity; cfg.fontSize = cfg.fontSize || state.fontSize; rgba = _this.getRGBA( cfg.color ); rgba.pop(); rgba.push( cfg.opacity ); cfg.color = "rgba(" + rgba.join() + ")"; } // UPDATE OBJECTS for ( i1 = 0; i1 < objects.length; i1++ ) { if ( objects[ i1 ] instanceof fabric.Text || objects[ i1 ] instanceof fabric.PathGroup || objects[ i1 ] instanceof fabric.Triangle ) { if ( cfg.color || cfg.opacity ) { objects[ i1 ].set( { fill: cfg.color } ); } if ( cfg.fontSize ) { objects[ i1 ].set( { fontSize: cfg.fontSize } ); } } else if ( objects[ i1 ] instanceof fabric.Path || objects[ i1 ] instanceof fabric.Line ) { if ( current instanceof fabric.Group ) { if ( cfg.color || cfg.opacity ) { objects[ i1 ].set( { stroke: cfg.color } ); } } else { if ( cfg.color || cfg.opacity ) { objects[ i1 ].set( { stroke: cfg.color } ); } if ( cfg.width ) { objects[ i1 ].set( { strokeWidth: cfg.width } ); } } } } // ADD UNDO if ( !skipped ) { state = JSON.stringify( _this.deepMerge( current.saveState().originalState, { cfg: { color: cfg.color, width: cfg.width, opacity: cfg.opacity } } ) ); current.recentState = state; _this.drawing.redos = []; _this.drawing.undos.push( { action: "modified", target: current, state: state } ); } _this.setup.fabric.renderAll(); } }, text: function( options ) { var cfg = _this.deepMerge( { text: _this.i18l( "menu.label.draw.text" ), top: _this.setup.fabric.height / 2, left: _this.setup.fabric.width / 2, fontSize: _this.drawing.fontSize, fontFamily: _this.setup.chart.fontFamily || "Verdana", fill: _this.drawing.color }, options || {} ); cfg.click = function() {}; var text = new fabric.IText( cfg.text, cfg ); // SET DRAWING FLAG _this.drawing.buffer.isDrawing = true; _this.setup.fabric.add( text ); _this.setup.fabric.setActiveObject( text ); text.selectAll(); text.enterEditing(); return text; }, line: function( options ) { var cfg = _this.deepMerge( { x1: ( _this.setup.fabric.width / 2 ) - ( _this.setup.fabric.width / 10 ), x2: ( _this.setup.fabric.width / 2 ) + ( _this.setup.fabric.width / 10 ), y1: ( _this.setup.fabric.height / 2 ), y2: ( _this.setup.fabric.height / 2 ), angle: 90, strokeLineCap: _this.drawing.lineCap, arrow: _this.drawing.arrow, color: _this.drawing.color, width: _this.drawing.width, group: [], }, options || {} ); var i1, arrow, arrowTop, arrowLeft; var line = new fabric.Line( [ cfg.x1, cfg.y1, cfg.x2, cfg.y2 ], { stroke: cfg.color, strokeWidth: cfg.width, strokeLineCap: cfg.strokeLineCap } ); cfg.group.push( line ); if ( cfg.arrow ) { cfg.angle = cfg.angle ? cfg.angle : _this.getAngle( cfg.x1, cfg.y1, cfg.x2, cfg.y2 ); if ( cfg.arrow == "start" ) { arrowTop = cfg.y1 + ( cfg.width / 2 ); arrowLeft = cfg.x1 + ( cfg.width / 2 ); } else if ( cfg.arrow == "middle" ) { arrowTop = cfg.y2 + ( cfg.width / 2 ) - ( ( cfg.y2 - cfg.y1 ) / 2 ); arrowLeft = cfg.x2 + ( cfg.width / 2 ) - ( ( cfg.x2 - cfg.x1 ) / 2 ); } else { // arrow: end arrowTop = cfg.y2 + ( cfg.width / 2 ); arrowLeft = cfg.x2 + ( cfg.width / 2 ); } arrow = new fabric.Triangle( { top: arrowTop, left: arrowLeft, fill: cfg.color, height: cfg.width * 7, width: cfg.width * 7, angle: cfg.angle, originX: "center", originY: "bottom" } ); cfg.group.push( arrow ); } // SET DRAWING FLAG _this.drawing.buffer.isDrawing = true; if ( cfg.action != "config" ) { if ( cfg.arrow ) { var group = new fabric.Group( cfg.group ); group.set( { cfg: cfg, fill: cfg.color, action: cfg.action, selectable: true, known: cfg.action == "change" } ); if ( cfg.action == "change" ) { _this.setup.fabric.setActiveObject( group ); } _this.setup.fabric.add( group ); return group; } else { _this.setup.fabric.add( line ); return line; } } else { for ( i1 = 0; i1 < cfg.group.length; i1++ ) { _this.setup.fabric.add( cfg.group[ i1 ] ); } } return cfg; } } }, defaults: { position: "top-right", fileName: "amCharts", action: "download", overflow: true, path: ( ( chart.path || "" ) + "plugins/export/" ), formats: { JPG: { mimeType: "image/jpg", extension: "jpg", capture: true }, PNG: { mimeType: "image/png", extension: "png", capture: true }, SVG: { mimeType: "text/xml", extension: "svg", capture: true }, PDF: { mimeType: "application/pdf", extension: "pdf", capture: true }, CSV: { mimeType: "text/plain", extension: "csv" }, JSON: { mimeType: "text/plain", extension: "json" }, XLSX: { mimeType: "application/octet-stream", extension: "xlsx" } }, fabric: { backgroundColor: "#FFFFFF", removeImages: true, forceRemoveImages: false, selection: false, loadTimeout: 5000, drawing: { enabled: true, arrow: "end", lineCap: "butt", mode: "pencil", modes: [ "pencil", "line", "arrow" ], color: "#000000", colors: [ "#000000", "#FFFFFF", "#FF0000", "#00FF00", "#0000FF" ], shapes: [ "11.svg", "14.svg", "16.svg", "17.svg", "20.svg", "27.svg" ], width: 1, fontSize: 11, widths: [ 1, 5, 10, 15 ], opacity: 1, opacities: [ 1, 0.8, 0.6, 0.4, 0.2 ], menu: undefined, autoClose: true }, border: { fill: "", fillOpacity: 0, stroke: "#000000", strokeWidth: 1, strokeOpacity: 1 } }, pdfMake: { images: {}, pageOrientation: "portrait", pageMargins: 40, pageOrigin: true, pageSize: "A4", pageSizes: { "4A0": [ 4767.87, 6740.79 ], "2A0": [ 3370.39, 4767.87 ], "A0": [ 2383.94, 3370.39 ], "A1": [ 1683.78, 2383.94 ], "A2": [ 1190.55, 1683.78 ], "A3": [ 841.89, 1190.55 ], "A4": [ 595.28, 841.89 ], "A5": [ 419.53, 595.28 ], "A6": [ 297.64, 419.53 ], "A7": [ 209.76, 297.64 ], "A8": [ 147.40, 209.76 ], "A9": [ 104.88, 147.40 ], "A10": [ 73.70, 104.88 ], "B0": [ 2834.65, 4008.19 ], "B1": [ 2004.09, 2834.65 ], "B2": [ 1417.32, 2004.09 ], "B3": [ 1000.63, 1417.32 ], "B4": [ 708.66, 1000.63 ], "B5": [ 498.90, 708.66 ], "B6": [ 354.33, 498.90 ], "B7": [ 249.45, 354.33 ], "B8": [ 175.75, 249.45 ], "B9": [ 124.72, 175.75 ], "B10": [ 87.87, 124.72 ], "C0": [ 2599.37, 3676.54 ], "C1": [ 1836.85, 2599.37 ], "C2": [ 1298.27, 1836.85 ], "C3": [ 918.43, 1298.27 ], "C4": [ 649.13, 918.43 ], "C5": [ 459.21, 649.13 ], "C6": [ 323.15, 459.21 ], "C7": [ 229.61, 323.15 ], "C8": [ 161.57, 229.61 ], "C9": [ 113.39, 161.57 ], "C10": [ 79.37, 113.39 ], "RA0": [ 2437.80, 3458.27 ], "RA1": [ 1729.13, 2437.80 ], "RA2": [ 1218.90, 1729.13 ], "RA3": [ 864.57, 1218.90 ], "RA4": [ 609.45, 864.57 ], "SRA0": [ 2551.18, 3628.35 ], "SRA1": [ 1814.17, 2551.18 ], "SRA2": [ 1275.59, 1814.17 ], "SRA3": [ 907.09, 1275.59 ], "SRA4": [ 637.80, 907.09 ], "EXECUTIVE": [ 521.86, 756.00 ], "FOLIO": [ 612.00, 936.00 ], "LEGAL": [ 612.00, 1008.00 ], "LETTER": [ 612.00, 792.00 ], "TABLOID": [ 792.00, 1224.00 ] } }, menu: undefined, divId: null, menuReviver: null, menuWalker: null, fallback: true, keyListener: true, fileListener: true, compress: true, debug: false }, /** * Returns translated message, takes english as default */ i18l: function( key, language ) { var lang = language ? language : _this.setup.chart.language ? _this.setup.chart.language : "en"; var catalog = AmCharts.translations[ _this.name ][ lang ] || AmCharts.translations[ _this.name ][ "en" ]; return catalog[ key ] || key; }, /** * Generates download file; if unsupported offers fallback to save manually */ download: function( data, type, filename ) { // SAVE if ( window.saveAs && _this.setup.hasBlob ) { var blob = _this.toBlob( { data: data, type: type }, function( data ) { saveAs( data, filename ); } ); // FALLBACK TEXTAREA } else if ( _this.config.fallback && type == "text/plain" ) { var div = document.createElement( "div" ); var msg = document.createElement( "div" ); var textarea = document.createElement( "textarea" ); msg.innerHTML = _this.i18l( "fallback.save.text" ); div.appendChild( msg ); div.appendChild( textarea ); msg.setAttribute( "class", "amcharts-export-fallback-message" ); div.setAttribute( "class", "amcharts-export-fallback" ); _this.setup.chart.containerDiv.appendChild( div ); // FULFILL TEXTAREA AND PRESELECT textarea.setAttribute( "readonly", "" ); textarea.value = data; textarea.focus(); textarea.select(); // UPDATE MENU _this.createMenu( [ { "class": "export-main export-close", label: "Done", click: function() { _this.createMenu( _this.config.menu ); _this.setup.chart.containerDiv.removeChild( div ); } } ] ); // FALLBACK IMAGE } else if ( _this.config.fallback && type.split( "/" )[ 0 ] == "image" ) { var div = document.createElement( "div" ); var msg = document.createElement( "div" ); var img = _this.toImage( { data: data } ); msg.innerHTML = _this.i18l( "fallback.save.image" ); // FULFILL TEXTAREA AND PRESELECT div.appendChild( msg ); div.appendChild( img ); msg.setAttribute( "class", "amcharts-export-fallback-message" ); div.setAttribute( "class", "amcharts-export-fallback" ); _this.setup.chart.containerDiv.appendChild( div ); // UPDATE MENU _this.createMenu( [ { "class": "export-main export-close", label: "Done", click: function() { _this.createMenu( _this.config.menu ); _this.setup.chart.containerDiv.removeChild( div ); } } ] ); // ERROR } else { throw new Error( "Unable to create file. Ensure saveAs (FileSaver.js) is supported." ); } return data; }, /** * Generates script, links tags and places them into the document's head * In case of reload it replaces the node to force the download */ loadResource: function( src, addons ) { var i1, exist, node, item, check, type; var url = src.indexOf( "//" ) != -1 ? src : [ _this.libs.path, src ].join( "" ); var loadCallback = function callback() { if ( addons ) { for ( i1 = 0; i1 < addons.length; i1++ ) { _this.loadResource( addons[ i1 ] ); } } } if ( src.indexOf( ".js" ) != -1 ) { node = document.createElement( "script" ); node.setAttribute( "type", "text/javascript" ); node.setAttribute( "src", url ); if ( _this.libs.async ) { node.setAttribute( "async", "" ); } } else if ( src.indexOf( ".css" ) != -1 ) { node = document.createElement( "link" ); node.setAttribute( "type", "text/css" ); node.setAttribute( "rel", "stylesheet" ); node.setAttribute( "href", url ); } // NODE CHECK for ( i1 = 0; i1 < document.head.childNodes.length; i1++ ) { item = document.head.childNodes[ i1 ]; check = item ? ( item.src || item.href ) : false; type = item ? item.tagName : false; if ( item && check && check.indexOf( src ) != -1 ) { if ( _this.libs.reload ) { document.head.removeChild( item ); } exist = true; break; } } // NAMESPACE CHECK for ( i1 in _this.libs.namespaces ) { var namespace = _this.libs.namespaces[ i1 ]; var check = src.toLowerCase(); var item = i1.toLowerCase(); if ( check.indexOf( item ) != -1 && window[ namespace ] !== undefined ) { exist = true; break; } } if ( !exist || _this.libs.reload ) { node.addEventListener( "load", loadCallback ); node.addEventListener( "error", function() { _this.handleLog( [ "amCharts[export]: Loading error on ", this.src || this.href ].join( "" ) ); } ); document.head.appendChild( node ); if ( !_this.listenersToRemove ) { _this.listenersToRemove = []; } _this.listenersToRemove.push( { node: node, method: loadCallback, event: "load" } ); } }, /** * Walker to generate the script,link tags */ loadDependencies: function() { var i1, i2; if ( _this.libs.autoLoad ) { for ( i1 = 0; i1 < _this.libs.resources.length; i1++ ) { if ( _this.libs.resources[ i1 ] instanceof Object ) { for ( i2 in _this.libs.resources[ i1 ] ) { _this.loadResource( i2, _this.libs.resources[ i1 ][ i2 ] ); } } else { _this.loadResource( _this.libs.resources[ i1 ] ); } } } }, /** * Converts string to number */ pxToNumber: function( attr, returnUndefined ) { if ( !attr && returnUndefined ) { return undefined; } return Number( String( attr ).replace( "px", "" ) ) || 0; }, /** * Converts number to string */ numberToPx: function( attr ) { return String( attr ) + "px"; }, /** * Referenceless copy of object type variables */ cloneObject: function( o ) { var clone, v, k, isObject, isDate; clone = Array.isArray( o ) ? [] : {}; // Walkthrough values for ( k in o ) { v = o[ k ]; isObject = typeof v === "object"; isDate = v instanceof Date; // Set value; call recursivly if value is an object clone[ k ] = isObject && !isDate ? _this.cloneObject( v ) : v; } return clone; }, /** * Recursive method to merge the given objects together * Overwrite flag replaces the value instead to crawl through */ deepMerge: function( a, b, overwrite ) { var i1, v, type = b instanceof Array ? "array" : "object"; // SKIP; OBJECTS AND ARRAYS ONLY if ( !( a instanceof Object || a instanceof Array ) ) { return a; } // WALKTHOUGH SOURCE for ( i1 in b ) { // PREVENT METHODS if ( type == "array" && isNaN( i1 ) ) { continue; } // ASSIGN VALUE v = b[ i1 ]; // NEW INSTANCE if ( a[ i1 ] == undefined || overwrite ) { if ( v instanceof Array ) { a[ i1 ] = new Array(); } else if ( v instanceof Function ) { a[ i1 ] = function() {}; } else if ( v instanceof Date ) { a[ i1 ] = new Date(); } else if ( v instanceof Object ) { a[ i1 ] = new Object(); } else if ( v instanceof Number ) { a[ i1 ] = new Number(); } else if ( v instanceof String ) { a[ i1 ] = new String(); } } // WALKTHROUGH RECURSIVLY if ( ( v instanceof Object || v instanceof Array ) && !( v instanceof Function || v instanceof Date || _this.isElement( v ) ) && i1 != "chart" && i1 != "scope" ) { _this.deepMerge( a[ i1 ], v, overwrite ); // ASSIGN } else { if ( a instanceof Array && !overwrite ) { a.push( v ); } else { a[ i1 ] = v; } } } return a; }, /** * Checks if given argument is a valid node */ isElement: function( thingy ) { return thingy instanceof Object && thingy && thingy.nodeType === 1; }, /** * Checks if given argument contains a hashbang and returns it */ isHashbanged: function( thingy ) { var str = String( thingy ).replace( /\"/g, "" ); return str.slice( 0, 3 ) == "url" ? str.slice( str.indexOf( "#" ) + 1, str.length - 1 ) : false; }, /** * Checks if given event has been thrown with pressed click / touch */ isPressed: function( event ) { // IE EXCEPTION if ( event.type == "mousemove" && event.which === 1 ) { // IGNORE // OTHERS } else if ( event.type == "touchmove" || event.buttons === 1 || event.button === 1 || event.which === 1 ) { _this.drawing.buffer.isPressed = true; } else { _this.drawing.buffer.isPressed = false; } return _this.drawing.buffer.isPressed; }, /** * Checks if given source needs to be removed */ removeImage: function( source ) { if ( source ) { // FORCE REMOVAL if ( _this.config.fabric.forceRemoveImages ) { return true; // REMOVE TAINTED } else if ( _this.config.fabric.removeImages && _this.isTainted( source ) ) { return true; // IE 10 internal bug handling SVG images in canvas context } else if ( _this.setup.isIE && ( _this.setup.IEversion == 10 || _this.setup.IEversion == 11 ) && source.toLowerCase().indexOf( ".svg" ) != -1 ) { return true; } } return false }, /** * Checks if given source is within the current origin */ isTainted: function( source ) { var origin = String( window.location.origin || window.location.protocol + "//" + window.location.hostname + ( window.location.port ? ':' + window.location.port : '' ) ); // CHECK GIVEN SOURCE if ( source ) { // LOCAL FILES ARE ALWAYS TAINTED if ( origin.indexOf( ":\\" ) != -1 || source.indexOf( ":\\" ) != -1 || origin.indexOf( "file://" ) != -1 || source.indexOf( "file://" ) != -1 ) { return true // MISMATCHING ORIGIN } else if ( source.indexOf( "//" ) != -1 && source.indexOf( origin.replace( /.*:/, "" ) ) == -1 ) { return true; } } return false; }, /* ** Checks several indicators for acceptance; */ isSupported: function() { // CHECK CONFIG if ( !_this.config.enabled ) { return false; } // CHECK IE; ATTEMPT TO ACCESS HEAD ELEMENT if ( _this.setup.isIE && _this.setup.IEversion <= 9 ) { if ( !Array.prototype.indexOf || !document.head || _this.config.fallback === false ) { return false; } } return true; }, getAngle: function( x1, y1, x2, y2 ) { var x = x2 - x1; var y = y2 - y1; var angle; if ( x == 0 ) { if ( y == 0 ) { angle = 0; } else if ( y > 0 ) { angle = Math.PI / 2; } else { angle = Math.PI * 3 / 2; } } else if ( y == 0 ) { if ( x > 0 ) { angle = 0; } else { angle = Math.PI; } } else { if ( x < 0 ) { angle = Math.atan( y / x ) + Math.PI; } else if ( y < 0 ) { angle = Math.atan( y / x ) + ( 2 * Math.PI ); } else { angle = Math.atan( y / x ); } } return angle * 180 / Math.PI; }, /** * Recursive method which crawls upwards to gather the requested attribute */ gatherAttribute: function( elm, attr, limit, lvl ) { var value, lvl = lvl ? lvl : 0, limit = limit ? limit : 3; if ( elm ) { value = elm.getAttribute( attr ); if ( !value && lvl < limit ) { return _this.gatherAttribute( elm.parentNode, attr, limit, lvl + 1 ); } } return value; }, /** * Recursive method which crawls upwards to gather the requested classname */ gatherClassName: function( elm, className, limit, lvl ) { var value, lvl = lvl ? lvl : 0, limit = limit ? limit : 3; if ( _this.isElement( elm ) ) { value = ( elm.getAttribute( "class" ) || "" ).split( " " ).indexOf( className ) != -1; if ( !value && lvl < limit ) { return _this.gatherClassName( elm.parentNode, className, limit, lvl + 1 ); } else if ( value ) { value = elm; } } return value; }, /** * Collects the clip-paths and patterns */ gatherElements: function( group, cfg, images ) { var i1, i2; for ( i1 = 0; i1 < group.children.length; i1++ ) { var childNode = group.children[ i1 ]; // CLIPPATH if ( childNode.tagName == "clipPath" ) { var bbox = {}; var transform = fabric.parseTransformAttribute( _this.gatherAttribute( childNode, "transform" ) ); // HIDE SIBLINGS; GATHER IT'S DIMENSIONS for ( i2 = 0; i2 < childNode.childNodes.length; i2++ ) { childNode.childNodes[ i2 ].setAttribute( "fill", "transparent" ); bbox = { x: _this.pxToNumber( childNode.childNodes[ i2 ].getAttribute( "x" ) ), y: _this.pxToNumber( childNode.childNodes[ i2 ].getAttribute( "y" ) ), width: _this.pxToNumber( childNode.childNodes[ i2 ].getAttribute( "width" ) ), height: _this.pxToNumber( childNode.childNodes[ i2 ].getAttribute( "height" ) ) } } group.clippings[ childNode.id ] = { svg: childNode, bbox: bbox, transform: transform }; // PATTERN } else if ( childNode.tagName == "pattern" ) { var props = { node: childNode, source: childNode.getAttribute( "xlink:href" ), width: Number( childNode.getAttribute( "width" ) ), height: Number( childNode.getAttribute( "height" ) ), repeat: "repeat", offsetX: 0, offsetY: 0 } // GATHER BACKGROUND for ( i2 = 0; i2 < childNode.childNodes.length; i2++ ) { // RECT; COLOR if ( childNode.childNodes[ i2 ].tagName == "rect" ) { props.fill = childNode.childNodes[ i2 ].getAttribute( "fill" ); // IMAGE } else if ( childNode.childNodes[ i2 ].tagName == "image" ) { var attrs = fabric.parseAttributes( childNode.childNodes[ i2 ], fabric.SHARED_ATTRIBUTES ); if ( attrs.transformMatrix ) { props.offsetX = attrs.transformMatrix[ 4 ]; props.offsetY = attrs.transformMatrix[ 5 ]; } } } // TAINTED if ( _this.removeImage( props.source ) ) { group.patterns[ childNode.id ] = props.fill ? props.fill : "transparent"; } else { group.patterns[ props.node.id ] = props; } // IMAGES } else if ( childNode.tagName == "image" ) { images.included++; // LOAD IMAGE MANUALLY; TO RERENDER THE CANVAS fabric.Image.fromURL( childNode.getAttribute( "xlink:href" ), function( img ) { images.loaded++; } ); // FILL STROKE POLYFILL ON EVERY ELEMENT } else { var attrs = [ "fill", "stroke" ]; for ( i2 = 0; i2 < attrs.length; i2++ ) { var attr = attrs[ i2 ]; var attrVal = childNode.getAttribute( attr ); var attrRGBA = _this.getRGBA( attrVal ); var isHashbanged = _this.isHashbanged( attrVal ); // VALIDATE AND RESET UNKNOWN COLORS (avoids fabric to crash) if ( attrVal && !attrRGBA && !isHashbanged ) { childNode.setAttribute( attr, "none" ); childNode.setAttribute( attr + "-opacity", "0" ); } } } } return group; }, /* ** GET RGBA COLOR ARRAY FROM INPUT */ getRGBA: function( source, returnInstance ) { if ( source != "none" && source != "transparent" && !_this.isHashbanged( source ) ) { source = new fabric.Color( source ); if ( source._source ) { return returnInstance ? source : source.getSource(); } } return false; }, /* ** GATHER MOUSE POSITION; */ gatherPosition: function( event, type ) { var ref = _this.drawing.buffer.position; var ivt = fabric.util.invertTransform( _this.setup.fabric.viewportTransform ); var pos; if ( event.type == "touchmove" ) { if ( "touches" in event ) { event = event.touches[ 0 ]; } else if ( "changedTouches" in event ) { event = event.changedTouches[ 0 ]; } } pos = fabric.util.transformPoint( _this.setup.fabric.getPointer( event, true ), ivt ); if ( type == 1 ) { ref.x1 = pos.x; ref.y1 = pos.y; } ref.x2 = pos.x; ref.y2 = pos.y; ref.xD = ( ref.x1 - ref.x2 ) < 0 ? ( ref.x1 - ref.x2 ) * -1 : ( ref.x1 - ref.x2 ); ref.yD = ( ref.y1 - ref.y2 ) < 0 ? ( ref.y1 - ref.y2 ) * -1 : ( ref.y1 - ref.y2 ); return ref; }, modifyFabric: function() { // ADAPTED THE WAY TO RECEIVE THE GRADIENTID fabric.ElementsParser.prototype.resolveGradient = function( obj, property ) { var instanceFillValue = obj.get( property ); if ( !( /^url\(/ ).test( instanceFillValue ) ) { return; } var gradientId = instanceFillValue.slice( instanceFillValue.indexOf( "#" ) + 1, instanceFillValue.length - 1 ); if ( fabric.gradientDefs[ this.svgUid ][ gradientId ] ) { var tmp = fabric.Gradient.fromElement( fabric.gradientDefs[ this.svgUid ][ gradientId ], obj ); // WORKAROUND FOR VERTICAL GRADIENT ISSUE; FOR NONE PIE CHARTS if ( tmp.coords.y1 && _this.setup.chart.type != "pie" ) { tmp.coords.y2 = tmp.coords.y1 * -1; tmp.coords.y1 = 0; } obj.set( property, tmp ); } }; // MULTILINE SUPPORT; TODO: BETTER POSITIONING fabric.Text.fromElement = function( element, options ) { if ( !element ) { return null; } var parsedAttributes = fabric.parseAttributes( element, fabric.Text.ATTRIBUTE_NAMES ); options = fabric.util.object.extend( ( options ? fabric.util.object.clone( options ) : {} ), parsedAttributes ); options.top = options.top || 0; options.left = options.left || 0; if ( 'dx' in parsedAttributes ) { options.left += parsedAttributes.dx; } if ( 'dy' in parsedAttributes ) { options.top += parsedAttributes.dy; } if ( !( 'fontSize' in options ) ) { options.fontSize = fabric.Text.DEFAULT_SVG_FONT_SIZE; } if ( !options.originX ) { options.originX = 'left'; } var textContent = ''; var textBuffer = []; // The XML is not properly parsed in IE9 so a workaround to get // textContent is through firstChild.data. Another workaround would be // to convert XML loaded from a file to be converted using DOMParser (same way loadSVGFromString() does) if ( !( 'textContent' in element ) ) { if ( 'firstChild' in element && element.firstChild !== null ) { if ( 'data' in element.firstChild && element.firstChild.data !== null ) { textBuffer.push( element.firstChild.data ); } } } else if ( element.childNodes ) { for ( var i1 = 0; i1 < element.childNodes.length; i1++ ) { textBuffer.push( element.childNodes[ i1 ].textContent ); } } else { textBuffer.push( element.textContent ); } textContent = textBuffer.join( "\n" ); //textContent = textContent.replace(/^\s+|\s+$|\n+/g, '').replace(/\s+/g, ' '); var text = new fabric.Text( textContent, options ), /* Adjust positioning: x/y attributes in SVG correspond to the bottom-left corner of text bounding box top/left properties in Fabric correspond to center point of text bounding box */ offX = 0; if ( text.originX === 'left' ) { offX = text.getWidth() / 2; } if ( text.originX === 'right' ) { offX = -text.getWidth() / 2; } if ( textBuffer.length > 1 ) { text.set( { left: text.getLeft() + offX, top: text.getTop() + text.fontSize * ( textBuffer.length - 1 ) * ( 0.18 + text._fontSizeFraction ), textAlign: options.originX, lineHeight: textBuffer.length > 1 ? 0.965 : 1.16, } ); } else { text.set( { left: text.getLeft() + offX, top: text.getTop() - text.getHeight() / 2 + text.fontSize * ( 0.18 + text._fontSizeFraction ) /* 0.3 is the old lineHeight */ } ); } return text; }; }, /** * Method to capture the current state of the chart */ capture: function( options, callback ) { var i1; var cfg = _this.deepMerge( _this.deepMerge( {}, _this.config.fabric ), options || {} ); var groups = []; var offset = { x: 0, y: 0, pX: 0, pY: 0, lX: 0, lY: 0, width: _this.setup.chart.divRealWidth, height: _this.setup.chart.divRealHeight }; var images = { loaded: 0, included: 0 } var legends = { items: [], width: 0, height: 0, maxWidth: 0, maxHeight: 0 } // NAMESPACE CHECK if ( !_this.handleNamespace( "fabric", { scope: this, cb: _this.capture, args: arguments } ) ) { return false; } // MODIFY FABRIC UNTIL IT'S OFFICIALLY SUPPORTED _this.modifyFabric(); // BEFORE CAPTURING _this.handleCallback( cfg.beforeCapture, cfg ); // GATHER SVGS var svgs = _this.setup.chart.containerDiv.getElementsByTagName( "svg" ); for ( i1 = 0; i1 < svgs.length; i1++ ) { var group = { svg: svgs[ i1 ], parent: svgs[ i1 ].parentNode, children: svgs[ i1 ].getElementsByTagName( "*" ), offset: { x: 0, y: 0 }, patterns: {}, clippings: {}, has: { legend: false, panel: false, scrollbar: false } } // CHECK IT'S SURROUNDINGS group.has.legend = _this.gatherClassName( group.parent, _this.setup.chart.classNamePrefix + "-legend-div", 1 ); group.has.panel = _this.gatherClassName( group.parent, _this.setup.chart.classNamePrefix + "-stock-panel-div" ); group.has.scrollbar = _this.gatherClassName( group.parent, _this.setup.chart.classNamePrefix + "-scrollbar-chart-div" ); // GATHER ELEMENTS group = _this.gatherElements( group, cfg, images ); // APPEND GROUP groups.push( group ); } // GATHER EXTERNAL LEGEND if ( _this.config.legend ) { // STOCK if ( _this.setup.chart.type == "stock" ) { for ( i1 = 0; i1 < _this.setup.chart.panels.length; i1++ ) { if ( _this.setup.chart.panels[ i1 ].stockLegend && _this.setup.chart.panels[ i1 ].stockLegend.divId ) { legends.items.push( _this.setup.chart.panels[ i1 ].stockLegend ); } } // NORMAL } else if ( _this.setup.chart.legend && _this.setup.chart.legend.divId ) { legends.items.push( _this.setup.chart.legend ); } // WALKTHROUGH for ( i1 = 0; i1 < legends.items.length; i1++ ) { var legend = legends.items[ i1 ]; var group = { svg: legend.container.container, parent: legend.container.container.parentNode, children: legend.container.container.getElementsByTagName( "*" ), offset: { x: 0, y: 0 }, legend: { id: i1, type: [ "top", "left" ].indexOf( _this.config.legend.position ) != -1 ? "unshift" : "push", position: _this.config.legend.position, width: _this.config.legend.width ? _this.config.legend.width : legend.container.div.offsetWidth, height: _this.config.legend.height ? _this.config.legend.height : legend.container.div.offsetHeight }, patterns: {}, clippings: {}, has: { legend: false, panel: false, scrollbar: false } } // GATHER DIMENSIONS legends.width += group.legend.width; legends.height += group.legend.height; legends.maxWidth = group.legend.width > legends.maxWidth ? group.legend.width : legends.maxWidth; legends.maxHeight = group.legend.height > legends.maxHeight ? group.legend.height : legends.maxHeight; // GATHER ELEMENTS group = _this.gatherElements( group, cfg, images ); // PRE/APPEND SVG groups[ group.legend.type ]( group ); } // ADAPT WIDTH IF NEEDED; EXPAND HEIGHT if ( [ "top", "bottom" ].indexOf( _this.config.legend.position ) != -1 ) { offset.width = legends.maxWidth > offset.width ? legends.maxWidth : offset.width; offset.height += legends.height; // EXPAND WIDTH; ADAPT HEIGHT IF NEEDED } else if ( [ "left", "right" ].indexOf( _this.config.legend.position ) != -1 ) { offset.width += legends.maxWidth; offset.height = legends.height > offset.height ? legends.height : offset.height; // SIMPLY EXPAND CANVAS } else { offset.height += legends.height; offset.width += legends.maxWidth; } } // CLEAR IF EXIST _this.drawing.enabled = cfg.drawing.enabled = cfg.action == "draw"; _this.drawing.buffer.enabled = _this.drawing.enabled; // history reasons _this.setup.wrapper = document.createElement( "div" ); _this.setup.wrapper.setAttribute( "class", _this.setup.chart.classNamePrefix + "-export-canvas" ); _this.setup.chart.containerDiv.appendChild( _this.setup.wrapper ); // STOCK CHART; SELECTOR OFFSET if ( _this.setup.chart.type == "stock" ) { var padding = { top: 0, right: 0, bottom: 0, left: 0 } if ( _this.setup.chart.leftContainer ) { offset.width -= _this.setup.chart.leftContainer.offsetWidth; padding.left = _this.setup.chart.leftContainer.offsetWidth + ( _this.setup.chart.panelsSettings.panelSpacing * 2 ); } if ( _this.setup.chart.rightContainer ) { offset.width -= _this.setup.chart.rightContainer.offsetWidth; padding.right = _this.setup.chart.rightContainer.offsetWidth + ( _this.setup.chart.panelsSettings.panelSpacing * 2 ); } if ( _this.setup.chart.periodSelector && [ "top", "bottom" ].indexOf( _this.setup.chart.periodSelector.position ) != -1 ) { offset.height -= _this.setup.chart.periodSelector.offsetHeight + _this.setup.chart.panelsSettings.panelSpacing; padding[ _this.setup.chart.periodSelector.position ] += _this.setup.chart.periodSelector.offsetHeight + _this.setup.chart.panelsSettings.panelSpacing; } if ( _this.setup.chart.dataSetSelector && [ "top", "bottom" ].indexOf( _this.setup.chart.dataSetSelector.position ) != -1 ) { offset.height -= _this.setup.chart.dataSetSelector.offsetHeight; padding[ _this.setup.chart.dataSetSelector.position ] += _this.setup.chart.dataSetSelector.offsetHeight; } // APPLY OFFSET ON WRAPPER _this.setup.wrapper.style.paddingTop = _this.numberToPx( padding.top ); _this.setup.wrapper.style.paddingRight = _this.numberToPx( padding.right ); _this.setup.wrapper.style.paddingBottom = _this.numberToPx( padding.bottom ); _this.setup.wrapper.style.paddingLeft = _this.numberToPx( padding.left ); } // CREATE CANVAS _this.setup.canvas = document.createElement( "canvas" ); _this.setup.wrapper.appendChild( _this.setup.canvas ); _this.setup.fabric = new fabric.Canvas( _this.setup.canvas, _this.deepMerge( { width: offset.width, height: offset.height, isDrawingMode: true }, cfg ) ); // REAPPLY FOR SOME REASON _this.deepMerge( _this.setup.fabric, cfg ); _this.deepMerge( _this.setup.fabric.freeDrawingBrush, cfg.drawing ); // RELIABLE VARIABLES; UPDATE DRAWING _this.deepMerge( _this.drawing, cfg.drawing ); _this.drawing.handler.change( cfg.drawing ); // OBSERVE MOUSE EVENTS _this.setup.fabric.on( "mouse:down", function( e ) { var p = _this.gatherPosition( e.e, 1 ); _this.drawing.buffer.pressedTS = Number( new Date() ); _this.isPressed( e.e ); // FLAG ISDRAWING _this.drawing.buffer.isDrawing = false; _this.drawing.buffer.isDrawingTimer = setTimeout( function() { if ( !_this.drawing.buffer.isSelected ) { _this.drawing.buffer.isDrawing = true; } }, 200 ); } ); _this.setup.fabric.on( "mouse:move", function( e ) { var p = _this.gatherPosition( e.e, 2 ); _this.isPressed( e.e ); // IS PRESSED BUT UNSELECTED if ( _this.drawing.buffer.isPressed && !_this.drawing.buffer.isSelected ) { // FLAG ISDRAWING _this.drawing.buffer.isDrawing = true; // CREATE INITIAL LINE / ARROW; JUST ON LEFT CLICK if ( !_this.drawing.buffer.line && _this.drawing.mode != "pencil" && ( p.xD > 5 || p.yD > 5 ) ) { // FORCE FABRIC TO DISABLE DRAWING MODE WHILE PRESSED / MOVEING MOUSE INPUT _this.setup.fabric.isDrawingMode = false; _this.setup.fabric._isCurrentlyDrawing = false; _this.setup.fabric.freeDrawingBrush.onMouseUp(); _this.setup.fabric.remove( _this.setup.fabric._objects.pop() ); // INITIAL POINT _this.drawing.buffer.line = _this.drawing.handler.line( { x1: p.x1, y1: p.y1, x2: p.x2, y2: p.y2, arrow: _this.drawing.mode == "line" ? false : _this.drawing.arrow, action: "config" } ); } } if ( _this.drawing.buffer.isSelected ) { _this.setup.fabric.isDrawingMode = false; } // UPDATE LINE / ARROW if ( _this.drawing.buffer.line ) { var obj, top, left; var l = _this.drawing.buffer.line; l.x2 = p.x2; l.y2 = p.y2; // // RESET INTERNAL FLAGS // _this.drawing.buffer.isDrawing = true; // _this.drawing.buffer.isPressed = true; // _this.drawing.buffer.hasLine = true; for ( i1 = 0; i1 < l.group.length; i1++ ) { obj = l.group[ i1 ]; if ( obj instanceof fabric.Line ) { obj.set( { x2: l.x2, y2: l.y2 } ); } else if ( obj instanceof fabric.Triangle ) { l.angle = ( _this.getAngle( l.x1, l.y1, l.x2, l.y2 ) + 90 ); if ( l.arrow == "start" ) { top = l.y1 + ( l.width / 2 ); left = l.x1 + ( l.width / 2 ); } else if ( l.arrow == "middle" ) { top = l.y2 + ( l.width / 2 ) - ( ( l.y2 - l.y1 ) / 2 ); left = l.x2 + ( l.width / 2 ) - ( ( l.x2 - l.x1 ) / 2 ); } else { // arrow: end top = l.y2 + ( l.width / 2 ); left = l.x2 + ( l.width / 2 ); } obj.set( { top: top, left: left, angle: l.angle } ); } } _this.setup.fabric.renderAll(); } } ); _this.setup.fabric.on( "mouse:up", function( e ) { // SELECT TARGET if ( !_this.drawing.buffer.isDrawing ) { var target = _this.setup.fabric.findTarget( e.e ); if ( target && target.selectable ) { _this.setup.fabric.setActiveObject( target ); } } // UPDATE LINE / ARROW if ( _this.drawing.buffer.line ) { for ( i1 = 0; i1 < _this.drawing.buffer.line.group.length; i1++ ) { _this.drawing.buffer.line.group[ i1 ].remove(); } delete _this.drawing.buffer.line.action; delete _this.drawing.buffer.line.group; _this.drawing.handler.line( _this.drawing.buffer.line ); } _this.drawing.buffer.line = false; _this.drawing.buffer.hasLine = false; _this.drawing.buffer.isPressed = false; // RESET ISDRAWING FLAG clearTimeout( _this.drawing.buffer.isDrawingTimer ); _this.drawing.buffer.isDrawing = false; } ); // OBSERVE OBJECT SELECTION _this.setup.fabric.on( "object:selected", function( e ) { _this.drawing.buffer.isSelected = true; _this.drawing.buffer.target = e.target; _this.setup.fabric.isDrawingMode = false; } ); _this.setup.fabric.on( "selection:cleared", function( e ) { _this.drawing.buffer.target = false; // FREEHAND WORKAROUND if ( _this.drawing.buffer.isSe