OfflineExporting.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619
  1. /* *
  2. *
  3. * Client side exporting module
  4. *
  5. * (c) 2015 Torstein Honsi / Oystein Moseng
  6. *
  7. * License: www.highcharts.com/license
  8. *
  9. * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
  10. *
  11. * */
  12. import Chart from '../Core/Chart/Chart.js';
  13. import H from '../Core/Globals.js';
  14. var win = H.win, doc = H.doc;
  15. import '../Core/Options.js';
  16. import SVGRenderer from '../Core/Renderer/SVG/SVGRenderer.js';
  17. import U from '../Core/Utilities.js';
  18. var addEvent = U.addEvent, error = U.error, extend = U.extend, fireEvent = U.fireEvent, getOptions = U.getOptions, merge = U.merge;
  19. import DownloadURL from '../Extensions/DownloadURL.js';
  20. var downloadURL = DownloadURL.downloadURL;
  21. var domurl = win.URL || win.webkitURL || win,
  22. // Milliseconds to defer image load event handlers to offset IE bug
  23. loadEventDeferDelay = H.isMS ? 150 : 0;
  24. // Dummy object so we can reuse our canvas-tools.js without errors
  25. H.CanVGRenderer = {};
  26. /* eslint-disable valid-jsdoc */
  27. /**
  28. * Downloads a script and executes a callback when done.
  29. *
  30. * @private
  31. * @function getScript
  32. * @param {string} scriptLocation
  33. * @param {Function} callback
  34. * @return {void}
  35. */
  36. function getScript(scriptLocation, callback) {
  37. var head = doc.getElementsByTagName('head')[0], script = doc.createElement('script');
  38. script.type = 'text/javascript';
  39. script.src = scriptLocation;
  40. script.onload = callback;
  41. script.onerror = function () {
  42. error('Error loading script ' + scriptLocation);
  43. };
  44. head.appendChild(script);
  45. }
  46. /**
  47. * Get blob URL from SVG code. Falls back to normal data URI.
  48. *
  49. * @private
  50. * @function Highcharts.svgToDataURL
  51. * @param {string} svg
  52. * @return {string}
  53. */
  54. function svgToDataUrl(svg) {
  55. // Webkit and not chrome
  56. var userAgent = win.navigator.userAgent;
  57. var webKit = (userAgent.indexOf('WebKit') > -1 &&
  58. userAgent.indexOf('Chrome') < 0);
  59. try {
  60. // Safari requires data URI since it doesn't allow navigation to blob
  61. // URLs. Firefox has an issue with Blobs and internal references,
  62. // leading to gradients not working using Blobs (#4550)
  63. if (!webKit && !H.isFirefox) {
  64. return domurl.createObjectURL(new win.Blob([svg], {
  65. type: 'image/svg+xml;charset-utf-16'
  66. }));
  67. }
  68. }
  69. catch (e) {
  70. // Ignore
  71. }
  72. return 'data:image/svg+xml;charset=UTF-8,' + encodeURIComponent(svg);
  73. }
  74. /**
  75. * Get data:URL from image URL. Pass in callbacks to handle results.
  76. *
  77. * @private
  78. * @function Highcharts.imageToDataUrl
  79. *
  80. * @param {string} imageURL
  81. *
  82. * @param {string} imageType
  83. *
  84. * @param {*} callbackArgs
  85. * callbackArgs is used only by callbacks.
  86. *
  87. * @param {number} scale
  88. *
  89. * @param {Function} successCallback
  90. * Receives four arguments: imageURL, imageType, callbackArgs, and scale.
  91. *
  92. * @param {Function} taintedCallback
  93. * Receives four arguments: imageURL, imageType, callbackArgs, and scale.
  94. *
  95. * @param {Function} noCanvasSupportCallback
  96. * Receives four arguments: imageURL, imageType, callbackArgs, and scale.
  97. *
  98. * @param {Function} failedLoadCallback
  99. * Receives four arguments: imageURL, imageType, callbackArgs, and scale.
  100. *
  101. * @param {Function} [finallyCallback]
  102. * finallyCallback is always called at the end of the process. All
  103. * callbacks receive four arguments: imageURL, imageType, callbackArgs,
  104. * and scale.
  105. *
  106. * @return {void}
  107. */
  108. function imageToDataUrl(imageURL, imageType, callbackArgs, scale, successCallback, taintedCallback, noCanvasSupportCallback, failedLoadCallback, finallyCallback) {
  109. var img = new win.Image(), taintedHandler, loadHandler = function () {
  110. setTimeout(function () {
  111. var canvas = doc.createElement('canvas'), ctx = canvas.getContext && canvas.getContext('2d'), dataURL;
  112. try {
  113. if (!ctx) {
  114. noCanvasSupportCallback(imageURL, imageType, callbackArgs, scale);
  115. }
  116. else {
  117. canvas.height = img.height * scale;
  118. canvas.width = img.width * scale;
  119. ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
  120. // Now we try to get the contents of the canvas.
  121. try {
  122. dataURL = canvas.toDataURL(imageType);
  123. successCallback(dataURL, imageType, callbackArgs, scale);
  124. }
  125. catch (e) {
  126. taintedHandler(imageURL, imageType, callbackArgs, scale);
  127. }
  128. }
  129. }
  130. finally {
  131. if (finallyCallback) {
  132. finallyCallback(imageURL, imageType, callbackArgs, scale);
  133. }
  134. }
  135. // IE bug where image is not always ready despite calling load
  136. // event.
  137. }, loadEventDeferDelay);
  138. },
  139. // Image load failed (e.g. invalid URL)
  140. errorHandler = function () {
  141. failedLoadCallback(imageURL, imageType, callbackArgs, scale);
  142. if (finallyCallback) {
  143. finallyCallback(imageURL, imageType, callbackArgs, scale);
  144. }
  145. };
  146. // This is called on load if the image drawing to canvas failed with a
  147. // security error. We retry the drawing with crossOrigin set to Anonymous.
  148. taintedHandler = function () {
  149. img = new win.Image();
  150. taintedHandler = taintedCallback;
  151. // Must be set prior to loading image source
  152. img.crossOrigin = 'Anonymous';
  153. img.onload = loadHandler;
  154. img.onerror = errorHandler;
  155. img.src = imageURL;
  156. };
  157. img.onload = loadHandler;
  158. img.onerror = errorHandler;
  159. img.src = imageURL;
  160. }
  161. /* eslint-enable valid-jsdoc */
  162. /**
  163. * Get data URL to an image of an SVG and call download on it options object:
  164. *
  165. * - **filename:** Name of resulting downloaded file without extension. Default
  166. * is `chart`.
  167. *
  168. * - **type:** File type of resulting download. Default is `image/png`.
  169. *
  170. * - **scale:** Scaling factor of downloaded image compared to source. Default
  171. * is `1`.
  172. *
  173. * - **libURL:** URL pointing to location of dependency scripts to download on
  174. * demand. Default is the exporting.libURL option of the global Highcharts
  175. * options pointing to our server.
  176. *
  177. * @function Highcharts.downloadSVGLocal
  178. *
  179. * @param {string} svg
  180. * The generated SVG
  181. *
  182. * @param {Highcharts.ExportingOptions} options
  183. * The exporting options
  184. *
  185. * @param {Function} failCallback
  186. * The callback function in case of errors
  187. *
  188. * @param {Function} [successCallback]
  189. * The callback function in case of success
  190. *
  191. * @return {void}
  192. */
  193. function downloadSVGLocal(svg, options, failCallback, successCallback) {
  194. var svgurl, blob, objectURLRevoke = true, finallyHandler, libURL = (options.libURL || getOptions().exporting.libURL), dummySVGContainer = doc.createElement('div'), imageType = options.type || 'image/png', filename = ((options.filename || 'chart') +
  195. '.' +
  196. (imageType === 'image/svg+xml' ? 'svg' : imageType.split('/')[1])), scale = options.scale || 1;
  197. // Allow libURL to end with or without fordward slash
  198. libURL = libURL.slice(-1) !== '/' ? libURL + '/' : libURL;
  199. /* eslint-disable valid-jsdoc */
  200. /**
  201. * @private
  202. */
  203. function svgToPdf(svgElement, margin) {
  204. var width = svgElement.width.baseVal.value + 2 * margin, height = svgElement.height.baseVal.value + 2 * margin, pdf = new win.jsPDF(// eslint-disable-line new-cap
  205. height > width ? 'p' : 'l', // setting orientation to portrait if height exceeds width
  206. 'pt', [width, height]);
  207. // Workaround for #7090, hidden elements were drawn anyway. It comes
  208. // down to https://github.com/yWorks/svg2pdf.js/issues/28. Check this
  209. // later.
  210. [].forEach.call(svgElement.querySelectorAll('*[visibility="hidden"]'), function (node) {
  211. node.parentNode.removeChild(node);
  212. });
  213. // Workaround for #13948, multiple stops in linear gradient set to 0
  214. // causing error in Acrobat
  215. var gradients = svgElement.querySelectorAll('linearGradient');
  216. for (var index = 0; index < gradients.length; index++) {
  217. var gradient = gradients[index];
  218. var stops = gradient.querySelectorAll('stop');
  219. var i = 0;
  220. while (i < stops.length &&
  221. stops[i].getAttribute('offset') === '0' &&
  222. stops[i + 1].getAttribute('offset') === '0') {
  223. stops[i].remove();
  224. i++;
  225. }
  226. }
  227. // Workaround for #15135, zero width spaces, which Highcharts uses to
  228. // break lines, are not correctly rendered in PDF. Replace it with a
  229. // regular space and offset by some pixels to compensate.
  230. [].forEach.call(svgElement.querySelectorAll('tspan'), function (tspan) {
  231. if (tspan.textContent === '\u200B') {
  232. tspan.textContent = ' ';
  233. tspan.setAttribute('dx', -5);
  234. }
  235. });
  236. win.svg2pdf(svgElement, pdf, { removeInvalid: true });
  237. return pdf.output('datauristring');
  238. }
  239. /**
  240. * @private
  241. * @return {void}
  242. */
  243. function downloadPDF() {
  244. dummySVGContainer.innerHTML = svg;
  245. var textElements = dummySVGContainer.getElementsByTagName('text'), titleElements, svgData,
  246. // Copy style property to element from parents if it's not there.
  247. // Searches up hierarchy until it finds prop, or hits the chart
  248. // container.
  249. setStylePropertyFromParents = function (el, propName) {
  250. var curParent = el;
  251. while (curParent && curParent !== dummySVGContainer) {
  252. if (curParent.style[propName]) {
  253. el.style[propName] =
  254. curParent.style[propName];
  255. break;
  256. }
  257. curParent = curParent.parentNode;
  258. }
  259. };
  260. // Workaround for the text styling. Making sure it does pick up settings
  261. // for parent elements.
  262. [].forEach.call(textElements, function (el) {
  263. // Workaround for the text styling. making sure it does pick up the
  264. // root element
  265. ['font-family', 'font-size'].forEach(function (property) {
  266. setStylePropertyFromParents(el, property);
  267. });
  268. el.style['font-family'] = (el.style['font-family'] &&
  269. el.style['font-family'].split(' ').splice(-1));
  270. // Workaround for plotband with width, removing title from text
  271. // nodes
  272. titleElements = el.getElementsByTagName('title');
  273. [].forEach.call(titleElements, function (titleElement) {
  274. el.removeChild(titleElement);
  275. });
  276. });
  277. svgData = svgToPdf(dummySVGContainer.firstChild, 0);
  278. try {
  279. downloadURL(svgData, filename);
  280. if (successCallback) {
  281. successCallback();
  282. }
  283. }
  284. catch (e) {
  285. failCallback(e);
  286. }
  287. }
  288. /* eslint-enable valid-jsdoc */
  289. // Initiate download depending on file type
  290. if (imageType === 'image/svg+xml') {
  291. // SVG download. In this case, we want to use Microsoft specific Blob if
  292. // available
  293. try {
  294. if (typeof win.navigator.msSaveOrOpenBlob !== 'undefined') {
  295. blob = new MSBlobBuilder();
  296. blob.append(svg);
  297. svgurl = blob.getBlob('image/svg+xml');
  298. }
  299. else {
  300. svgurl = svgToDataUrl(svg);
  301. }
  302. downloadURL(svgurl, filename);
  303. if (successCallback) {
  304. successCallback();
  305. }
  306. }
  307. catch (e) {
  308. failCallback(e);
  309. }
  310. }
  311. else if (imageType === 'application/pdf') {
  312. if (win.jsPDF && win.svg2pdf) {
  313. downloadPDF();
  314. }
  315. else {
  316. // Must load pdf libraries first. // Don't destroy the object URL
  317. // yet since we are doing things asynchronously. A cleaner solution
  318. // would be nice, but this will do for now.
  319. objectURLRevoke = true;
  320. getScript(libURL + 'jspdf.js', function () {
  321. getScript(libURL + 'svg2pdf.js', function () {
  322. downloadPDF();
  323. });
  324. });
  325. }
  326. }
  327. else {
  328. // PNG/JPEG download - create bitmap from SVG
  329. svgurl = svgToDataUrl(svg);
  330. finallyHandler = function () {
  331. try {
  332. domurl.revokeObjectURL(svgurl);
  333. }
  334. catch (e) {
  335. // Ignore
  336. }
  337. };
  338. // First, try to get PNG by rendering on canvas
  339. imageToDataUrl(svgurl, imageType, {}, scale, function (imageURL) {
  340. // Success
  341. try {
  342. downloadURL(imageURL, filename);
  343. if (successCallback) {
  344. successCallback();
  345. }
  346. }
  347. catch (e) {
  348. failCallback(e);
  349. }
  350. }, function () {
  351. // Failed due to tainted canvas
  352. // Create new and untainted canvas
  353. var canvas = doc.createElement('canvas'), ctx = canvas.getContext('2d'), imageWidth = svg.match(/^<svg[^>]*width\s*=\s*\"?(\d+)\"?[^>]*>/)[1] * scale, imageHeight = svg.match(/^<svg[^>]*height\s*=\s*\"?(\d+)\"?[^>]*>/)[1] * scale, downloadWithCanVG = function () {
  354. ctx.drawSvg(svg, 0, 0, imageWidth, imageHeight);
  355. try {
  356. downloadURL(win.navigator.msSaveOrOpenBlob ?
  357. canvas.msToBlob() :
  358. canvas.toDataURL(imageType), filename);
  359. if (successCallback) {
  360. successCallback();
  361. }
  362. }
  363. catch (e) {
  364. failCallback(e);
  365. }
  366. finally {
  367. finallyHandler();
  368. }
  369. };
  370. canvas.width = imageWidth;
  371. canvas.height = imageHeight;
  372. if (win.canvg) {
  373. // Use preloaded canvg
  374. downloadWithCanVG();
  375. }
  376. else {
  377. // Must load canVG first. // Don't destroy the object URL
  378. // yet since we are doing things asynchronously. A cleaner
  379. // solution would be nice, but this will do for now.
  380. objectURLRevoke = true;
  381. // Get RGBColor.js first, then canvg
  382. getScript(libURL + 'rgbcolor.js', function () {
  383. getScript(libURL + 'canvg.js', function () {
  384. downloadWithCanVG();
  385. });
  386. });
  387. }
  388. },
  389. // No canvas support
  390. failCallback,
  391. // Failed to load image
  392. failCallback,
  393. // Finally
  394. function () {
  395. if (objectURLRevoke) {
  396. finallyHandler();
  397. }
  398. });
  399. }
  400. }
  401. /* eslint-disable valid-jsdoc */
  402. /**
  403. * Get SVG of chart prepared for client side export. This converts embedded
  404. * images in the SVG to data URIs. It requires the regular exporting module. The
  405. * options and chartOptions arguments are passed to the getSVGForExport
  406. * function.
  407. *
  408. * @private
  409. * @function Highcharts.Chart#getSVGForLocalExport
  410. * @param {Highcharts.ExportingOptions} options
  411. * @param {Highcharts.Options} chartOptions
  412. * @param {Function} failCallback
  413. * @param {Function} successCallback
  414. * @return {void}
  415. */
  416. Chart.prototype.getSVGForLocalExport = function (options, chartOptions, failCallback, successCallback) {
  417. var chart = this, images, imagesEmbedded = 0, chartCopyContainer, chartCopyOptions, el, i, l, href,
  418. // After grabbing the SVG of the chart's copy container we need to do
  419. // sanitation on the SVG
  420. sanitize = function (svg) {
  421. return chart.sanitizeSVG(svg, chartCopyOptions);
  422. },
  423. // When done with last image we have our SVG
  424. checkDone = function () {
  425. if (imagesEmbedded === images.length) {
  426. successCallback(sanitize(chartCopyContainer.innerHTML));
  427. }
  428. },
  429. // Success handler, we converted image to base64!
  430. embeddedSuccess = function (imageURL, imageType, callbackArgs) {
  431. ++imagesEmbedded;
  432. // Change image href in chart copy
  433. callbackArgs.imageElement.setAttributeNS('http://www.w3.org/1999/xlink', 'href', imageURL);
  434. checkDone();
  435. };
  436. // Hook into getSVG to get a copy of the chart copy's container (#8273)
  437. chart.unbindGetSVG = addEvent(chart, 'getSVG', function (e) {
  438. chartCopyOptions = e.chartCopy.options;
  439. chartCopyContainer = e.chartCopy.container.cloneNode(true);
  440. });
  441. // Trigger hook to get chart copy
  442. chart.getSVGForExport(options, chartOptions);
  443. images = chartCopyContainer.getElementsByTagName('image');
  444. try {
  445. // If there are no images to embed, the SVG is okay now.
  446. if (!images.length) {
  447. // Use SVG of chart copy
  448. successCallback(sanitize(chartCopyContainer.innerHTML));
  449. return;
  450. }
  451. // Go through the images we want to embed
  452. for (i = 0, l = images.length; i < l; ++i) {
  453. el = images[i];
  454. href = el.getAttributeNS('http://www.w3.org/1999/xlink', 'href');
  455. if (href) {
  456. imageToDataUrl(href, 'image/png', { imageElement: el }, options.scale, embeddedSuccess,
  457. // Tainted canvas
  458. failCallback,
  459. // No canvas support
  460. failCallback,
  461. // Failed to load source
  462. failCallback);
  463. // Hidden, boosted series have blank href (#10243)
  464. }
  465. else {
  466. ++imagesEmbedded;
  467. el.parentNode.removeChild(el);
  468. checkDone();
  469. }
  470. }
  471. }
  472. catch (e) {
  473. failCallback(e);
  474. }
  475. // Clean up
  476. chart.unbindGetSVG();
  477. };
  478. /* eslint-enable valid-jsdoc */
  479. /**
  480. * Exporting and offline-exporting modules required. Export a chart to an image
  481. * locally in the user's browser.
  482. *
  483. * @function Highcharts.Chart#exportChartLocal
  484. *
  485. * @param {Highcharts.ExportingOptions} [exportingOptions]
  486. * Exporting options, the same as in
  487. * {@link Highcharts.Chart#exportChart}.
  488. *
  489. * @param {Highcharts.Options} [chartOptions]
  490. * Additional chart options for the exported chart. For example a
  491. * different background color can be added here, or `dataLabels`
  492. * for export only.
  493. *
  494. * @return {void}
  495. *
  496. * @requires modules/exporting
  497. */
  498. Chart.prototype.exportChartLocal = function (exportingOptions, chartOptions) {
  499. var chart = this, options = merge(chart.options.exporting, exportingOptions), fallbackToExportServer = function (err) {
  500. if (options.fallbackToExportServer === false) {
  501. if (options.error) {
  502. options.error(options, err);
  503. }
  504. else {
  505. error(28, true); // Fallback disabled
  506. }
  507. }
  508. else {
  509. chart.exportChart(options);
  510. }
  511. }, svgSuccess = function (svg) {
  512. // If SVG contains foreignObjects all exports except SVG will fail,
  513. // as both CanVG and svg2pdf choke on this. Gracefully fall back.
  514. if (svg.indexOf('<foreignObject') > -1 &&
  515. options.type !== 'image/svg+xml') {
  516. fallbackToExportServer('Image type not supported' +
  517. 'for charts with embedded HTML');
  518. }
  519. else {
  520. downloadSVGLocal(svg, extend({ filename: chart.getFilename() }, options), fallbackToExportServer, function () { return fireEvent(chart, 'exportChartLocalSuccess'); });
  521. }
  522. },
  523. // Return true if the SVG contains images with external data. With the
  524. // boost module there are `image` elements with encoded PNGs, these are
  525. // supported by svg2pdf and should pass (#10243).
  526. hasExternalImages = function () {
  527. return [].some.call(chart.container.getElementsByTagName('image'), function (image) {
  528. var href = image.getAttribute('href');
  529. return href !== '' && href.indexOf('data:') !== 0;
  530. });
  531. };
  532. // If we are on IE and in styled mode, add a whitelist to the renderer for
  533. // inline styles that we want to pass through. There are so many styles by
  534. // default in IE that we don't want to blacklist them all.
  535. if (H.isMS && chart.styledMode) {
  536. SVGRenderer.prototype.inlineWhitelist = [
  537. /^blockSize/,
  538. /^border/,
  539. /^caretColor/,
  540. /^color/,
  541. /^columnRule/,
  542. /^columnRuleColor/,
  543. /^cssFloat/,
  544. /^cursor/,
  545. /^fill$/,
  546. /^fillOpacity/,
  547. /^font/,
  548. /^inlineSize/,
  549. /^length/,
  550. /^lineHeight/,
  551. /^opacity/,
  552. /^outline/,
  553. /^parentRule/,
  554. /^rx$/,
  555. /^ry$/,
  556. /^stroke/,
  557. /^textAlign/,
  558. /^textAnchor/,
  559. /^textDecoration/,
  560. /^transform/,
  561. /^vectorEffect/,
  562. /^visibility/,
  563. /^x$/,
  564. /^y$/
  565. ];
  566. }
  567. // Always fall back on:
  568. // - MS browsers: Embedded images JPEG/PNG, or any PDF
  569. // - Embedded images and PDF
  570. if ((H.isMS &&
  571. (options.type === 'application/pdf' ||
  572. chart.container.getElementsByTagName('image').length &&
  573. options.type !== 'image/svg+xml')) || (options.type === 'application/pdf' &&
  574. hasExternalImages())) {
  575. fallbackToExportServer('Image type not supported for this chart/browser.');
  576. return;
  577. }
  578. chart.getSVGForLocalExport(options, chartOptions, fallbackToExportServer, svgSuccess);
  579. };
  580. // Extend the default options to use the local exporter logic
  581. merge(true, getOptions().exporting, {
  582. libURL: 'https://code.highcharts.com/9.0.1/lib/',
  583. // When offline-exporting is loaded, redefine the menu item definitions
  584. // related to download.
  585. menuItemDefinitions: {
  586. downloadPNG: {
  587. textKey: 'downloadPNG',
  588. onclick: function () {
  589. this.exportChartLocal();
  590. }
  591. },
  592. downloadJPEG: {
  593. textKey: 'downloadJPEG',
  594. onclick: function () {
  595. this.exportChartLocal({
  596. type: 'image/jpeg'
  597. });
  598. }
  599. },
  600. downloadSVG: {
  601. textKey: 'downloadSVG',
  602. onclick: function () {
  603. this.exportChartLocal({
  604. type: 'image/svg+xml'
  605. });
  606. }
  607. },
  608. downloadPDF: {
  609. textKey: 'downloadPDF',
  610. onclick: function () {
  611. this.exportChartLocal({
  612. type: 'application/pdf'
  613. });
  614. }
  615. }
  616. }
  617. });
  618. // Compatibility
  619. H.downloadSVGLocal = downloadSVGLocal;