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.

685 lines
24 KiB

2 years ago
  1. /**
  2. * @license Highcharts JS v3.0.6 (2013-10-04)
  3. * Exporting module
  4. *
  5. * (c) 2010-2013 Torstein Hønsi
  6. *
  7. * License: www.highcharts.com/license
  8. */
  9. // JSLint options:
  10. /*global Highcharts, document, window, Math, setTimeout */
  11. (function (Highcharts) { // encapsulate
  12. // create shortcuts
  13. var Chart = Highcharts.Chart,
  14. addEvent = Highcharts.addEvent,
  15. removeEvent = Highcharts.removeEvent,
  16. createElement = Highcharts.createElement,
  17. discardElement = Highcharts.discardElement,
  18. css = Highcharts.css,
  19. merge = Highcharts.merge,
  20. each = Highcharts.each,
  21. extend = Highcharts.extend,
  22. math = Math,
  23. mathMax = math.max,
  24. doc = document,
  25. win = window,
  26. isTouchDevice = Highcharts.isTouchDevice,
  27. M = 'M',
  28. L = 'L',
  29. DIV = 'div',
  30. HIDDEN = 'hidden',
  31. NONE = 'none',
  32. PREFIX = 'highcharts-',
  33. ABSOLUTE = 'absolute',
  34. PX = 'px',
  35. UNDEFINED,
  36. symbols = Highcharts.Renderer.prototype.symbols,
  37. defaultOptions = Highcharts.getOptions(),
  38. buttonOffset;
  39. // Add language
  40. extend(defaultOptions.lang, {
  41. printChart: 'Print chart',
  42. downloadPNG: 'Download PNG image',
  43. downloadJPEG: 'Download JPEG image',
  44. downloadPDF: 'Download PDF document',
  45. downloadSVG: 'Download SVG vector image',
  46. contextButtonTitle: 'Chart context menu'
  47. });
  48. // Buttons and menus are collected in a separate config option set called 'navigation'.
  49. // This can be extended later to add control buttons like zoom and pan right click menus.
  50. defaultOptions.navigation = {
  51. menuStyle: {
  52. border: '1px solid #A0A0A0',
  53. background: '#FFFFFF',
  54. padding: '5px 0'
  55. },
  56. menuItemStyle: {
  57. padding: '0 10px',
  58. background: NONE,
  59. color: '#303030',
  60. fontSize: isTouchDevice ? '14px' : '11px'
  61. },
  62. menuItemHoverStyle: {
  63. background: '#4572A5',
  64. color: '#FFFFFF'
  65. },
  66. buttonOptions: {
  67. symbolFill: '#E0E0E0',
  68. symbolSize: 14,
  69. symbolStroke: '#666',
  70. symbolStrokeWidth: 3,
  71. symbolX: 12.5,
  72. symbolY: 10.5,
  73. align: 'right',
  74. buttonSpacing: 3,
  75. height: 22,
  76. // text: null,
  77. theme: {
  78. fill: 'white', // capture hover
  79. stroke: 'none'
  80. },
  81. verticalAlign: 'top',
  82. width: 24
  83. }
  84. };
  85. // Add the export related options
  86. defaultOptions.exporting = {
  87. //enabled: true,
  88. //filename: 'chart',
  89. type: 'image/png',
  90. url: 'http://export.highcharts.com/',
  91. //width: undefined,
  92. //scale: 2
  93. buttons: {
  94. contextButton: {
  95. menuClassName: PREFIX + 'contextmenu',
  96. //x: -10,
  97. symbol: 'menu',
  98. _titleKey: 'contextButtonTitle',
  99. menuItems: [{
  100. textKey: 'printChart',
  101. onclick: function () {
  102. this.print();
  103. }
  104. }, {
  105. separator: true
  106. }, {
  107. textKey: 'downloadPNG',
  108. onclick: function () {
  109. this.exportChart();
  110. }
  111. }, {
  112. textKey: 'downloadJPEG',
  113. onclick: function () {
  114. this.exportChart({
  115. type: 'image/jpeg'
  116. });
  117. }
  118. }, {
  119. textKey: 'downloadPDF',
  120. onclick: function () {
  121. this.exportChart({
  122. type: 'application/pdf'
  123. });
  124. }
  125. }, {
  126. textKey: 'downloadSVG',
  127. onclick: function () {
  128. this.exportChart({
  129. type: 'image/svg+xml'
  130. });
  131. }
  132. }
  133. // Enable this block to add "View SVG" to the dropdown menu
  134. /*
  135. ,{
  136. text: 'View SVG',
  137. onclick: function () {
  138. var svg = this.getSVG()
  139. .replace(/</g, '\n&lt;')
  140. .replace(/>/g, '&gt;');
  141. doc.body.innerHTML = '<pre>' + svg + '</pre>';
  142. }
  143. } // */
  144. ]
  145. }
  146. }
  147. };
  148. // Add the Highcharts.post utility
  149. Highcharts.post = function (url, data) {
  150. var name,
  151. form;
  152. // create the form
  153. form = createElement('form', {
  154. method: 'post',
  155. action: url,
  156. enctype: 'multipart/form-data'
  157. }, {
  158. display: NONE
  159. }, doc.body);
  160. // add the data
  161. for (name in data) {
  162. createElement('input', {
  163. type: HIDDEN,
  164. name: name,
  165. value: data[name]
  166. }, null, form);
  167. }
  168. // submit
  169. form.submit();
  170. // clean up
  171. discardElement(form);
  172. };
  173. extend(Chart.prototype, {
  174. /**
  175. * Return an SVG representation of the chart
  176. *
  177. * @param additionalOptions {Object} Additional chart options for the generated SVG representation
  178. */
  179. getSVG: function (additionalOptions) {
  180. var chart = this,
  181. chartCopy,
  182. sandbox,
  183. svg,
  184. seriesOptions,
  185. sourceWidth,
  186. sourceHeight,
  187. cssWidth,
  188. cssHeight,
  189. options = merge(chart.options, additionalOptions); // copy the options and add extra options
  190. // IE compatibility hack for generating SVG content that it doesn't really understand
  191. if (!doc.createElementNS) {
  192. /*jslint unparam: true*//* allow unused parameter ns in function below */
  193. doc.createElementNS = function (ns, tagName) {
  194. return doc.createElement(tagName);
  195. };
  196. /*jslint unparam: false*/
  197. }
  198. // create a sandbox where a new chart will be generated
  199. sandbox = createElement(DIV, null, {
  200. position: ABSOLUTE,
  201. top: '-9999em',
  202. width: chart.chartWidth + PX,
  203. height: chart.chartHeight + PX
  204. }, doc.body);
  205. // get the source size
  206. cssWidth = chart.renderTo.style.width;
  207. cssHeight = chart.renderTo.style.height;
  208. sourceWidth = options.exporting.sourceWidth ||
  209. options.chart.width ||
  210. (/px$/.test(cssWidth) && parseInt(cssWidth, 10)) ||
  211. 600;
  212. sourceHeight = options.exporting.sourceHeight ||
  213. options.chart.height ||
  214. (/px$/.test(cssHeight) && parseInt(cssHeight, 10)) ||
  215. 400;
  216. // override some options
  217. extend(options.chart, {
  218. animation: false,
  219. renderTo: sandbox,
  220. forExport: true,
  221. width: sourceWidth,
  222. height: sourceHeight
  223. });
  224. options.exporting.enabled = false; // hide buttons in print
  225. // prepare for replicating the chart
  226. options.series = [];
  227. each(chart.series, function (serie) {
  228. seriesOptions = merge(serie.options, {
  229. animation: false, // turn off animation
  230. showCheckbox: false,
  231. visible: serie.visible
  232. });
  233. if (!seriesOptions.isInternal) { // used for the navigator series that has its own option set
  234. options.series.push(seriesOptions);
  235. }
  236. });
  237. // generate the chart copy
  238. chartCopy = new Highcharts.Chart(options, chart.callback);
  239. // reflect axis extremes in the export
  240. each(['xAxis', 'yAxis'], function (axisType) {
  241. each(chart[axisType], function (axis, i) {
  242. var axisCopy = chartCopy[axisType][i],
  243. extremes = axis.getExtremes(),
  244. userMin = extremes.userMin,
  245. userMax = extremes.userMax;
  246. if (axisCopy && (userMin !== UNDEFINED || userMax !== UNDEFINED)) {
  247. axisCopy.setExtremes(userMin, userMax, true, false);
  248. }
  249. });
  250. });
  251. // get the SVG from the container's innerHTML
  252. svg = chartCopy.container.innerHTML;
  253. // free up memory
  254. options = null;
  255. chartCopy.destroy();
  256. discardElement(sandbox);
  257. // sanitize
  258. svg = svg
  259. .replace(/zIndex="[^"]+"/g, '')
  260. .replace(/isShadow="[^"]+"/g, '')
  261. .replace(/symbolName="[^"]+"/g, '')
  262. .replace(/jQuery[0-9]+="[^"]+"/g, '')
  263. .replace(/url\([^#]+#/g, 'url(#')
  264. .replace(/<svg /, '<svg xmlns:xlink="http://www.w3.org/1999/xlink" ')
  265. .replace(/ href=/g, ' xlink:href=')
  266. .replace(/\n/, ' ')
  267. .replace(/<\/svg>.*?$/, '</svg>') // any HTML added to the container after the SVG (#894)
  268. /* This fails in IE < 8
  269. .replace(/([0-9]+)\.([0-9]+)/g, function(s1, s2, s3) { // round off to save weight
  270. return s2 +'.'+ s3[0];
  271. })*/
  272. // Replace HTML entities, issue #347
  273. .replace(/&nbsp;/g, '\u00A0') // no-break space
  274. .replace(/&shy;/g, '\u00AD') // soft hyphen
  275. // IE specific
  276. .replace(/<IMG /g, '<image ')
  277. .replace(/height=([^" ]+)/g, 'height="$1"')
  278. .replace(/width=([^" ]+)/g, 'width="$1"')
  279. .replace(/hc-svg-href="([^"]+)">/g, 'xlink:href="$1"/>')
  280. .replace(/id=([^" >]+)/g, 'id="$1"')
  281. .replace(/class=([^" >]+)/g, 'class="$1"')
  282. .replace(/ transform /g, ' ')
  283. .replace(/:(path|rect)/g, '$1')
  284. .replace(/style="([^"]+)"/g, function (s) {
  285. return s.toLowerCase();
  286. });
  287. // IE9 beta bugs with innerHTML. Test again with final IE9.
  288. svg = svg.replace(/(url\(#highcharts-[0-9]+)&quot;/g, '$1')
  289. .replace(/&quot;/g, "'");
  290. return svg;
  291. },
  292. /**
  293. * Submit the SVG representation of the chart to the server
  294. * @param {Object} options Exporting options. Possible members are url, type and width.
  295. * @param {Object} chartOptions Additional chart options for the SVG representation of the chart
  296. */
  297. exportChart: function (options, chartOptions) {
  298. options = options || {};
  299. var chart = this,
  300. chartExportingOptions = chart.options.exporting,
  301. svg = chart.getSVG(merge(
  302. { chart: { borderRadius: 0 } },
  303. chartExportingOptions.chartOptions,
  304. chartOptions,
  305. {
  306. exporting: {
  307. sourceWidth: options.sourceWidth || chartExportingOptions.sourceWidth,
  308. sourceHeight: options.sourceHeight || chartExportingOptions.sourceHeight
  309. }
  310. }
  311. ));
  312. // merge the options
  313. options = merge(chart.options.exporting, options);
  314. // do the post
  315. Highcharts.post(options.url, {
  316. filename: options.filename || 'chart',
  317. type: options.type,
  318. width: options.width || 0, // IE8 fails to post undefined correctly, so use 0
  319. scale: options.scale || 2,
  320. svg: svg
  321. });
  322. },
  323. /**
  324. * Print the chart
  325. */
  326. print: function () {
  327. var chart = this,
  328. container = chart.container,
  329. origDisplay = [],
  330. origParent = container.parentNode,
  331. body = doc.body,
  332. childNodes = body.childNodes;
  333. if (chart.isPrinting) { // block the button while in printing mode
  334. return;
  335. }
  336. chart.isPrinting = true;
  337. // hide all body content
  338. each(childNodes, function (node, i) {
  339. if (node.nodeType === 1) {
  340. origDisplay[i] = node.style.display;
  341. node.style.display = NONE;
  342. }
  343. });
  344. // pull out the chart
  345. body.appendChild(container);
  346. // print
  347. win.focus(); // #1510
  348. win.print();
  349. // allow the browser to prepare before reverting
  350. setTimeout(function () {
  351. // put the chart back in
  352. origParent.appendChild(container);
  353. // restore all body content
  354. each(childNodes, function (node, i) {
  355. if (node.nodeType === 1) {
  356. node.style.display = origDisplay[i];
  357. }
  358. });
  359. chart.isPrinting = false;
  360. }, 1000);
  361. },
  362. /**
  363. * Display a popup menu for choosing the export type
  364. *
  365. * @param {String} className An identifier for the menu
  366. * @param {Array} items A collection with text and onclicks for the items
  367. * @param {Number} x The x position of the opener button
  368. * @param {Number} y The y position of the opener button
  369. * @param {Number} width The width of the opener button
  370. * @param {Number} height The height of the opener button
  371. */
  372. contextMenu: function (className, items, x, y, width, height, button) {
  373. var chart = this,
  374. navOptions = chart.options.navigation,
  375. menuItemStyle = navOptions.menuItemStyle,
  376. chartWidth = chart.chartWidth,
  377. chartHeight = chart.chartHeight,
  378. cacheName = 'cache-' + className,
  379. menu = chart[cacheName],
  380. menuPadding = mathMax(width, height), // for mouse leave detection
  381. boxShadow = '3px 3px 10px #888',
  382. innerMenu,
  383. hide,
  384. hideTimer,
  385. menuStyle;
  386. // create the menu only the first time
  387. if (!menu) {
  388. // create a HTML element above the SVG
  389. chart[cacheName] = menu = createElement(DIV, {
  390. className: className
  391. }, {
  392. position: ABSOLUTE,
  393. zIndex: 1000,
  394. padding: menuPadding + PX
  395. }, chart.container);
  396. innerMenu = createElement(DIV, null,
  397. extend({
  398. MozBoxShadow: boxShadow,
  399. WebkitBoxShadow: boxShadow,
  400. boxShadow: boxShadow
  401. }, navOptions.menuStyle), menu);
  402. // hide on mouse out
  403. hide = function () {
  404. css(menu, { display: NONE });
  405. if (button) {
  406. button.setState(0);
  407. }
  408. chart.openMenu = false;
  409. };
  410. // Hide the menu some time after mouse leave (#1357)
  411. addEvent(menu, 'mouseleave', function () {
  412. hideTimer = setTimeout(hide, 500);
  413. });
  414. addEvent(menu, 'mouseenter', function () {
  415. clearTimeout(hideTimer);
  416. });
  417. // Hide it on clicking or touching outside the menu (#2258)
  418. addEvent(document, 'mousedown', function (e) {
  419. if (!chart.pointer.inClass(e.target, className)) {
  420. hide();
  421. }
  422. });
  423. // create the items
  424. each(items, function (item) {
  425. if (item) {
  426. var element = item.separator ?
  427. createElement('hr', null, null, innerMenu) :
  428. createElement(DIV, {
  429. onmouseover: function () {
  430. css(this, navOptions.menuItemHoverStyle);
  431. },
  432. onmouseout: function () {
  433. css(this, menuItemStyle);
  434. },
  435. onclick: function () {
  436. hide();
  437. item.onclick.apply(chart, arguments);
  438. },
  439. innerHTML: item.text || chart.options.lang[item.textKey]
  440. }, extend({
  441. cursor: 'pointer'
  442. }, menuItemStyle), innerMenu);
  443. // Keep references to menu divs to be able to destroy them
  444. chart.exportDivElements.push(element);
  445. }
  446. });
  447. // Keep references to menu and innerMenu div to be able to destroy them
  448. chart.exportDivElements.push(innerMenu, menu);
  449. chart.exportMenuWidth = menu.offsetWidth;
  450. chart.exportMenuHeight = menu.offsetHeight;
  451. }
  452. menuStyle = { display: 'block' };
  453. // if outside right, right align it
  454. if (x + chart.exportMenuWidth > chartWidth) {
  455. menuStyle.right = (chartWidth - x - width - menuPadding) + PX;
  456. } else {
  457. menuStyle.left = (x - menuPadding) + PX;
  458. }
  459. // if outside bottom, bottom align it
  460. if (y + height + chart.exportMenuHeight > chartHeight && button.alignOptions.verticalAlign !== 'top') {
  461. menuStyle.bottom = (chartHeight - y - menuPadding) + PX;
  462. } else {
  463. menuStyle.top = (y + height - menuPadding) + PX;
  464. }
  465. css(menu, menuStyle);
  466. chart.openMenu = true;
  467. },
  468. /**
  469. * Add the export button to the chart
  470. */
  471. addButton: function (options) {
  472. var chart = this,
  473. renderer = chart.renderer,
  474. btnOptions = merge(chart.options.navigation.buttonOptions, options),
  475. onclick = btnOptions.onclick,
  476. menuItems = btnOptions.menuItems,
  477. symbol,
  478. button,
  479. symbolAttr = {
  480. stroke: btnOptions.symbolStroke,
  481. fill: btnOptions.symbolFill
  482. },
  483. symbolSize = btnOptions.symbolSize || 12;
  484. if (!chart.btnCount) {
  485. chart.btnCount = 0;
  486. }
  487. // Keeps references to the button elements
  488. if (!chart.exportDivElements) {
  489. chart.exportDivElements = [];
  490. chart.exportSVGElements = [];
  491. }
  492. if (btnOptions.enabled === false) {
  493. return;
  494. }
  495. var attr = btnOptions.theme,
  496. states = attr.states,
  497. hover = states && states.hover,
  498. select = states && states.select,
  499. callback;
  500. delete attr.states;
  501. if (onclick) {
  502. callback = function () {
  503. onclick.apply(chart, arguments);
  504. };
  505. } else if (menuItems) {
  506. callback = function () {
  507. chart.contextMenu(
  508. button.menuClassName,
  509. menuItems,
  510. button.translateX,
  511. button.translateY,
  512. button.width,
  513. button.height,
  514. button
  515. );
  516. button.setState(2);
  517. };
  518. }
  519. if (btnOptions.text && btnOptions.symbol) {
  520. attr.paddingLeft = Highcharts.pick(attr.paddingLeft, 25);
  521. } else if (!btnOptions.text) {
  522. extend(attr, {
  523. width: btnOptions.width,
  524. height: btnOptions.height,
  525. padding: 0
  526. });
  527. }
  528. button = renderer.button(btnOptions.text, 0, 0, callback, attr, hover, select)
  529. .attr({
  530. title: chart.options.lang[btnOptions._titleKey],
  531. 'stroke-linecap': 'round'
  532. });
  533. button.menuClassName = options.menuClassName || PREFIX + 'menu-' + chart.btnCount++;
  534. if (btnOptions.symbol) {
  535. symbol = renderer.symbol(
  536. btnOptions.symbol,
  537. btnOptions.symbolX - (symbolSize / 2),
  538. btnOptions.symbolY - (symbolSize / 2),
  539. symbolSize,
  540. symbolSize
  541. )
  542. .attr(extend(symbolAttr, {
  543. 'stroke-width': btnOptions.symbolStrokeWidth || 1,
  544. zIndex: 1
  545. })).add(button);
  546. }
  547. button.add()
  548. .align(extend(btnOptions, {
  549. width: button.width,
  550. x: Highcharts.pick(btnOptions.x, buttonOffset) // #1654
  551. }), true, 'spacingBox');
  552. buttonOffset += (button.width + btnOptions.buttonSpacing) * (btnOptions.align === 'right' ? -1 : 1);
  553. chart.exportSVGElements.push(button, symbol);
  554. },
  555. /**
  556. * Destroy the buttons.
  557. */
  558. destroyExport: function (e) {
  559. var chart = e.target,
  560. i,
  561. elem;
  562. // Destroy the extra buttons added
  563. for (i = 0; i < chart.exportSVGElements.length; i++) {
  564. elem = chart.exportSVGElements[i];
  565. // Destroy and null the svg/vml elements
  566. if (elem) { // #1822
  567. elem.onclick = elem.ontouchstart = null;
  568. chart.exportSVGElements[i] = elem.destroy();
  569. }
  570. }
  571. // Destroy the divs for the menu
  572. for (i = 0; i < chart.exportDivElements.length; i++) {
  573. elem = chart.exportDivElements[i];
  574. // Remove the event handler
  575. removeEvent(elem, 'mouseleave');
  576. // Remove inline events
  577. chart.exportDivElements[i] = elem.onmouseout = elem.onmouseover = elem.ontouchstart = elem.onclick = null;
  578. // Destroy the div by moving to garbage bin
  579. discardElement(elem);
  580. }
  581. }
  582. });
  583. symbols.menu = function (x, y, width, height) {
  584. var arr = [
  585. M, x, y + 2.5,
  586. L, x + width, y + 2.5,
  587. M, x, y + height / 2 + 0.5,
  588. L, x + width, y + height / 2 + 0.5,
  589. M, x, y + height - 1.5,
  590. L, x + width, y + height - 1.5
  591. ];
  592. return arr;
  593. };
  594. // Add the buttons on chart load
  595. Chart.prototype.callbacks.push(function (chart) {
  596. var n,
  597. exportingOptions = chart.options.exporting,
  598. buttons = exportingOptions.buttons;
  599. buttonOffset = 0;
  600. if (exportingOptions.enabled !== false) {
  601. for (n in buttons) {
  602. chart.addButton(buttons[n]);
  603. }
  604. // Destroy the export elements at chart destroy
  605. addEvent(chart, 'destroy', chart.destroyExport);
  606. }
  607. });
  608. }(Highcharts));