dvf-2.x-dev/js/jquery.chart_export.js
js/jquery.chart_export.js
(function ($) {
"use strict";
/*
* ChartExport jQuery plugin.
* ---------------------------
* This plugin handles exporting a chart to a svg/png format.
*
* Usage.
* ------
* Bind this to the button you wish to trigger the download of the chart.
* Eg. $('button).chartExport({svg: 'svg.my-chart', format: 'svg'});
*
* Options/settings:
* - svg: (required) This is either a jQuery object or jQuery selector for the chart.
* - format: (string) The save as format (svg or png). Defaults to svg
* - filename: (string) The filename to use when saving, Defaults to 'chart'
* - includeExportStyles: (bool) Should we inject some styles that fix output bugs. Defaults to true
* - exportStylesheet: (string) Optional stylesheet to embed in the svg. Requires includeExportStyles = true.
* - errorMsg: (string) The error message to display if no browser support for downloading.
*/
/**
* chartExport class.
*
* @param dom
* The dom for the triggering element (eg. the button/link).
* @param settings
* Settings for the class. @see defaults.
* @returns {chartExport}
* Return self for chaining.
*/
var ChartExport = function (dom, settings) {
var self = this;
// Defaults.
self.defaults = {
// The button this is getting bound to.
$button: $(dom),
// Chart SVG selector or jQuery object.
svg: 'svg',
// Format to save as.
format: 'svg',
// Filename (without extension).
filename: 'chart',
// Export as png dimensions. When values are empty, will automatically size.
width: '',
height: '',
// Embed additional styles in svg (fixes display bugs with billboard.js charts)
includeExportStyles: true,
exportStylesheets: [],
// If an export stylesheet is provided, these styles get replaced with the stylesheet content.
exportStyles: 'svg{font:10px sans-serif}line,path{fill:none;stroke:#000}.bb-bar{stroke:none!important}',
// Passed Validation requirements.
valid: true,
// Error message.
errorMsg: 'Sorry, your browser does not support this function.',
// Manual download message.
manualDownloadMessage: 'Your browser requires manual saving of this file, when redirected to the image, ' +
'right click and "Save {saveType} As..." to save it to your computer with the filename "{filename}"',
ieDownloadMessage: 'Right click the graph and select "Save picture as" then in the save dialog, set the ' +
'"Save as type" to '
};
// Update defaults with passed settings.
self.settings = $.extend(self.defaults, settings);
/*
* Validate requirements.
*/
self.validateRequirements = function () {
// TODO: Check html5 support.
try {
var isFileSaverSupported = !!new Blob;
} catch (e) {
self.settings.valid = false;
}
// Return self for chaining.
return self;
};
/*
* Parse the svg object.
*/
self.parseSvg = function () {
// Processed class.
var processedClass = 'chart-export-processed';
// If svg is not a jQuery object, make it one.
if (self.settings.svg.jquery === undefined) {
self.settings.svg = $(self.settings.svg);
}
// Ensure our svg is actually an svg (not a wrapper element).
self.settings.svg = self.settings.svg.is('svg') ? self.settings.svg : self.settings.svg.find('> svg');
// If no svg, validation failed.
if (self.settings.svg.length === 0) {
self.settings.valid = false;
return self;
}
// Don't process the svg more than once.
if (self.settings.svg.data('processed') === true) {
return self;
}
// Add some XML attributes.
self.settings.svg
.attr('version', 1.1)
.attr('xmlns', 'http://www.w3.org/2000/svg')
.attr('class', 'chart-export ' + self.settings.svg.attr('class'))
.data('processed', true);
// Remove clip path from groups.
self.settings.svg.find('g').removeAttr('clip-path');
// Add font family to texts.
self.settings.svg.find('text').attr('font-family', '\'arial\'');
// Include billboard.js styles.
self.includeExportStyles();
// Return self for chaining.
return self;
};
/*
* Inject billboard.js styles if required.
*/
self.includeExportStyles = function () {
// Check if styles need to be added first.
if (!self.settings.includeExportStyles) {
return self;
}
// jQuery 1.5+ is required to use exported styles as deferreds are used.
// See: https://api.jquery.com/jquery.when/
if (typeof $.when !== 'function') {
return self;
}
// If export stylesheets are provided, we load their content.
if (self.settings.exportStylesheets.length > 0) {
var deferreds = [], cssUrl, i;
// Ensure we don't have any duplicate stylesheets before fetching (a possible result
// of drupal_add_js running multiple times).
var cssSheets = self.settings.exportStylesheets.filter(function(element, index, array){
return array.indexOf(element) >= index;
});
// Fetch each stylesheet and append to exportStyles.
for (i in cssSheets) {
cssUrl = cssSheets[i];
deferreds.push(
$.get(cssUrl, self.appendStyles)
);
}
// After all the sheets are fetched, add them.
$.when.apply($, deferreds).then(function () {
self.embedStyles();
});
} else {
// No export stylesheet, use default styles.
self.embedStyles();
}
// Return self for chaining.
return self;
};
/*
* Callback for joining all external styles together.
*/
self.appendStyles = function(css) {
self.settings.exportStyles += css;
};
// Embed current export styles.
self.embedStyles = function () {
// Create a style element.
self.$chartStyles = $('<style>')
.attr('type', 'text/css')
.html("<![CDATA[\n" + self.settings.exportStyles + "\n]]>")
.appendTo($('defs', self.settings.svg));
};
/*
* Save as SVG.
*/
self.saveSVG = function () {
// Save using blob and filesaver.js.
var blob = new Blob([self.svgHtml], {type: "image/svg+xml"});
saveAs(blob, self.getFilename());
};
/*
* Save as PNG.
*/
self.savePNG = function () {
// Create a new image and add the svg as a src.
var image = new Image();
// Unescape/encodeURIComponent solves issues with non utf-8 strings.
image.src = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(self.svgHtml)));
// On image load, trigger its download with html5 download attr.
image.onload = function () {
// Once loaded, turn the image object into a 2d canvas.
var canvas = document.createElement('canvas'), context, dimensions;
dimensions = self.getDimensions(image);
canvas.width = dimensions.width;
canvas.height = dimensions.height;
context = canvas.getContext('2d');
context.drawImage(image, 0, 0, dimensions.width, dimensions.height);
// Save using canvas-toBlob.js.
canvas.toBlob(function (blob) {
saveAs(blob, self.getFilename());
});
};
};
/*
* Returns the width and height for the canvas based on settings and AR.
*
* @param image
* The image with the src populated as the chart.
*/
self.getDimensions = function (image) {
// Default to the image width/height.
var d = {width: image.width, height: image.height}, ar;
// If overridden we do some additional processing with the settings.
if (self.settings.width !== '') {
// Width set, height optional.
d.width = parseInt(self.settings.width);
// If no height, we calculate with the aspect ratio.
if (self.settings.height === '') {
ar = (image.height / image.width);
d.height = Math.round((d.width * ar));
} else {
// Defined width and height: May result in cropping or poor AR.
d.height = parseInt(self.settings.height);
}
} else if (self.settings.height !== '') {
// No width, only height defined, calculate width with AR.
d.height = parseInt(self.settings.height);
ar = (image.width / image.height);
d.width = Math.round((d.height * ar));
}
// Return the dimensions object.
return d;
};
/*
* Click action.
*/
self.bindClick = function (e) {
e.preventDefault();
// If not passed validation, clicking the button returns an error msg.
if (self.settings.valid === false) {
return alert(self.settings.errorMsg);
}
// Safari currently doesn't support filesaver
// @see https://github.com/eligrey/FileSaver.js/issues/12
// So as a workaround we instruct the user on how to download.
if (self.isSafari()) {
var saveType = 'png' === self.settings.format ? 'Image' : 'Page';
alert(self.settings.manualDownloadMessage.replace('{filename}', self.getFilename()).replace('{saveType}', saveType));
}
// IE doesn't have very good blob support causing save to fail, so show a
// message instead. IE also has issues with duplicating xmlns and outerHTML.
// Works fine in EDGE.
if (self.isIE()) {
alert(self.settings.ieDownloadMessage + '"' + self.settings.format.toUpperCase() + '"');
return;
}
// Get the html for the svg (emulates cross browser outerHtml).
self.svgHtml = self.settings.svg[0].outerHTML;
// Call the save method based on format.
switch (self.settings.format.toLowerCase()) {
case 'svg':
self.saveSVG();
break;
case 'png':
self.savePNG();
break;
}
};
/*
* Safari check due to issue with filesaver.
*/
self.isSafari = function () {
return navigator.vendor && navigator.vendor.indexOf('Apple') > -1 && navigator.userAgent && !navigator.userAgent.match('CriOS');
};
/*
* Internet Explorer check due to issue with blobs (and other things).
*/
self.isIE = function () {
var ua = window.navigator.userAgent;
var msie = ua.indexOf("MSIE ");
return (msie > 0 || !!navigator.userAgent.match(/Trident.*rv\:11\./));
};
/*
* Return a clean filename with extension.
*/
self.getFilename = function () {
var cleanFilename = self.settings.filename.replace(/[^a-z0-9_\-]/gi, '_').toLowerCase();
return cleanFilename + '.' + self.settings.format;
};
/*
* Init the class.
*/
self.init = function () {
// Only one export per button.
if (self.settings.$button.hasClass('chart-export-processed')) {
return;
}
// Prepare requirements and svg.
self
.validateRequirements()
.parseSvg();
// Bind click and mark as processed.
self.settings.$button
.click(self.bindClick)
.addClass('chart-export-processed');
};
// Kick it off on construct.
self.init();
// Return self for chaining.
return self;
};
/**
* jQuery plugin/function.
*/
$.fn.chartExport = function (settings) {
return this.each(function (i, dom) {
new ChartExport(dom, settings)
});
};
})(jQuery);
