You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
709 lines
18 KiB
709 lines
18 KiB
/** |
|
* @license Highcharts JS v3.0.6 (2013-10-04) |
|
* 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) { |
|
var name, |
|
form; |
|
|
|
// create the form |
|
form = createElement('form', { |
|
method: 'post', |
|
action: url, |
|
enctype: 'multipart/form-data' |
|
}, { |
|
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, |
|
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: 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); |
|
|
|
// 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 |
|
}); |
|
|
|
}, |
|
|
|
/** |
|
* 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); |
|
|
|
// print |
|
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; |
|
|
|
// 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) |
|
addEvent(document, 'mousedown', function (e) { |
|
if (!chart.pointer.inClass(e.target, className)) { |
|
hide(); |
|
} |
|
}); |
|
|
|
|
|
// 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));
|
|
|