You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

561 lines
20 KiB

2 years ago
  1. /**
  2. * @license Data plugin for Highcharts
  3. *
  4. * (c) 2012-2013 Torstein Hønsi
  5. * Last revision 2013-06-07
  6. *
  7. * License: www.highcharts.com/license
  8. */
  9. /*
  10. * The Highcharts Data plugin is a utility to ease parsing of input sources like
  11. * CSV, HTML tables or grid views into basic configuration options for use
  12. * directly in the Highcharts constructor.
  13. *
  14. * Demo: http://jsfiddle.net/highcharts/SnLFj/
  15. *
  16. * --- OPTIONS ---
  17. *
  18. * - columns : Array<Array<Mixed>>
  19. * A two-dimensional array representing the input data on tabular form. This input can
  20. * be used when the data is already parsed, for example from a grid view component.
  21. * Each cell can be a string or number. If not switchRowsAndColumns is set, the columns
  22. * are interpreted as series. See also the rows option.
  23. *
  24. * - complete : Function(chartOptions)
  25. * The callback that is evaluated when the data is finished loading, optionally from an
  26. * external source, and parsed. The first argument passed is a finished chart options
  27. * object, containing series and an xAxis with categories if applicable. Thise options
  28. * can be extended with additional options and passed directly to the chart constructor.
  29. *
  30. * - csv : String
  31. * A comma delimited string to be parsed. Related options are startRow, endRow, startColumn
  32. * and endColumn to delimit what part of the table is used. The lineDelimiter and
  33. * itemDelimiter options define the CSV delimiter formats.
  34. *
  35. * - endColumn : Integer
  36. * In tabular input data, the first row (indexed by 0) to use. Defaults to the last
  37. * column containing data.
  38. *
  39. * - endRow : Integer
  40. * In tabular input data, the last row (indexed by 0) to use. Defaults to the last row
  41. * containing data.
  42. *
  43. * - googleSpreadsheetKey : String
  44. * A Google Spreadsheet key. See https://developers.google.com/gdata/samples/spreadsheet_sample
  45. * for general information on GS.
  46. *
  47. * - googleSpreadsheetWorksheet : String
  48. * The Google Spreadsheet worksheet. The available id's can be read from
  49. * https://spreadsheets.google.com/feeds/worksheets/{key}/public/basic
  50. *
  51. * - itemDelimiter : String
  52. * Item or cell delimiter for parsing CSV. Defaults to ",".
  53. *
  54. * - lineDelimiter : String
  55. * Line delimiter for parsing CSV. Defaults to "\n".
  56. *
  57. * - parsed : Function
  58. * A callback function to access the parsed columns, the two-dimentional input data
  59. * array directly, before they are interpreted into series data and categories.
  60. *
  61. * - parseDate : Function
  62. * A callback function to parse string representations of dates into JavaScript timestamps.
  63. * Return an integer on success.
  64. *
  65. * - rows : Array<Array<Mixed>>
  66. * The same as the columns input option, but defining rows intead of columns.
  67. *
  68. * - startColumn : Integer
  69. * In tabular input data, the first column (indexed by 0) to use.
  70. *
  71. * - startRow : Integer
  72. * In tabular input data, the first row (indexed by 0) to use.
  73. *
  74. * - table : String|HTMLElement
  75. * A HTML table or the id of such to be parsed as input data. Related options ara startRow,
  76. * endRow, startColumn and endColumn to delimit what part of the table is used.
  77. */
  78. // JSLint options:
  79. /*global jQuery */
  80. (function (Highcharts) {
  81. // Utilities
  82. var each = Highcharts.each;
  83. // The Data constructor
  84. var Data = function (dataOptions, chartOptions) {
  85. this.init(dataOptions, chartOptions);
  86. };
  87. // Set the prototype properties
  88. Highcharts.extend(Data.prototype, {
  89. /**
  90. * Initialize the Data object with the given options
  91. */
  92. init: function (options, chartOptions) {
  93. this.options = options;
  94. this.chartOptions = chartOptions;
  95. this.columns = options.columns || this.rowsToColumns(options.rows) || [];
  96. // No need to parse or interpret anything
  97. if (this.columns.length) {
  98. this.dataFound();
  99. // Parse and interpret
  100. } else {
  101. // Parse a CSV string if options.csv is given
  102. this.parseCSV();
  103. // Parse a HTML table if options.table is given
  104. this.parseTable();
  105. // Parse a Google Spreadsheet
  106. this.parseGoogleSpreadsheet();
  107. }
  108. },
  109. /**
  110. * Get the column distribution. For example, a line series takes a single column for
  111. * Y values. A range series takes two columns for low and high values respectively,
  112. * and an OHLC series takes four columns.
  113. */
  114. getColumnDistribution: function () {
  115. var chartOptions = this.chartOptions,
  116. getValueCount = function (type) {
  117. return (Highcharts.seriesTypes[type || 'line'].prototype.pointArrayMap || [0]).length;
  118. },
  119. globalType = chartOptions && chartOptions.chart && chartOptions.chart.type,
  120. individualCounts = [];
  121. each((chartOptions && chartOptions.series) || [], function (series) {
  122. individualCounts.push(getValueCount(series.type || globalType));
  123. });
  124. this.valueCount = {
  125. global: getValueCount(globalType),
  126. individual: individualCounts
  127. };
  128. },
  129. dataFound: function () {
  130. // Interpret the values into right types
  131. this.parseTypes();
  132. // Use first row for series names?
  133. this.findHeaderRow();
  134. // Handle columns if a handleColumns callback is given
  135. this.parsed();
  136. // Complete if a complete callback is given
  137. this.complete();
  138. },
  139. /**
  140. * Parse a CSV input string
  141. */
  142. parseCSV: function () {
  143. var self = this,
  144. options = this.options,
  145. csv = options.csv,
  146. columns = this.columns,
  147. startRow = options.startRow || 0,
  148. endRow = options.endRow || Number.MAX_VALUE,
  149. startColumn = options.startColumn || 0,
  150. endColumn = options.endColumn || Number.MAX_VALUE,
  151. lines,
  152. activeRowNo = 0;
  153. if (csv) {
  154. lines = csv
  155. .replace(/\r\n/g, "\n") // Unix
  156. .replace(/\r/g, "\n") // Mac
  157. .split(options.lineDelimiter || "\n");
  158. each(lines, function (line, rowNo) {
  159. var trimmed = self.trim(line),
  160. isComment = trimmed.indexOf('#') === 0,
  161. isBlank = trimmed === '',
  162. items;
  163. if (rowNo >= startRow && rowNo <= endRow && !isComment && !isBlank) {
  164. items = line.split(options.itemDelimiter || ',');
  165. each(items, function (item, colNo) {
  166. if (colNo >= startColumn && colNo <= endColumn) {
  167. if (!columns[colNo - startColumn]) {
  168. columns[colNo - startColumn] = [];
  169. }
  170. columns[colNo - startColumn][activeRowNo] = item;
  171. }
  172. });
  173. activeRowNo += 1;
  174. }
  175. });
  176. this.dataFound();
  177. }
  178. },
  179. /**
  180. * Parse a HTML table
  181. */
  182. parseTable: function () {
  183. var options = this.options,
  184. table = options.table,
  185. columns = this.columns,
  186. startRow = options.startRow || 0,
  187. endRow = options.endRow || Number.MAX_VALUE,
  188. startColumn = options.startColumn || 0,
  189. endColumn = options.endColumn || Number.MAX_VALUE,
  190. colNo;
  191. if (table) {
  192. if (typeof table === 'string') {
  193. table = document.getElementById(table);
  194. }
  195. each(table.getElementsByTagName('tr'), function (tr, rowNo) {
  196. colNo = 0;
  197. if (rowNo >= startRow && rowNo <= endRow) {
  198. each(tr.childNodes, function (item) {
  199. if ((item.tagName === 'TD' || item.tagName === 'TH') && colNo >= startColumn && colNo <= endColumn) {
  200. if (!columns[colNo]) {
  201. columns[colNo] = [];
  202. }
  203. columns[colNo][rowNo - startRow] = item.innerHTML;
  204. colNo += 1;
  205. }
  206. });
  207. }
  208. });
  209. this.dataFound(); // continue
  210. }
  211. },
  212. /**
  213. * TODO:
  214. * - switchRowsAndColumns
  215. */
  216. parseGoogleSpreadsheet: function () {
  217. var self = this,
  218. options = this.options,
  219. googleSpreadsheetKey = options.googleSpreadsheetKey,
  220. columns = this.columns,
  221. startRow = options.startRow || 0,
  222. endRow = options.endRow || Number.MAX_VALUE,
  223. startColumn = options.startColumn || 0,
  224. endColumn = options.endColumn || Number.MAX_VALUE,
  225. gr, // google row
  226. gc; // google column
  227. if (googleSpreadsheetKey) {
  228. jQuery.getJSON('https://spreadsheets.google.com/feeds/cells/' +
  229. googleSpreadsheetKey + '/' + (options.googleSpreadsheetWorksheet || 'od6') +
  230. '/public/values?alt=json-in-script&callback=?',
  231. function (json) {
  232. // Prepare the data from the spreadsheat
  233. var cells = json.feed.entry,
  234. cell,
  235. cellCount = cells.length,
  236. colCount = 0,
  237. rowCount = 0,
  238. i;
  239. // First, find the total number of columns and rows that
  240. // are actually filled with data
  241. for (i = 0; i < cellCount; i++) {
  242. cell = cells[i];
  243. colCount = Math.max(colCount, cell.gs$cell.col);
  244. rowCount = Math.max(rowCount, cell.gs$cell.row);
  245. }
  246. // Set up arrays containing the column data
  247. for (i = 0; i < colCount; i++) {
  248. if (i >= startColumn && i <= endColumn) {
  249. // Create new columns with the length of either end-start or rowCount
  250. columns[i - startColumn] = [];
  251. // Setting the length to avoid jslint warning
  252. columns[i - startColumn].length = Math.min(rowCount, endRow - startRow);
  253. }
  254. }
  255. // Loop over the cells and assign the value to the right
  256. // place in the column arrays
  257. for (i = 0; i < cellCount; i++) {
  258. cell = cells[i];
  259. gr = cell.gs$cell.row - 1; // rows start at 1
  260. gc = cell.gs$cell.col - 1; // columns start at 1
  261. // If both row and col falls inside start and end
  262. // set the transposed cell value in the newly created columns
  263. if (gc >= startColumn && gc <= endColumn &&
  264. gr >= startRow && gr <= endRow) {
  265. columns[gc - startColumn][gr - startRow] = cell.content.$t;
  266. }
  267. }
  268. self.dataFound();
  269. });
  270. }
  271. },
  272. /**
  273. * Find the header row. For now, we just check whether the first row contains
  274. * numbers or strings. Later we could loop down and find the first row with
  275. * numbers.
  276. */
  277. findHeaderRow: function () {
  278. var headerRow = 0;
  279. each(this.columns, function (column) {
  280. if (typeof column[0] !== 'string') {
  281. headerRow = null;
  282. }
  283. });
  284. this.headerRow = 0;
  285. },
  286. /**
  287. * Trim a string from whitespace
  288. */
  289. trim: function (str) {
  290. return typeof str === 'string' ? str.replace(/^\s+|\s+$/g, '') : str;
  291. },
  292. /**
  293. * Parse numeric cells in to number types and date types in to true dates.
  294. * @param {Object} columns
  295. */
  296. parseTypes: function () {
  297. var columns = this.columns,
  298. col = columns.length,
  299. row,
  300. val,
  301. floatVal,
  302. trimVal,
  303. dateVal;
  304. while (col--) {
  305. row = columns[col].length;
  306. while (row--) {
  307. val = columns[col][row];
  308. floatVal = parseFloat(val);
  309. trimVal = this.trim(val);
  310. /*jslint eqeq: true*/
  311. if (trimVal == floatVal) { // is numeric
  312. /*jslint eqeq: false*/
  313. columns[col][row] = floatVal;
  314. // If the number is greater than milliseconds in a year, assume datetime
  315. if (floatVal > 365 * 24 * 3600 * 1000) {
  316. columns[col].isDatetime = true;
  317. } else {
  318. columns[col].isNumeric = true;
  319. }
  320. } else { // string, continue to determine if it is a date string or really a string
  321. dateVal = this.parseDate(val);
  322. if (col === 0 && typeof dateVal === 'number' && !isNaN(dateVal)) { // is date
  323. columns[col][row] = dateVal;
  324. columns[col].isDatetime = true;
  325. } else { // string
  326. columns[col][row] = trimVal === '' ? null : trimVal;
  327. }
  328. }
  329. }
  330. }
  331. },
  332. //*
  333. dateFormats: {
  334. 'YYYY-mm-dd': {
  335. regex: '^([0-9]{4})-([0-9]{2})-([0-9]{2})$',
  336. parser: function (match) {
  337. return Date.UTC(+match[1], match[2] - 1, +match[3]);
  338. }
  339. }
  340. },
  341. // */
  342. /**
  343. * Parse a date and return it as a number. Overridable through options.parseDate.
  344. */
  345. parseDate: function (val) {
  346. var parseDate = this.options.parseDate,
  347. ret,
  348. key,
  349. format,
  350. match;
  351. if (parseDate) {
  352. ret = parseDate(val);
  353. }
  354. if (typeof val === 'string') {
  355. for (key in this.dateFormats) {
  356. format = this.dateFormats[key];
  357. match = val.match(format.regex);
  358. if (match) {
  359. ret = format.parser(match);
  360. }
  361. }
  362. }
  363. return ret;
  364. },
  365. /**
  366. * Reorganize rows into columns
  367. */
  368. rowsToColumns: function (rows) {
  369. var row,
  370. rowsLength,
  371. col,
  372. colsLength,
  373. columns;
  374. if (rows) {
  375. columns = [];
  376. rowsLength = rows.length;
  377. for (row = 0; row < rowsLength; row++) {
  378. colsLength = rows[row].length;
  379. for (col = 0; col < colsLength; col++) {
  380. if (!columns[col]) {
  381. columns[col] = [];
  382. }
  383. columns[col][row] = rows[row][col];
  384. }
  385. }
  386. }
  387. return columns;
  388. },
  389. /**
  390. * A hook for working directly on the parsed columns
  391. */
  392. parsed: function () {
  393. if (this.options.parsed) {
  394. this.options.parsed.call(this, this.columns);
  395. }
  396. },
  397. /**
  398. * If a complete callback function is provided in the options, interpret the
  399. * columns into a Highcharts options object.
  400. */
  401. complete: function () {
  402. var columns = this.columns,
  403. firstCol,
  404. type,
  405. options = this.options,
  406. valueCount,
  407. series,
  408. data,
  409. i,
  410. j,
  411. seriesIndex;
  412. if (options.complete) {
  413. this.getColumnDistribution();
  414. // Use first column for X data or categories?
  415. if (columns.length > 1) {
  416. firstCol = columns.shift();
  417. if (this.headerRow === 0) {
  418. firstCol.shift(); // remove the first cell
  419. }
  420. if (firstCol.isDatetime) {
  421. type = 'datetime';
  422. } else if (!firstCol.isNumeric) {
  423. type = 'category';
  424. }
  425. }
  426. // Get the names and shift the top row
  427. for (i = 0; i < columns.length; i++) {
  428. if (this.headerRow === 0) {
  429. columns[i].name = columns[i].shift();
  430. }
  431. }
  432. // Use the next columns for series
  433. series = [];
  434. for (i = 0, seriesIndex = 0; i < columns.length; seriesIndex++) {
  435. // This series' value count
  436. valueCount = Highcharts.pick(this.valueCount.individual[seriesIndex], this.valueCount.global);
  437. // Iterate down the cells of each column and add data to the series
  438. data = [];
  439. for (j = 0; j < columns[i].length; j++) {
  440. data[j] = [
  441. firstCol[j],
  442. columns[i][j] !== undefined ? columns[i][j] : null
  443. ];
  444. if (valueCount > 1) {
  445. data[j].push(columns[i + 1][j] !== undefined ? columns[i + 1][j] : null);
  446. }
  447. if (valueCount > 2) {
  448. data[j].push(columns[i + 2][j] !== undefined ? columns[i + 2][j] : null);
  449. }
  450. if (valueCount > 3) {
  451. data[j].push(columns[i + 3][j] !== undefined ? columns[i + 3][j] : null);
  452. }
  453. if (valueCount > 4) {
  454. data[j].push(columns[i + 4][j] !== undefined ? columns[i + 4][j] : null);
  455. }
  456. }
  457. // Add the series
  458. series[seriesIndex] = {
  459. name: columns[i].name,
  460. data: data
  461. };
  462. i += valueCount;
  463. }
  464. // Do the callback
  465. options.complete({
  466. xAxis: {
  467. type: type
  468. },
  469. series: series
  470. });
  471. }
  472. }
  473. });
  474. // Register the Data prototype and data function on Highcharts
  475. Highcharts.Data = Data;
  476. Highcharts.data = function (options, chartOptions) {
  477. return new Data(options, chartOptions);
  478. };
  479. // Extend Chart.init so that the Chart constructor accepts a new configuration
  480. // option group, data.
  481. Highcharts.wrap(Highcharts.Chart.prototype, 'init', function (proceed, userOptions, callback) {
  482. var chart = this;
  483. if (userOptions && userOptions.data) {
  484. Highcharts.data(Highcharts.extend(userOptions.data, {
  485. complete: function (dataOptions) {
  486. // Merge series configs
  487. if (userOptions.series) {
  488. each(userOptions.series, function (series, i) {
  489. userOptions.series[i] = Highcharts.merge(series, dataOptions.series[i]);
  490. });
  491. }
  492. // Do the merge
  493. userOptions = Highcharts.merge(dataOptions, userOptions);
  494. proceed.call(chart, userOptions, callback);
  495. }
  496. }), userOptions);
  497. } else {
  498. proceed.call(chart, userOptions, callback);
  499. }
  500. });
  501. }(Highcharts));