ExportData.js 36 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006
  1. /* *
  2. *
  3. * Experimental data export module for Highcharts
  4. *
  5. * (c) 2010-2021 Torstein Honsi
  6. *
  7. * License: www.highcharts.com/license
  8. *
  9. * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
  10. *
  11. * */
  12. // @todo
  13. // - Set up systematic tests for all series types, paired with tests of the data
  14. // module importing the same data.
  15. 'use strict';
  16. import Axis from '../Core/Axis/Axis.js';
  17. import Chart from '../Core/Chart/Chart.js';
  18. import AST from '../Core/Renderer/HTML/AST.js';
  19. import H from '../Core/Globals.js';
  20. var doc = H.doc, seriesTypes = H.seriesTypes, win = H.win;
  21. import O from '../Core/Options.js';
  22. var getOptions = O.getOptions, setOptions = O.setOptions;
  23. import U from '../Core/Utilities.js';
  24. var addEvent = U.addEvent, defined = U.defined, extend = U.extend, find = U.find, fireEvent = U.fireEvent, isNumber = U.isNumber, pick = U.pick;
  25. /**
  26. * Function callback to execute while data rows are processed for exporting.
  27. * This allows the modification of data rows before processed into the final
  28. * format.
  29. *
  30. * @callback Highcharts.ExportDataCallbackFunction
  31. * @extends Highcharts.EventCallbackFunction<Highcharts.Chart>
  32. *
  33. * @param {Highcharts.Chart} this
  34. * Chart context where the event occured.
  35. *
  36. * @param {Highcharts.ExportDataEventObject} event
  37. * Event object with data rows that can be modified.
  38. */
  39. /**
  40. * Contains information about the export data event.
  41. *
  42. * @interface Highcharts.ExportDataEventObject
  43. */ /**
  44. * Contains the data rows for the current export task and can be modified.
  45. * @name Highcharts.ExportDataEventObject#dataRows
  46. * @type {Array<Array<string>>}
  47. */
  48. import DownloadURL from '../Extensions/DownloadURL.js';
  49. var downloadURL = DownloadURL.downloadURL;
  50. // Can we add this to utils? Also used in screen-reader.js
  51. /**
  52. * HTML encode some characters vulnerable for XSS.
  53. * @private
  54. * @param {string} html The input string
  55. * @return {string} The excaped string
  56. */
  57. function htmlencode(html) {
  58. return html
  59. .replace(/&/g, '&amp;')
  60. .replace(/</g, '&lt;')
  61. .replace(/>/g, '&gt;')
  62. .replace(/"/g, '&quot;')
  63. .replace(/'/g, '&#x27;')
  64. .replace(/\//g, '&#x2F;');
  65. }
  66. setOptions({
  67. /**
  68. * Callback that fires while exporting data. This allows the modification of
  69. * data rows before processed into the final format.
  70. *
  71. * @type {Highcharts.ExportDataCallbackFunction}
  72. * @context Highcharts.Chart
  73. * @requires modules/export-data
  74. * @apioption chart.events.exportData
  75. */
  76. /**
  77. * When set to `false` will prevent the series data from being included in
  78. * any form of data export.
  79. *
  80. * Since version 6.0.0 until 7.1.0 the option was existing undocumented
  81. * as `includeInCSVExport`.
  82. *
  83. * @type {boolean}
  84. * @since 7.1.0
  85. * @requires modules/export-data
  86. * @apioption plotOptions.series.includeInDataExport
  87. */
  88. /**
  89. * @optionparent exporting
  90. * @private
  91. */
  92. exporting: {
  93. /**
  94. * Caption for the data table. Same as chart title by default. Set to
  95. * `false` to disable.
  96. *
  97. * @sample highcharts/export-data/multilevel-table
  98. * Multiple table headers
  99. *
  100. * @type {boolean|string}
  101. * @since 6.0.4
  102. * @requires modules/export-data
  103. * @apioption exporting.tableCaption
  104. */
  105. /**
  106. * Options for exporting data to CSV or ExCel, or displaying the data
  107. * in a HTML table or a JavaScript structure.
  108. *
  109. * This module adds data export options to the export menu and provides
  110. * functions like `Chart.getCSV`, `Chart.getTable`, `Chart.getDataRows`
  111. * and `Chart.viewData`.
  112. *
  113. * The XLS converter is limited and only creates a HTML string that is
  114. * passed for download, which works but creates a warning before
  115. * opening. The workaround for this is to use a third party XLSX
  116. * converter, as demonstrated in the sample below.
  117. *
  118. * @sample highcharts/export-data/categorized/ Categorized data
  119. * @sample highcharts/export-data/stock-timeaxis/ Highcharts Stock time axis
  120. * @sample highcharts/export-data/xlsx/
  121. * Using a third party XLSX converter
  122. *
  123. * @since 6.0.0
  124. * @requires modules/export-data
  125. */
  126. csv: {
  127. /**
  128. *
  129. * Options for annotations in the export-data table.
  130. *
  131. * @since 8.2.0
  132. * @requires modules/export-data
  133. * @requires modules/annotations
  134. *
  135. *
  136. */
  137. annotations: {
  138. /**
  139. * The way to mark the separator for annotations
  140. * combined in one export-data table cell.
  141. *
  142. * @since 8.2.0
  143. * @requires modules/annotations
  144. */
  145. itemDelimiter: '; ',
  146. /**
  147. * When several labels are assigned to a specific point,
  148. * they will be displayed in one field in the table.
  149. *
  150. * @sample highcharts/export-data/join-annotations/
  151. * Concatenate point annotations with itemDelimiter set.
  152. *
  153. * @since 8.2.0
  154. * @requires modules/annotations
  155. */
  156. join: false
  157. },
  158. /**
  159. * Formatter callback for the column headers. Parameters are:
  160. * - `item` - The series or axis object)
  161. * - `key` - The point key, for example y or z
  162. * - `keyLength` - The amount of value keys for this item, for
  163. * example a range series has the keys `low` and `high` so the
  164. * key length is 2.
  165. *
  166. * If [useMultiLevelHeaders](#exporting.useMultiLevelHeaders) is
  167. * true, columnHeaderFormatter by default returns an object with
  168. * columnTitle and topLevelColumnTitle for each key. Columns with
  169. * the same topLevelColumnTitle have their titles merged into a
  170. * single cell with colspan for table/Excel export.
  171. *
  172. * If `useMultiLevelHeaders` is false, or for CSV export, it returns
  173. * the series name, followed by the key if there is more than one
  174. * key.
  175. *
  176. * For the axis it returns the axis title or "Category" or
  177. * "DateTime" by default.
  178. *
  179. * Return `false` to use Highcharts' proposed header.
  180. *
  181. * @sample highcharts/export-data/multilevel-table
  182. * Multiple table headers
  183. *
  184. * @type {Function|null}
  185. */
  186. columnHeaderFormatter: null,
  187. /**
  188. * Which date format to use for exported dates on a datetime X axis.
  189. * See `Highcharts.dateFormat`.
  190. */
  191. dateFormat: '%Y-%m-%d %H:%M:%S',
  192. /**
  193. * Which decimal point to use for exported CSV. Defaults to the same
  194. * as the browser locale, typically `.` (English) or `,` (German,
  195. * French etc).
  196. *
  197. * @type {string|null}
  198. * @since 6.0.4
  199. */
  200. decimalPoint: null,
  201. /**
  202. * The item delimiter in the exported data. Use `;` for direct
  203. * exporting to Excel. Defaults to a best guess based on the browser
  204. * locale. If the locale _decimal point_ is `,`, the `itemDelimiter`
  205. * defaults to `;`, otherwise the `itemDelimiter` defaults to `,`.
  206. *
  207. * @type {string|null}
  208. */
  209. itemDelimiter: null,
  210. /**
  211. * The line delimiter in the exported data, defaults to a newline.
  212. */
  213. lineDelimiter: '\n'
  214. },
  215. /**
  216. * Show a HTML table below the chart with the chart's current data.
  217. *
  218. * @sample highcharts/export-data/showtable/
  219. * Show the table
  220. * @sample highcharts/studies/exporting-table-html
  221. * Experiment with putting the table inside the subtitle to
  222. * allow exporting it.
  223. *
  224. * @since 6.0.0
  225. * @requires modules/export-data
  226. */
  227. showTable: false,
  228. /**
  229. * Use multi level headers in data table. If [csv.columnHeaderFormatter
  230. * ](#exporting.csv.columnHeaderFormatter) is defined, it has to return
  231. * objects in order for multi level headers to work.
  232. *
  233. * @sample highcharts/export-data/multilevel-table
  234. * Multiple table headers
  235. *
  236. * @since 6.0.4
  237. * @requires modules/export-data
  238. */
  239. useMultiLevelHeaders: true,
  240. /**
  241. * If using multi level table headers, use rowspans for headers that
  242. * have only one level.
  243. *
  244. * @sample highcharts/export-data/multilevel-table
  245. * Multiple table headers
  246. *
  247. * @since 6.0.4
  248. * @requires modules/export-data
  249. */
  250. useRowspanHeaders: true
  251. },
  252. /**
  253. * @optionparent lang
  254. *
  255. * @private
  256. */
  257. lang: {
  258. /**
  259. * The text for the menu item.
  260. *
  261. * @since 6.0.0
  262. * @requires modules/export-data
  263. */
  264. downloadCSV: 'Download CSV',
  265. /**
  266. * The text for the menu item.
  267. *
  268. * @since 6.0.0
  269. * @requires modules/export-data
  270. */
  271. downloadXLS: 'Download XLS',
  272. /**
  273. * The text for exported table.
  274. *
  275. * @since 8.1.0
  276. * @requires modules/export-data
  277. */
  278. exportData: {
  279. /**
  280. * The annotation column title.
  281. */
  282. annotationHeader: 'Annotations',
  283. /**
  284. * The category column title.
  285. */
  286. categoryHeader: 'Category',
  287. /**
  288. * The category column title when axis type set to "datetime".
  289. */
  290. categoryDatetimeHeader: 'DateTime'
  291. },
  292. /**
  293. * The text for the menu item.
  294. *
  295. * @since 6.0.0
  296. * @requires modules/export-data
  297. */
  298. viewData: 'View data table',
  299. /**
  300. * The text for the menu item.
  301. *
  302. * @since 8.2.0
  303. * @requires modules/export-data
  304. */
  305. hideData: 'Hide data table'
  306. }
  307. });
  308. /* eslint-disable no-invalid-this */
  309. // Add an event listener to handle the showTable option
  310. addEvent(Chart, 'render', function () {
  311. if (this.options &&
  312. this.options.exporting &&
  313. this.options.exporting.showTable &&
  314. !this.options.chart.forExport &&
  315. !this.dataTableDiv) {
  316. this.viewData();
  317. }
  318. });
  319. /* eslint-enable no-invalid-this */
  320. /**
  321. * Set up key-to-axis bindings. This is used when the Y axis is datetime or
  322. * categorized. For example in an arearange series, the low and high values
  323. * should be formatted according to the Y axis type, and in order to link them
  324. * we need this map.
  325. *
  326. * @private
  327. * @function Highcharts.Chart#setUpKeyToAxis
  328. */
  329. Chart.prototype.setUpKeyToAxis = function () {
  330. if (seriesTypes.arearange) {
  331. seriesTypes.arearange.prototype.keyToAxis = {
  332. low: 'y',
  333. high: 'y'
  334. };
  335. }
  336. if (seriesTypes.gantt) {
  337. seriesTypes.gantt.prototype.keyToAxis = {
  338. start: 'x',
  339. end: 'x'
  340. };
  341. }
  342. };
  343. /**
  344. * Export-data module required. Returns a two-dimensional array containing the
  345. * current chart data.
  346. *
  347. * @function Highcharts.Chart#getDataRows
  348. *
  349. * @param {boolean} [multiLevelHeaders]
  350. * Use multilevel headers for the rows by default. Adds an extra row with
  351. * top level headers. If a custom columnHeaderFormatter is defined, this
  352. * can override the behavior.
  353. *
  354. * @return {Array<Array<(number|string)>>}
  355. * The current chart data
  356. *
  357. * @fires Highcharts.Chart#event:exportData
  358. */
  359. Chart.prototype.getDataRows = function (multiLevelHeaders) {
  360. var hasParallelCoords = this.hasParallelCoordinates, time = this.time, csvOptions = ((this.options.exporting && this.options.exporting.csv) || {}), xAxis, xAxes = this.xAxis, rows = {}, rowArr = [], dataRows, topLevelColumnTitles = [], columnTitles = [], columnTitleObj, i, x, xTitle, langOptions = this.options.lang, exportDataOptions = langOptions.exportData, categoryHeader = exportDataOptions.categoryHeader, categoryDatetimeHeader = exportDataOptions.categoryDatetimeHeader,
  361. // Options
  362. columnHeaderFormatter = function (item, key, keyLength) {
  363. if (csvOptions.columnHeaderFormatter) {
  364. var s = csvOptions.columnHeaderFormatter(item, key, keyLength);
  365. if (s !== false) {
  366. return s;
  367. }
  368. }
  369. if (!item) {
  370. return categoryHeader;
  371. }
  372. if (item instanceof Axis) {
  373. return (item.options.title && item.options.title.text) ||
  374. (item.dateTime ? categoryDatetimeHeader : categoryHeader);
  375. }
  376. if (multiLevelHeaders) {
  377. return {
  378. columnTitle: keyLength > 1 ?
  379. key :
  380. item.name,
  381. topLevelColumnTitle: item.name
  382. };
  383. }
  384. return item.name + (keyLength > 1 ? ' (' + key + ')' : '');
  385. },
  386. // Map the categories for value axes
  387. getCategoryAndDateTimeMap = function (series, pointArrayMap, pIdx) {
  388. var categoryMap = {}, dateTimeValueAxisMap = {};
  389. pointArrayMap.forEach(function (prop) {
  390. var axisName = ((series.keyToAxis && series.keyToAxis[prop]) ||
  391. prop) + 'Axis',
  392. // Points in parallel coordinates refers to all yAxis
  393. // not only `series.yAxis`
  394. axis = isNumber(pIdx) ?
  395. series.chart[axisName][pIdx] :
  396. series[axisName];
  397. categoryMap[prop] = (axis && axis.categories) || [];
  398. dateTimeValueAxisMap[prop] = (axis && axis.dateTime);
  399. });
  400. return {
  401. categoryMap: categoryMap,
  402. dateTimeValueAxisMap: dateTimeValueAxisMap
  403. };
  404. },
  405. // Create point array depends if xAxis is category
  406. // or point.name is defined #13293
  407. getPointArray = function (series, xAxis) {
  408. var namedPoints = series.data.filter(function (d) {
  409. return (typeof d.y !== 'undefined') && d.name;
  410. });
  411. if (namedPoints.length &&
  412. xAxis &&
  413. !xAxis.categories &&
  414. !series.keyToAxis) {
  415. if (series.pointArrayMap) {
  416. var pointArrayMapCheck = series.pointArrayMap.filter(function (p) { return p === 'x'; });
  417. if (pointArrayMapCheck.length) {
  418. series.pointArrayMap.unshift('x');
  419. return series.pointArrayMap;
  420. }
  421. }
  422. return ['x', 'y'];
  423. }
  424. return series.pointArrayMap || ['y'];
  425. }, xAxisIndices = [];
  426. // Loop the series and index values
  427. i = 0;
  428. this.setUpKeyToAxis();
  429. this.series.forEach(function (series) {
  430. var keys = series.options.keys, xAxis = series.xAxis, pointArrayMap = keys || getPointArray(series, xAxis), valueCount = pointArrayMap.length, xTaken = !series.requireSorting && {}, xAxisIndex = xAxes.indexOf(xAxis), categoryAndDatetimeMap = getCategoryAndDateTimeMap(series, pointArrayMap), mockSeries, j;
  431. if (series.options.includeInDataExport !== false &&
  432. !series.options.isInternal &&
  433. series.visible !== false // #55
  434. ) {
  435. // Build a lookup for X axis index and the position of the first
  436. // series that belongs to that X axis. Includes -1 for non-axis
  437. // series types like pies.
  438. if (!find(xAxisIndices, function (index) {
  439. return index[0] === xAxisIndex;
  440. })) {
  441. xAxisIndices.push([xAxisIndex, i]);
  442. }
  443. // Compute the column headers and top level headers, usually the
  444. // same as series names
  445. j = 0;
  446. while (j < valueCount) {
  447. columnTitleObj = columnHeaderFormatter(series, pointArrayMap[j], pointArrayMap.length);
  448. columnTitles.push(columnTitleObj.columnTitle || columnTitleObj);
  449. if (multiLevelHeaders) {
  450. topLevelColumnTitles.push(columnTitleObj.topLevelColumnTitle ||
  451. columnTitleObj);
  452. }
  453. j++;
  454. }
  455. mockSeries = {
  456. chart: series.chart,
  457. autoIncrement: series.autoIncrement,
  458. options: series.options,
  459. pointArrayMap: series.pointArrayMap
  460. };
  461. // Export directly from options.data because we need the uncropped
  462. // data (#7913), and we need to support Boost (#7026).
  463. series.options.data.forEach(function eachData(options, pIdx) {
  464. var key, prop, val, name, point;
  465. // In parallel coordinates chart, each data point is connected
  466. // to a separate yAxis, conform this
  467. if (hasParallelCoords) {
  468. categoryAndDatetimeMap = getCategoryAndDateTimeMap(series, pointArrayMap, pIdx);
  469. }
  470. point = { series: mockSeries };
  471. series.pointClass.prototype.applyOptions.apply(point, [options]);
  472. key = point.x;
  473. name = series.data[pIdx] && series.data[pIdx].name;
  474. j = 0;
  475. // Pies, funnels, geo maps etc. use point name in X row
  476. if (!xAxis ||
  477. series.exportKey === 'name' ||
  478. (!hasParallelCoords && xAxis && xAxis.hasNames) && name) {
  479. key = name;
  480. }
  481. if (xTaken) {
  482. if (xTaken[key]) {
  483. key += '|' + pIdx;
  484. }
  485. xTaken[key] = true;
  486. }
  487. if (!rows[key]) {
  488. // Generate the row
  489. rows[key] = [];
  490. // Contain the X values from one or more X axes
  491. rows[key].xValues = [];
  492. }
  493. rows[key].x = point.x;
  494. rows[key].name = name;
  495. rows[key].xValues[xAxisIndex] = point.x;
  496. while (j < valueCount) {
  497. prop = pointArrayMap[j]; // y, z etc
  498. val = point[prop];
  499. rows[key][i + j] = pick(
  500. // Y axis category if present
  501. categoryAndDatetimeMap.categoryMap[prop][val],
  502. // datetime yAxis
  503. categoryAndDatetimeMap.dateTimeValueAxisMap[prop] ?
  504. time.dateFormat(csvOptions.dateFormat, val) :
  505. null,
  506. // linear/log yAxis
  507. val);
  508. j++;
  509. }
  510. });
  511. i = i + j;
  512. }
  513. });
  514. // Make a sortable array
  515. for (x in rows) {
  516. if (Object.hasOwnProperty.call(rows, x)) {
  517. rowArr.push(rows[x]);
  518. }
  519. }
  520. var xAxisIndex, column;
  521. // Add computed column headers and top level headers to final row set
  522. dataRows = multiLevelHeaders ? [topLevelColumnTitles, columnTitles] :
  523. [columnTitles];
  524. i = xAxisIndices.length;
  525. while (i--) { // Start from end to splice in
  526. xAxisIndex = xAxisIndices[i][0];
  527. column = xAxisIndices[i][1];
  528. xAxis = xAxes[xAxisIndex];
  529. // Sort it by X values
  530. rowArr.sort(function (// eslint-disable-line no-loop-func
  531. a, b) {
  532. return a.xValues[xAxisIndex] - b.xValues[xAxisIndex];
  533. });
  534. // Add header row
  535. xTitle = columnHeaderFormatter(xAxis);
  536. dataRows[0].splice(column, 0, xTitle);
  537. if (multiLevelHeaders && dataRows[1]) {
  538. // If using multi level headers, we just added top level header.
  539. // Also add for sub level
  540. dataRows[1].splice(column, 0, xTitle);
  541. }
  542. // Add the category column
  543. rowArr.forEach(function (// eslint-disable-line no-loop-func
  544. row) {
  545. var category = row.name;
  546. if (xAxis && !defined(category)) {
  547. if (xAxis.dateTime) {
  548. if (row.x instanceof Date) {
  549. row.x = row.x.getTime();
  550. }
  551. category = time.dateFormat(csvOptions.dateFormat, row.x);
  552. }
  553. else if (xAxis.categories) {
  554. category = pick(xAxis.names[row.x], xAxis.categories[row.x], row.x);
  555. }
  556. else {
  557. category = row.x;
  558. }
  559. }
  560. // Add the X/date/category
  561. row.splice(column, 0, category);
  562. });
  563. }
  564. dataRows = dataRows.concat(rowArr);
  565. fireEvent(this, 'exportData', { dataRows: dataRows });
  566. return dataRows;
  567. };
  568. /**
  569. * Export-data module required. Returns the current chart data as a CSV string.
  570. *
  571. * @function Highcharts.Chart#getCSV
  572. *
  573. * @param {boolean} [useLocalDecimalPoint]
  574. * Whether to use the local decimal point as detected from the browser.
  575. * This makes it easier to export data to Excel in the same locale as the
  576. * user is.
  577. *
  578. * @return {string}
  579. * CSV representation of the data
  580. */
  581. Chart.prototype.getCSV = function (useLocalDecimalPoint) {
  582. var csv = '', rows = this.getDataRows(), csvOptions = this.options.exporting.csv, decimalPoint = pick(csvOptions.decimalPoint, csvOptions.itemDelimiter !== ',' && useLocalDecimalPoint ?
  583. (1.1).toLocaleString()[1] :
  584. '.'),
  585. // use ';' for direct to Excel
  586. itemDelimiter = pick(csvOptions.itemDelimiter, decimalPoint === ',' ? ';' : ','),
  587. // '\n' isn't working with the js csv data extraction
  588. lineDelimiter = csvOptions.lineDelimiter;
  589. // Transform the rows to CSV
  590. rows.forEach(function (row, i) {
  591. var val = '', j = row.length;
  592. while (j--) {
  593. val = row[j];
  594. if (typeof val === 'string') {
  595. val = '"' + val + '"';
  596. }
  597. if (typeof val === 'number') {
  598. if (decimalPoint !== '.') {
  599. val = val.toString().replace('.', decimalPoint);
  600. }
  601. }
  602. row[j] = val;
  603. }
  604. // Add the values
  605. csv += row.join(itemDelimiter);
  606. // Add the line delimiter
  607. if (i < rows.length - 1) {
  608. csv += lineDelimiter;
  609. }
  610. });
  611. return csv;
  612. };
  613. /**
  614. * Export-data module required. Build a HTML table with the chart's current
  615. * data.
  616. *
  617. * @sample highcharts/export-data/viewdata/
  618. * View the data from the export menu
  619. *
  620. * @function Highcharts.Chart#getTable
  621. *
  622. * @param {boolean} [useLocalDecimalPoint]
  623. * Whether to use the local decimal point as detected from the browser.
  624. * This makes it easier to export data to Excel in the same locale as the
  625. * user is.
  626. *
  627. * @return {string}
  628. * HTML representation of the data.
  629. *
  630. * @fires Highcharts.Chart#event:afterGetTable
  631. */
  632. Chart.prototype.getTable = function (useLocalDecimalPoint) {
  633. var serialize = function (node) {
  634. if (!node.tagName || node.tagName === '#text') {
  635. // Text node
  636. return node.textContent || '';
  637. }
  638. var attributes = node.attributes;
  639. var html = "<" + node.tagName;
  640. if (attributes) {
  641. Object.keys(attributes).forEach(function (key) {
  642. var value = attributes[key];
  643. html += " " + key + "=\"" + value + "\"";
  644. });
  645. }
  646. html += '>';
  647. html += node.textContent || '';
  648. (node.children || []).forEach(function (child) {
  649. html += serialize(child);
  650. });
  651. html += "</" + node.tagName + ">";
  652. return html;
  653. };
  654. var tree = this.getTableAST(useLocalDecimalPoint);
  655. return serialize(tree);
  656. };
  657. /**
  658. * Get the AST of a HTML table representing the chart data.
  659. *
  660. * @private
  661. *
  662. * @function Highcharts.Chart#getTableAST
  663. *
  664. * @param {boolean} [useLocalDecimalPoint]
  665. * Whether to use the local decimal point as detected from the browser.
  666. * This makes it easier to export data to Excel in the same locale as the
  667. * user is.
  668. *
  669. * @return {Highcharts.ASTNode}
  670. * The abstract syntax tree
  671. */
  672. Chart.prototype.getTableAST = function (useLocalDecimalPoint) {
  673. var treeChildren = [];
  674. var options = this.options, decimalPoint = useLocalDecimalPoint ? (1.1).toLocaleString()[1] : '.', useMultiLevelHeaders = pick(options.exporting.useMultiLevelHeaders, true), rows = this.getDataRows(useMultiLevelHeaders), rowLength = 0, topHeaders = useMultiLevelHeaders ? rows.shift() : null, subHeaders = rows.shift(),
  675. // Compare two rows for equality
  676. isRowEqual = function (row1, row2) {
  677. var i = row1.length;
  678. if (row2.length === i) {
  679. while (i--) {
  680. if (row1[i] !== row2[i]) {
  681. return false;
  682. }
  683. }
  684. }
  685. else {
  686. return false;
  687. }
  688. return true;
  689. },
  690. // Get table cell HTML from value
  691. getCellHTMLFromValue = function (tagName, classes, attributes, value) {
  692. var textContent = pick(value, ''), className = 'text' + (classes ? ' ' + classes : '');
  693. // Convert to string if number
  694. if (typeof textContent === 'number') {
  695. textContent = textContent.toString();
  696. if (decimalPoint === ',') {
  697. textContent = textContent.replace('.', decimalPoint);
  698. }
  699. className = 'number';
  700. }
  701. else if (!value) {
  702. className = 'empty';
  703. }
  704. attributes = extend({ 'class': className }, attributes);
  705. return {
  706. tagName: tagName,
  707. attributes: attributes,
  708. textContent: textContent
  709. };
  710. },
  711. // Get table header markup from row data
  712. getTableHeaderHTML = function (topheaders, subheaders, rowLength) {
  713. var theadChildren = [];
  714. var i = 0, len = rowLength || subheaders && subheaders.length, next, cur, curColspan = 0, rowspan;
  715. // Clean up multiple table headers. Chart.getDataRows() returns two
  716. // levels of headers when using multilevel, not merged. We need to
  717. // merge identical headers, remove redundant headers, and keep it
  718. // all marked up nicely.
  719. if (useMultiLevelHeaders &&
  720. topheaders &&
  721. subheaders &&
  722. !isRowEqual(topheaders, subheaders)) {
  723. var trChildren = [];
  724. for (; i < len; ++i) {
  725. cur = topheaders[i];
  726. next = topheaders[i + 1];
  727. if (cur === next) {
  728. ++curColspan;
  729. }
  730. else if (curColspan) {
  731. // Ended colspan
  732. // Add cur to HTML with colspan.
  733. trChildren.push(getCellHTMLFromValue('th', 'highcharts-table-topheading', {
  734. scope: 'col',
  735. colspan: curColspan + 1
  736. }, cur));
  737. curColspan = 0;
  738. }
  739. else {
  740. // Cur is standalone. If it is same as sublevel,
  741. // remove sublevel and add just toplevel.
  742. if (cur === subheaders[i]) {
  743. if (options.exporting.useRowspanHeaders) {
  744. rowspan = 2;
  745. delete subheaders[i];
  746. }
  747. else {
  748. rowspan = 1;
  749. subheaders[i] = '';
  750. }
  751. }
  752. else {
  753. rowspan = 1;
  754. }
  755. var cell = getCellHTMLFromValue('th', 'highcharts-table-topheading', { scope: 'col' }, cur);
  756. if (rowspan > 1 && cell.attributes) {
  757. cell.attributes.valign = 'top';
  758. cell.attributes.rowspan = rowspan;
  759. }
  760. trChildren.push(cell);
  761. }
  762. }
  763. theadChildren.push({
  764. tagName: 'tr',
  765. children: trChildren
  766. });
  767. }
  768. // Add the subheaders (the only headers if not using multilevels)
  769. if (subheaders) {
  770. var trChildren = [];
  771. for (i = 0, len = subheaders.length; i < len; ++i) {
  772. if (typeof subheaders[i] !== 'undefined') {
  773. trChildren.push(getCellHTMLFromValue('th', null, { scope: 'col' }, subheaders[i]));
  774. }
  775. }
  776. theadChildren.push({
  777. tagName: 'tr',
  778. children: trChildren
  779. });
  780. }
  781. return {
  782. tagName: 'thead',
  783. children: theadChildren
  784. };
  785. };
  786. // Add table caption
  787. if (options.exporting.tableCaption !== false) {
  788. treeChildren.push({
  789. tagName: 'caption',
  790. attributes: {
  791. 'class': 'highcharts-table-caption'
  792. },
  793. textContent: pick(options.exporting.tableCaption, (options.title.text ?
  794. htmlencode(options.title.text) :
  795. 'Chart'))
  796. });
  797. }
  798. // Find longest row
  799. for (var i = 0, len = rows.length; i < len; ++i) {
  800. if (rows[i].length > rowLength) {
  801. rowLength = rows[i].length;
  802. }
  803. }
  804. // Add header
  805. treeChildren.push(getTableHeaderHTML(topHeaders, subHeaders, Math.max(rowLength, subHeaders.length)));
  806. // Transform the rows to HTML
  807. var trs = [];
  808. rows.forEach(function (row) {
  809. var trChildren = [];
  810. for (var j = 0; j < rowLength; j++) {
  811. // Make first column a header too. Especially important for
  812. // category axes, but also might make sense for datetime? Should
  813. // await user feedback on this.
  814. trChildren.push(getCellHTMLFromValue(j ? 'td' : 'th', null, j ? {} : { scope: 'row' }, row[j]));
  815. }
  816. trs.push({
  817. tagName: 'tr',
  818. children: trChildren
  819. });
  820. });
  821. treeChildren.push({
  822. tagName: 'tbody',
  823. children: trs
  824. });
  825. var e = {
  826. tree: {
  827. tagName: 'table',
  828. id: "highcharts-data-table-" + this.index,
  829. children: treeChildren
  830. }
  831. };
  832. fireEvent(this, 'aftergetTableAST', e);
  833. return e.tree;
  834. };
  835. /**
  836. * Get a blob object from content, if blob is supported
  837. *
  838. * @private
  839. * @param {string} content
  840. * The content to create the blob from.
  841. * @param {string} type
  842. * The type of the content.
  843. * @return {string|undefined}
  844. * The blob object, or undefined if not supported.
  845. */
  846. function getBlobFromContent(content, type) {
  847. var nav = win.navigator, webKit = (nav.userAgent.indexOf('WebKit') > -1 &&
  848. nav.userAgent.indexOf('Chrome') < 0), domurl = win.URL || win.webkitURL || win;
  849. try {
  850. // MS specific
  851. if (nav.msSaveOrOpenBlob && win.MSBlobBuilder) {
  852. var blob = new win.MSBlobBuilder();
  853. blob.append(content);
  854. return blob.getBlob('image/svg+xml');
  855. }
  856. // Safari requires data URI since it doesn't allow navigation to blob
  857. // URLs.
  858. if (!webKit) {
  859. return domurl.createObjectURL(new win.Blob(['\uFEFF' + content], // #7084
  860. { type: type }));
  861. }
  862. }
  863. catch (e) {
  864. // Ignore
  865. }
  866. }
  867. /**
  868. * Generates a data URL of CSV for local download in the browser. This is the
  869. * default action for a click on the 'Download CSV' button.
  870. *
  871. * See {@link Highcharts.Chart#getCSV} to get the CSV data itself.
  872. *
  873. * @function Highcharts.Chart#downloadCSV
  874. *
  875. * @requires modules/exporting
  876. */
  877. Chart.prototype.downloadCSV = function () {
  878. var csv = this.getCSV(true);
  879. downloadURL(getBlobFromContent(csv, 'text/csv') ||
  880. 'data:text/csv,\uFEFF' + encodeURIComponent(csv), this.getFilename() + '.csv');
  881. };
  882. /**
  883. * Generates a data URL of an XLS document for local download in the browser.
  884. * This is the default action for a click on the 'Download XLS' button.
  885. *
  886. * See {@link Highcharts.Chart#getTable} to get the table data itself.
  887. *
  888. * @function Highcharts.Chart#downloadXLS
  889. *
  890. * @requires modules/exporting
  891. */
  892. Chart.prototype.downloadXLS = function () {
  893. var uri = 'data:application/vnd.ms-excel;base64,', template = '<html xmlns:o="urn:schemas-microsoft-com:office:office" ' +
  894. 'xmlns:x="urn:schemas-microsoft-com:office:excel" ' +
  895. 'xmlns="http://www.w3.org/TR/REC-html40">' +
  896. '<head><!--[if gte mso 9]><xml><x:ExcelWorkbook>' +
  897. '<x:ExcelWorksheets><x:ExcelWorksheet>' +
  898. '<x:Name>Ark1</x:Name>' +
  899. '<x:WorksheetOptions><x:DisplayGridlines/></x:WorksheetOptions>' +
  900. '</x:ExcelWorksheet></x:ExcelWorksheets></x:ExcelWorkbook>' +
  901. '</xml><![endif]-->' +
  902. '<style>td{border:none;font-family: Calibri, sans-serif;} ' +
  903. '.number{mso-number-format:"0.00";} ' +
  904. '.text{ mso-number-format:"\@";}</style>' +
  905. '<meta name=ProgId content=Excel.Sheet>' +
  906. '<meta charset=UTF-8>' +
  907. '</head><body>' +
  908. this.getTable(true) +
  909. '</body></html>', base64 = function (s) {
  910. return win.btoa(unescape(encodeURIComponent(s))); // #50
  911. };
  912. downloadURL(getBlobFromContent(template, 'application/vnd.ms-excel') ||
  913. uri + base64(template), this.getFilename() + '.xls');
  914. };
  915. /**
  916. * Export-data module required. View the data in a table below the chart.
  917. *
  918. * @function Highcharts.Chart#viewData
  919. *
  920. * @fires Highcharts.Chart#event:afterViewData
  921. */
  922. Chart.prototype.viewData = function () {
  923. this.toggleDataTable(true);
  924. };
  925. /**
  926. * Export-data module required. Hide the data table when visible.
  927. *
  928. * @function Highcharts.Chart#hideData
  929. */
  930. Chart.prototype.hideData = function () {
  931. this.toggleDataTable(false);
  932. };
  933. Chart.prototype.toggleDataTable = function (show) {
  934. show = pick(show, !this.isDataTableVisible);
  935. // Create the div
  936. if (show && !this.dataTableDiv) {
  937. this.dataTableDiv = doc.createElement('div');
  938. this.dataTableDiv.className = 'highcharts-data-table';
  939. // Insert after the chart container
  940. this.renderTo.parentNode.insertBefore(this.dataTableDiv, this.renderTo.nextSibling);
  941. }
  942. // Toggle the visibility
  943. if (this.dataTableDiv) {
  944. this.dataTableDiv.style.display = show ? 'block' : 'none';
  945. // Generate the data table
  946. if (show) {
  947. this.dataTableDiv.innerHTML = '';
  948. var ast = new AST([this.getTableAST()]);
  949. ast.addToDOM(this.dataTableDiv);
  950. fireEvent(this, 'afterViewData', this.dataTableDiv);
  951. }
  952. }
  953. // Set the flag
  954. this.isDataTableVisible = show;
  955. // Change the menu item text
  956. var exportDivElements = this.exportDivElements, options = this.options.exporting, menuItems = options &&
  957. options.buttons &&
  958. options.buttons.contextButton.menuItems, lang = this.options.lang;
  959. if (exportingOptions &&
  960. exportingOptions.menuItemDefinitions &&
  961. lang &&
  962. lang.viewData &&
  963. lang.hideData &&
  964. menuItems &&
  965. exportDivElements &&
  966. exportDivElements.length) {
  967. AST.setElementHTML(exportDivElements[menuItems.indexOf('viewData')], this.isDataTableVisible ? lang.hideData : lang.viewData);
  968. }
  969. };
  970. // Add "Download CSV" to the exporting menu.
  971. var exportingOptions = getOptions().exporting;
  972. if (exportingOptions) {
  973. extend(exportingOptions.menuItemDefinitions, {
  974. downloadCSV: {
  975. textKey: 'downloadCSV',
  976. onclick: function () {
  977. this.downloadCSV();
  978. }
  979. },
  980. downloadXLS: {
  981. textKey: 'downloadXLS',
  982. onclick: function () {
  983. this.downloadXLS();
  984. }
  985. },
  986. viewData: {
  987. textKey: 'viewData',
  988. onclick: function () {
  989. this.toggleDataTable();
  990. }
  991. }
  992. });
  993. if (exportingOptions.buttons) {
  994. exportingOptions.buttons.contextButton.menuItems.push('separator', 'downloadCSV', 'downloadXLS', 'viewData');
  995. }
  996. }
  997. // Series specific
  998. if (seriesTypes.map) {
  999. seriesTypes.map.prototype.exportKey = 'name';
  1000. }
  1001. if (seriesTypes.mapbubble) {
  1002. seriesTypes.mapbubble.prototype.exportKey = 'name';
  1003. }
  1004. if (seriesTypes.treemap) {
  1005. seriesTypes.treemap.prototype.exportKey = 'name';
  1006. }