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.

603 lines
16 KiB

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