export-data.src.js 30 KB

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