纽威
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.

715 lines
17 KiB

3 years ago
  1. /**
  2. * @license Highcharts JS v3.0.10 (2014-03-10)
  3. * Exporting module
  4. *
  5. * (c) 2010-2014 Torstein Honsi
  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, formAttributes) {
  150. var name,
  151. form;
  152. // create the form
  153. form = createElement('form', merge({
  154. method: 'post',
  155. action: url,
  156. enctype: 'multipart/form-data'
  157. }, formAttributes), {
  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, width and formAttributes.
  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. }, options.formAttributes);
  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. }, 200);
  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. docMouseUpHandler = function (e) {
  387. if (!chart.pointer.inClass(e.target, className)) {
  388. hide();
  389. }
  390. };
  391. // create the menu only the first time
  392. if (!menu) {
  393. // create a HTML element above the SVG
  394. chart[cacheName] = menu = createElement(DIV, {
  395. className: className
  396. }, {
  397. position: ABSOLUTE,
  398. zIndex: 1000,
  399. padding: menuPadding + PX
  400. }, chart.container);
  401. innerMenu = createElement(DIV, null,
  402. extend({
  403. MozBoxShadow: boxShadow,
  404. WebkitBoxShadow: boxShadow,
  405. boxShadow: boxShadow
  406. }, navOptions.menuStyle), menu);
  407. // hide on mouse out
  408. hide = function () {
  409. css(menu, { display: NONE });
  410. if (button) {
  411. button.setState(0);
  412. }
  413. chart.openMenu = false;
  414. };
  415. // Hide the menu some time after mouse leave (#1357)
  416. addEvent(menu, 'mouseleave', function () {
  417. hideTimer = setTimeout(hide, 500);
  418. });
  419. addEvent(menu, 'mouseenter', function () {
  420. clearTimeout(hideTimer);
  421. });
  422. // Hide it on clicking or touching outside the menu (#2258, #2335, #2407)
  423. addEvent(document, 'mouseup', docMouseUpHandler);
  424. addEvent(chart, 'destroy', function () {
  425. removeEvent(document, 'mouseup', docMouseUpHandler);
  426. });
  427. // create the items
  428. each(items, function (item) {
  429. if (item) {
  430. var element = item.separator ?
  431. createElement('hr', null, null, innerMenu) :
  432. createElement(DIV, {
  433. onmouseover: function () {
  434. css(this, navOptions.menuItemHoverStyle);
  435. },
  436. onmouseout: function () {
  437. css(this, menuItemStyle);
  438. },
  439. onclick: function () {
  440. hide();
  441. item.onclick.apply(chart, arguments);
  442. },
  443. innerHTML: item.text || chart.options.lang[item.textKey]
  444. }, extend({
  445. cursor: 'pointer'
  446. }, menuItemStyle), innerMenu);
  447. // Keep references to menu divs to be able to destroy them
  448. chart.exportDivElements.push(element);
  449. }
  450. });
  451. // Keep references to menu and innerMenu div to be able to destroy them
  452. chart.exportDivElements.push(innerMenu, menu);
  453. chart.exportMenuWidth = menu.offsetWidth;
  454. chart.exportMenuHeight = menu.offsetHeight;
  455. }
  456. menuStyle = { display: 'block' };
  457. // if outside right, right align it
  458. if (x + chart.exportMenuWidth > chartWidth) {
  459. menuStyle.right = (chartWidth - x - width - menuPadding) + PX;
  460. } else {
  461. menuStyle.left = (x - menuPadding) + PX;
  462. }
  463. // if outside bottom, bottom align it
  464. if (y + height + chart.exportMenuHeight > chartHeight && button.alignOptions.verticalAlign !== 'top') {
  465. menuStyle.bottom = (chartHeight - y - menuPadding) + PX;
  466. } else {
  467. menuStyle.top = (y + height - menuPadding) + PX;
  468. }
  469. css(menu, menuStyle);
  470. chart.openMenu = true;
  471. },
  472. /**
  473. * Add the export button to the chart
  474. */
  475. addButton: function (options) {
  476. var chart = this,
  477. renderer = chart.renderer,
  478. btnOptions = merge(chart.options.navigation.buttonOptions, options),
  479. onclick = btnOptions.onclick,
  480. menuItems = btnOptions.menuItems,
  481. symbol,
  482. button,
  483. symbolAttr = {
  484. stroke: btnOptions.symbolStroke,
  485. fill: btnOptions.symbolFill
  486. },
  487. symbolSize = btnOptions.symbolSize || 12;
  488. if (!chart.btnCount) {
  489. chart.btnCount = 0;
  490. }
  491. // Keeps references to the button elements
  492. if (!chart.exportDivElements) {
  493. chart.exportDivElements = [];
  494. chart.exportSVGElements = [];
  495. }
  496. if (btnOptions.enabled === false) {
  497. return;
  498. }
  499. var attr = btnOptions.theme,
  500. states = attr.states,
  501. hover = states && states.hover,
  502. select = states && states.select,
  503. callback;
  504. delete attr.states;
  505. if (onclick) {
  506. callback = function () {
  507. onclick.apply(chart, arguments);
  508. };
  509. } else if (menuItems) {
  510. callback = function () {
  511. chart.contextMenu(
  512. button.menuClassName,
  513. menuItems,
  514. button.translateX,
  515. button.translateY,
  516. button.width,
  517. button.height,
  518. button
  519. );
  520. button.setState(2);
  521. };
  522. }
  523. if (btnOptions.text && btnOptions.symbol) {
  524. attr.paddingLeft = Highcharts.pick(attr.paddingLeft, 25);
  525. } else if (!btnOptions.text) {
  526. extend(attr, {
  527. width: btnOptions.width,
  528. height: btnOptions.height,
  529. padding: 0
  530. });
  531. }
  532. button = renderer.button(btnOptions.text, 0, 0, callback, attr, hover, select)
  533. .attr({
  534. title: chart.options.lang[btnOptions._titleKey],
  535. 'stroke-linecap': 'round'
  536. });
  537. button.menuClassName = options.menuClassName || PREFIX + 'menu-' + chart.btnCount++;
  538. if (btnOptions.symbol) {
  539. symbol = renderer.symbol(
  540. btnOptions.symbol,
  541. btnOptions.symbolX - (symbolSize / 2),
  542. btnOptions.symbolY - (symbolSize / 2),
  543. symbolSize,
  544. symbolSize
  545. )
  546. .attr(extend(symbolAttr, {
  547. 'stroke-width': btnOptions.symbolStrokeWidth || 1,
  548. zIndex: 1
  549. })).add(button);
  550. }
  551. button.add()
  552. .align(extend(btnOptions, {
  553. width: button.width,
  554. x: Highcharts.pick(btnOptions.x, buttonOffset) // #1654
  555. }), true, 'spacingBox');
  556. buttonOffset += (button.width + btnOptions.buttonSpacing) * (btnOptions.align === 'right' ? -1 : 1);
  557. chart.exportSVGElements.push(button, symbol);
  558. },
  559. /**
  560. * Destroy the buttons.
  561. */
  562. destroyExport: function (e) {
  563. var chart = e.target,
  564. i,
  565. elem;
  566. // Destroy the extra buttons added
  567. for (i = 0; i < chart.exportSVGElements.length; i++) {
  568. elem = chart.exportSVGElements[i];
  569. // Destroy and null the svg/vml elements
  570. if (elem) { // #1822
  571. elem.onclick = elem.ontouchstart = null;
  572. chart.exportSVGElements[i] = elem.destroy();
  573. }
  574. }
  575. // Destroy the divs for the menu
  576. for (i = 0; i < chart.exportDivElements.length; i++) {
  577. elem = chart.exportDivElements[i];
  578. // Remove the event handler
  579. removeEvent(elem, 'mouseleave');
  580. // Remove inline events
  581. chart.exportDivElements[i] = elem.onmouseout = elem.onmouseover = elem.ontouchstart = elem.onclick = null;
  582. // Destroy the div by moving to garbage bin
  583. discardElement(elem);
  584. }
  585. }
  586. });
  587. symbols.menu = function (x, y, width, height) {
  588. var arr = [
  589. M, x, y + 2.5,
  590. L, x + width, y + 2.5,
  591. M, x, y + height / 2 + 0.5,
  592. L, x + width, y + height / 2 + 0.5,
  593. M, x, y + height - 1.5,
  594. L, x + width, y + height - 1.5
  595. ];
  596. return arr;
  597. };
  598. // Add the buttons on chart load
  599. Chart.prototype.callbacks.push(function (chart) {
  600. var n,
  601. exportingOptions = chart.options.exporting,
  602. buttons = exportingOptions.buttons;
  603. buttonOffset = 0;
  604. if (exportingOptions.enabled !== false) {
  605. for (n in buttons) {
  606. chart.addButton(buttons[n]);
  607. }
  608. // Destroy the export elements at chart destroy
  609. addEvent(chart, 'destroy', chart.destroyExport);
  610. }
  611. });
  612. }(Highcharts));