Created
December 3, 2013 11:07
-
-
Save TorsteinHonsi/7767462 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* @license @product.name@ JS [email protected]@ (@product.date@) | |
* Exporting module | |
* | |
* (c) 2010-2013 Torstein Hønsi | |
* | |
* License: www.highcharts.com/license | |
*/ | |
// JSLint options: | |
/*global Highcharts, document, window, Math, setTimeout */ | |
(function (Highcharts) { // encapsulate | |
// create shortcuts | |
var Chart = Highcharts.Chart, | |
addEvent = Highcharts.addEvent, | |
removeEvent = Highcharts.removeEvent, | |
createElement = Highcharts.createElement, | |
discardElement = Highcharts.discardElement, | |
css = Highcharts.css, | |
merge = Highcharts.merge, | |
each = Highcharts.each, | |
extend = Highcharts.extend, | |
math = Math, | |
mathMax = math.max, | |
doc = document, | |
win = window, | |
isTouchDevice = Highcharts.isTouchDevice, | |
M = 'M', | |
L = 'L', | |
DIV = 'div', | |
HIDDEN = 'hidden', | |
NONE = 'none', | |
PREFIX = 'highcharts-', | |
ABSOLUTE = 'absolute', | |
PX = 'px', | |
UNDEFINED, | |
symbols = Highcharts.Renderer.prototype.symbols, | |
defaultOptions = Highcharts.getOptions(), | |
buttonOffset; | |
// Add language | |
extend(defaultOptions.lang, { | |
printChart: 'Print chart', | |
downloadPNG: 'Download PNG image', | |
downloadJPEG: 'Download JPEG image', | |
downloadPDF: 'Download PDF document', | |
downloadSVG: 'Download SVG vector image', | |
contextButtonTitle: 'Chart context menu' | |
}); | |
// Buttons and menus are collected in a separate config option set called 'navigation'. | |
// This can be extended later to add control buttons like zoom and pan right click menus. | |
defaultOptions.navigation = { | |
menuStyle: { | |
border: '1px solid #A0A0A0', | |
background: '#FFFFFF', | |
padding: '5px 0' | |
}, | |
menuItemStyle: { | |
padding: '0 10px', | |
background: NONE, | |
color: '#303030', | |
fontSize: isTouchDevice ? '14px' : '11px' | |
}, | |
menuItemHoverStyle: { | |
background: '#4572A5', | |
color: '#FFFFFF' | |
}, | |
buttonOptions: { | |
symbolFill: '#E0E0E0', | |
symbolSize: 14, | |
symbolStroke: '#666', | |
symbolStrokeWidth: 3, | |
symbolX: 12.5, | |
symbolY: 10.5, | |
align: 'right', | |
buttonSpacing: 3, | |
height: 22, | |
// text: null, | |
theme: { | |
fill: 'white', // capture hover | |
stroke: 'none' | |
}, | |
verticalAlign: 'top', | |
width: 24 | |
} | |
}; | |
// Add the export related options | |
defaultOptions.exporting = { | |
//enabled: true, | |
//filename: 'chart', | |
type: 'image/png', | |
url: 'http://export.highcharts.com/', | |
//width: undefined, | |
//scale: 2 | |
buttons: { | |
contextButton: { | |
menuClassName: PREFIX + 'contextmenu', | |
//x: -10, | |
symbol: 'menu', | |
_titleKey: 'contextButtonTitle', | |
menuItems: [{ | |
textKey: 'printChart', | |
onclick: function () { | |
this.print(); | |
} | |
}, { | |
separator: true | |
}, { | |
textKey: 'downloadPNG', | |
onclick: function () { | |
this.exportChart(); | |
} | |
}, { | |
textKey: 'downloadJPEG', | |
onclick: function () { | |
this.exportChart({ | |
type: 'image/jpeg' | |
}); | |
} | |
}, { | |
textKey: 'downloadPDF', | |
onclick: function () { | |
this.exportChart({ | |
type: 'application/pdf' | |
}); | |
} | |
}, { | |
textKey: 'downloadSVG', | |
onclick: function () { | |
this.exportChart({ | |
type: 'image/svg+xml' | |
}); | |
} | |
} | |
// Enable this block to add "View SVG" to the dropdown menu | |
/* | |
,{ | |
text: 'View SVG', | |
onclick: function () { | |
var svg = this.getSVG() | |
.replace(/</g, '\n<') | |
.replace(/>/g, '>'); | |
doc.body.innerHTML = '<pre>' + svg + '</pre>'; | |
} | |
} // */ | |
] | |
} | |
} | |
}; | |
// Add the Highcharts.post utility | |
Highcharts.post = function (url, data, formAttributes) { | |
var name, | |
form; | |
// create the form | |
form = createElement('form', merge({ | |
method: 'post', | |
action: url, | |
enctype: 'multipart/form-data' | |
}, formAttributes), { // docs | |
display: NONE | |
}, doc.body); | |
// add the data | |
for (name in data) { | |
createElement('input', { | |
type: HIDDEN, | |
name: name, | |
value: data[name] | |
}, null, form); | |
} | |
// submit | |
form.submit(); | |
// clean up | |
discardElement(form); | |
}; | |
extend(Chart.prototype, { | |
/** | |
* Return an SVG representation of the chart | |
* | |
* @param additionalOptions {Object} Additional chart options for the generated SVG representation | |
*/ | |
getSVG: function (additionalOptions) { | |
var chart = this, | |
chartCopy, | |
sandbox, | |
svg, | |
seriesOptions, | |
sourceWidth, | |
sourceHeight, | |
cssWidth, | |
cssHeight, | |
html, | |
options = merge(chart.options, additionalOptions); // copy the options and add extra options | |
// IE compatibility hack for generating SVG content that it doesn't really understand | |
if (!doc.createElementNS) { | |
/*jslint unparam: true*//* allow unused parameter ns in function below */ | |
doc.createElementNS = function (ns, tagName) { | |
return doc.createElement(tagName); | |
}; | |
/*jslint unparam: false*/ | |
} | |
// create a sandbox where a new chart will be generated | |
sandbox = createElement(DIV, null, { | |
position: ABSOLUTE, | |
top: '-9999em', | |
width: chart.chartWidth + PX, | |
height: chart.chartHeight + PX | |
}, doc.body); | |
// get the source size | |
cssWidth = chart.renderTo.style.width; | |
cssHeight = chart.renderTo.style.height; | |
sourceWidth = options.exporting.sourceWidth || | |
options.chart.width || | |
(/px$/.test(cssWidth) && parseInt(cssWidth, 10)) || | |
600; | |
sourceHeight = options.exporting.sourceHeight || | |
options.chart.height || | |
(/px$/.test(cssHeight) && parseInt(cssHeight, 10)) || | |
400; | |
// override some options | |
extend(options.chart, { | |
animation: false, | |
renderTo: sandbox, | |
forExport: options.exporting.allowHTML !== true, | |
width: sourceWidth, | |
height: sourceHeight | |
}); | |
options.exporting.enabled = false; // hide buttons in print | |
// prepare for replicating the chart | |
options.series = []; | |
each(chart.series, function (serie) { | |
seriesOptions = merge(serie.options, { | |
animation: false, // turn off animation | |
showCheckbox: false, | |
visible: serie.visible | |
}); | |
if (!seriesOptions.isInternal) { // used for the navigator series that has its own option set | |
options.series.push(seriesOptions); | |
} | |
}); | |
// generate the chart copy | |
chartCopy = new Highcharts.Chart(options, chart.callback); | |
// reflect axis extremes in the export | |
each(['xAxis', 'yAxis'], function (axisType) { | |
each(chart[axisType], function (axis, i) { | |
var axisCopy = chartCopy[axisType][i], | |
extremes = axis.getExtremes(), | |
userMin = extremes.userMin, | |
userMax = extremes.userMax; | |
if (axisCopy && (userMin !== UNDEFINED || userMax !== UNDEFINED)) { | |
axisCopy.setExtremes(userMin, userMax, true, false); | |
} | |
}); | |
}); | |
// get the SVG from the container's innerHTML | |
svg = chartCopy.container.innerHTML; | |
// free up memory | |
options = null; | |
chartCopy.destroy(); | |
discardElement(sandbox); | |
// Move HTML into a foreignObject | |
html = svg.match(/<\/svg>(.*?$)/); | |
if (html) { | |
html = '<foreignObject x="0" y="0 width="200" height="200">' + | |
'<body xmlns="http://www.w3.org/1999/xhtml">' + | |
html[1] + | |
'</body>' + | |
'</foreignObject>'; | |
svg = svg.replace('</svg>', html + '</svg>'); | |
} | |
// sanitize | |
svg = svg | |
.replace(/zIndex="[^"]+"/g, '') | |
.replace(/isShadow="[^"]+"/g, '') | |
.replace(/symbolName="[^"]+"/g, '') | |
.replace(/jQuery[0-9]+="[^"]+"/g, '') | |
.replace(/url\([^#]+#/g, 'url(#') | |
.replace(/<svg /, '<svg xmlns:xlink="http://www.w3.org/1999/xlink" ') | |
.replace(/ href=/g, ' xlink:href=') | |
.replace(/\n/, ' ') | |
.replace(/<\/svg>.*?$/, '</svg>') // any HTML added to the container after the SVG (#894) | |
/* This fails in IE < 8 | |
.replace(/([0-9]+)\.([0-9]+)/g, function(s1, s2, s3) { // round off to save weight | |
return s2 +'.'+ s3[0]; | |
})*/ | |
// Replace HTML entities, issue #347 | |
.replace(/ /g, '\u00A0') // no-break space | |
.replace(/­/g, '\u00AD') // soft hyphen | |
// IE specific | |
.replace(/<IMG /g, '<image ') | |
.replace(/height=([^" ]+)/g, 'height="$1"') | |
.replace(/width=([^" ]+)/g, 'width="$1"') | |
.replace(/hc-svg-href="([^"]+)">/g, 'xlink:href="$1"/>') | |
.replace(/id=([^" >]+)/g, 'id="$1"') | |
.replace(/class=([^" >]+)/g, 'class="$1"') | |
.replace(/ transform /g, ' ') | |
.replace(/:(path|rect)/g, '$1') | |
.replace(/style="([^"]+)"/g, function (s) { | |
return s.toLowerCase(); | |
}); | |
// IE9 beta bugs with innerHTML. Test again with final IE9. | |
svg = svg.replace(/(url\(#highcharts-[0-9]+)"/g, '$1') | |
.replace(/"/g, "'"); | |
return svg; | |
}, | |
/** | |
* Submit the SVG representation of the chart to the server | |
* @param {Object} options Exporting options. Possible members are url, type and width. | |
* @param {Object} chartOptions Additional chart options for the SVG representation of the chart | |
*/ | |
exportChart: function (options, chartOptions) { | |
options = options || {}; | |
var chart = this, | |
chartExportingOptions = chart.options.exporting, | |
svg = chart.getSVG(merge( | |
{ chart: { borderRadius: 0 } }, | |
chartExportingOptions.chartOptions, | |
chartOptions, | |
{ | |
exporting: { | |
sourceWidth: options.sourceWidth || chartExportingOptions.sourceWidth, | |
sourceHeight: options.sourceHeight || chartExportingOptions.sourceHeight | |
} | |
} | |
)); | |
// merge the options | |
options = merge(chart.options.exporting, options); | |
// do the post | |
Highcharts.post(options.url, { | |
filename: options.filename || 'chart', | |
type: options.type, | |
width: options.width || 0, // IE8 fails to post undefined correctly, so use 0 | |
scale: options.scale || 2, | |
svg: svg | |
}, options.formAttributes); | |
}, | |
/** | |
* Print the chart | |
*/ | |
print: function () { | |
var chart = this, | |
container = chart.container, | |
origDisplay = [], | |
origParent = container.parentNode, | |
body = doc.body, | |
childNodes = body.childNodes; | |
if (chart.isPrinting) { // block the button while in printing mode | |
return; | |
} | |
chart.isPrinting = true; | |
// hide all body content | |
each(childNodes, function (node, i) { | |
if (node.nodeType === 1) { | |
origDisplay[i] = node.style.display; | |
node.style.display = NONE; | |
} | |
}); | |
// pull out the chart | |
body.appendChild(container); | |
win.focus(); // #1510 | |
win.print(); | |
// allow the browser to prepare before reverting | |
setTimeout(function () { | |
// put the chart back in | |
origParent.appendChild(container); | |
// restore all body content | |
each(childNodes, function (node, i) { | |
if (node.nodeType === 1) { | |
node.style.display = origDisplay[i]; | |
} | |
}); | |
chart.isPrinting = false; | |
}, 1000); | |
}, | |
/** | |
* Display a popup menu for choosing the export type | |
* | |
* @param {String} className An identifier for the menu | |
* @param {Array} items A collection with text and onclicks for the items | |
* @param {Number} x The x position of the opener button | |
* @param {Number} y The y position of the opener button | |
* @param {Number} width The width of the opener button | |
* @param {Number} height The height of the opener button | |
*/ | |
contextMenu: function (className, items, x, y, width, height, button) { | |
var chart = this, | |
navOptions = chart.options.navigation, | |
menuItemStyle = navOptions.menuItemStyle, | |
chartWidth = chart.chartWidth, | |
chartHeight = chart.chartHeight, | |
cacheName = 'cache-' + className, | |
menu = chart[cacheName], | |
menuPadding = mathMax(width, height), // for mouse leave detection | |
boxShadow = '3px 3px 10px #888', | |
innerMenu, | |
hide, | |
hideTimer, | |
menuStyle, | |
docMouseUpHandler = function (e) { | |
if (!chart.pointer.inClass(e.target, className)) { | |
hide(); | |
} | |
}; | |
// create the menu only the first time | |
if (!menu) { | |
// create a HTML element above the SVG | |
chart[cacheName] = menu = createElement(DIV, { | |
className: className | |
}, { | |
position: ABSOLUTE, | |
zIndex: 1000, | |
padding: menuPadding + PX | |
}, chart.container); | |
innerMenu = createElement(DIV, null, | |
extend({ | |
MozBoxShadow: boxShadow, | |
WebkitBoxShadow: boxShadow, | |
boxShadow: boxShadow | |
}, navOptions.menuStyle), menu); | |
// hide on mouse out | |
hide = function () { | |
css(menu, { display: NONE }); | |
if (button) { | |
button.setState(0); | |
} | |
chart.openMenu = false; | |
}; | |
// Hide the menu some time after mouse leave (#1357) | |
addEvent(menu, 'mouseleave', function () { | |
hideTimer = setTimeout(hide, 500); | |
}); | |
addEvent(menu, 'mouseenter', function () { | |
clearTimeout(hideTimer); | |
}); | |
// Hide it on clicking or touching outside the menu (#2258, #2335, #2407) | |
addEvent(document, 'mouseup', docMouseUpHandler); | |
addEvent(chart, 'destroy', function () { | |
removeEvent(document, 'mouseup', docMouseUpHandler); | |
}); | |
// create the items | |
each(items, function (item) { | |
if (item) { | |
var element = item.separator ? | |
createElement('hr', null, null, innerMenu) : | |
createElement(DIV, { | |
onmouseover: function () { | |
css(this, navOptions.menuItemHoverStyle); | |
}, | |
onmouseout: function () { | |
css(this, menuItemStyle); | |
}, | |
onclick: function () { | |
hide(); | |
item.onclick.apply(chart, arguments); | |
}, | |
innerHTML: item.text || chart.options.lang[item.textKey] | |
}, extend({ | |
cursor: 'pointer' | |
}, menuItemStyle), innerMenu); | |
// Keep references to menu divs to be able to destroy them | |
chart.exportDivElements.push(element); | |
} | |
}); | |
// Keep references to menu and innerMenu div to be able to destroy them | |
chart.exportDivElements.push(innerMenu, menu); | |
chart.exportMenuWidth = menu.offsetWidth; | |
chart.exportMenuHeight = menu.offsetHeight; | |
} | |
menuStyle = { display: 'block' }; | |
// if outside right, right align it | |
if (x + chart.exportMenuWidth > chartWidth) { | |
menuStyle.right = (chartWidth - x - width - menuPadding) + PX; | |
} else { | |
menuStyle.left = (x - menuPadding) + PX; | |
} | |
// if outside bottom, bottom align it | |
if (y + height + chart.exportMenuHeight > chartHeight && button.alignOptions.verticalAlign !== 'top') { | |
menuStyle.bottom = (chartHeight - y - menuPadding) + PX; | |
} else { | |
menuStyle.top = (y + height - menuPadding) + PX; | |
} | |
css(menu, menuStyle); | |
chart.openMenu = true; | |
}, | |
/** | |
* Add the export button to the chart | |
*/ | |
addButton: function (options) { | |
var chart = this, | |
renderer = chart.renderer, | |
btnOptions = merge(chart.options.navigation.buttonOptions, options), | |
onclick = btnOptions.onclick, | |
menuItems = btnOptions.menuItems, | |
symbol, | |
button, | |
symbolAttr = { | |
stroke: btnOptions.symbolStroke, | |
fill: btnOptions.symbolFill | |
}, | |
symbolSize = btnOptions.symbolSize || 12; | |
if (!chart.btnCount) { | |
chart.btnCount = 0; | |
} | |
// Keeps references to the button elements | |
if (!chart.exportDivElements) { | |
chart.exportDivElements = []; | |
chart.exportSVGElements = []; | |
} | |
if (btnOptions.enabled === false) { | |
return; | |
} | |
var attr = btnOptions.theme, | |
states = attr.states, | |
hover = states && states.hover, | |
select = states && states.select, | |
callback; | |
delete attr.states; | |
if (onclick) { | |
callback = function () { | |
onclick.apply(chart, arguments); | |
}; | |
} else if (menuItems) { | |
callback = function () { | |
chart.contextMenu( | |
button.menuClassName, | |
menuItems, | |
button.translateX, | |
button.translateY, | |
button.width, | |
button.height, | |
button | |
); | |
button.setState(2); | |
}; | |
} | |
if (btnOptions.text && btnOptions.symbol) { | |
attr.paddingLeft = Highcharts.pick(attr.paddingLeft, 25); | |
} else if (!btnOptions.text) { | |
extend(attr, { | |
width: btnOptions.width, | |
height: btnOptions.height, | |
padding: 0 | |
}); | |
} | |
button = renderer.button(btnOptions.text, 0, 0, callback, attr, hover, select) | |
.attr({ | |
title: chart.options.lang[btnOptions._titleKey], | |
'stroke-linecap': 'round' | |
}); | |
button.menuClassName = options.menuClassName || PREFIX + 'menu-' + chart.btnCount++; | |
if (btnOptions.symbol) { | |
symbol = renderer.symbol( | |
btnOptions.symbol, | |
btnOptions.symbolX - (symbolSize / 2), | |
btnOptions.symbolY - (symbolSize / 2), | |
symbolSize, | |
symbolSize | |
) | |
.attr(extend(symbolAttr, { | |
'stroke-width': btnOptions.symbolStrokeWidth || 1, | |
zIndex: 1 | |
})).add(button); | |
} | |
button.add() | |
.align(extend(btnOptions, { | |
width: button.width, | |
x: Highcharts.pick(btnOptions.x, buttonOffset) // #1654 | |
}), true, 'spacingBox'); | |
buttonOffset += (button.width + btnOptions.buttonSpacing) * (btnOptions.align === 'right' ? -1 : 1); | |
chart.exportSVGElements.push(button, symbol); | |
}, | |
/** | |
* Destroy the buttons. | |
*/ | |
destroyExport: function (e) { | |
var chart = e.target, | |
i, | |
elem; | |
// Destroy the extra buttons added | |
for (i = 0; i < chart.exportSVGElements.length; i++) { | |
elem = chart.exportSVGElements[i]; | |
// Destroy and null the svg/vml elements | |
if (elem) { // #1822 | |
elem.onclick = elem.ontouchstart = null; | |
chart.exportSVGElements[i] = elem.destroy(); | |
} | |
} | |
// Destroy the divs for the menu | |
for (i = 0; i < chart.exportDivElements.length; i++) { | |
elem = chart.exportDivElements[i]; | |
// Remove the event handler | |
removeEvent(elem, 'mouseleave'); | |
// Remove inline events | |
chart.exportDivElements[i] = elem.onmouseout = elem.onmouseover = elem.ontouchstart = elem.onclick = null; | |
// Destroy the div by moving to garbage bin | |
discardElement(elem); | |
} | |
} | |
}); | |
symbols.menu = function (x, y, width, height) { | |
var arr = [ | |
M, x, y + 2.5, | |
L, x + width, y + 2.5, | |
M, x, y + height / 2 + 0.5, | |
L, x + width, y + height / 2 + 0.5, | |
M, x, y + height - 1.5, | |
L, x + width, y + height - 1.5 | |
]; | |
return arr; | |
}; | |
// Add the buttons on chart load | |
Chart.prototype.callbacks.push(function (chart) { | |
var n, | |
exportingOptions = chart.options.exporting, | |
buttons = exportingOptions.buttons; | |
buttonOffset = 0; | |
if (exportingOptions.enabled !== false) { | |
for (n in buttons) { | |
chart.addButton(buttons[n]); | |
} | |
// Destroy the export elements at chart destroy | |
addEvent(chart, 'destroy', chart.destroyExport); | |
} | |
}); | |
}(Highcharts)); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment