2
* @license Data plugin for Highcharts
4
* (c) 2012-2014 Torstein Honsi
6
* License: www.highcharts.com/license
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.
14
* Demo: http://jsfiddle.net/highcharts/SnLFj/
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.
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.
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.
35
* - endColumn : Integer
36
* In tabular input data, the first row (indexed by 0) to use. Defaults to the last
37
* column containing data.
40
* In tabular input data, the last row (indexed by 0) to use. Defaults to the last row
43
* - googleSpreadsheetKey : String
44
* A Google Spreadsheet key. See https://developers.google.com/gdata/samples/spreadsheet_sample
45
* for general information on GS.
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
51
* - itemDelimiter : String
52
* Item or cell delimiter for parsing CSV. Defaults to the tab character "\t" if a tab character
53
* is found in the CSV string, if not it defaults to ",".
55
* - lineDelimiter : String
56
* Line delimiter for parsing CSV. Defaults to "\n".
59
* A callback function to access the parsed columns, the two-dimentional input data
60
* array directly, before they are interpreted into series data and categories.
62
* - parseDate : Function
63
* A callback function to parse string representations of dates into JavaScript timestamps.
64
* Return an integer on success.
66
* - rows : Array<Array<Mixed>>
67
* The same as the columns input option, but defining rows intead of columns.
69
* - startColumn : Integer
70
* In tabular input data, the first column (indexed by 0) to use.
72
* - startRow : Integer
73
* In tabular input data, the first row (indexed by 0) to use.
75
* - switchRowsAndColumns : Boolean
76
* Switch rows and columns of the input data, so that this.columns effectively becomes the
77
* rows of the data set, and the rows are interpreted as series.
79
* - table : String|HTMLElement
80
* A HTML table or the id of such to be parsed as input data. Related options ara startRow,
81
* endRow, startColumn and endColumn to delimit what part of the table is used.
87
(function (Highcharts) {
90
var each = Highcharts.each;
93
// The Data constructor
94
var Data = function (dataOptions, chartOptions) {
95
this.init(dataOptions, chartOptions);
98
// Set the prototype properties
99
Highcharts.extend(Data.prototype, {
102
* Initialize the Data object with the given options
104
init: function (options, chartOptions) {
105
this.options = options;
106
this.chartOptions = chartOptions;
107
this.columns = options.columns || this.rowsToColumns(options.rows) || [];
109
// No need to parse or interpret anything
110
if (this.columns.length) {
113
// Parse and interpret
116
// Parse a CSV string if options.csv is given
119
// Parse a HTML table if options.table is given
122
// Parse a Google Spreadsheet
123
this.parseGoogleSpreadsheet();
129
* Get the column distribution. For example, a line series takes a single column for
130
* Y values. A range series takes two columns for low and high values respectively,
131
* and an OHLC series takes four columns.
133
getColumnDistribution: function () {
134
var chartOptions = this.chartOptions,
135
getValueCount = function (type) {
136
return (Highcharts.seriesTypes[type || 'line'].prototype.pointArrayMap || [0]).length;
138
globalType = chartOptions && chartOptions.chart && chartOptions.chart.type,
139
individualCounts = [];
141
each((chartOptions && chartOptions.series) || [], function (series) {
142
individualCounts.push(getValueCount(series.type || globalType));
146
global: getValueCount(globalType),
147
individual: individualCounts
152
* When the data is parsed into columns, either by CSV, table, GS or direct input,
153
* continue with other operations.
155
dataFound: function () {
157
if (this.options.switchRowsAndColumns) {
158
this.columns = this.rowsToColumns(this.columns);
161
// Interpret the values into right types
164
// Use first row for series names?
165
this.findHeaderRow();
167
// Handle columns if a handleColumns callback is given
170
// Complete if a complete callback is given
176
* Parse a CSV input string
178
parseCSV: function () {
180
options = this.options,
182
columns = this.columns,
183
startRow = options.startRow || 0,
184
endRow = options.endRow || Number.MAX_VALUE,
185
startColumn = options.startColumn || 0,
186
endColumn = options.endColumn || Number.MAX_VALUE,
194
.replace(/\r\n/g, "\n") // Unix
195
.replace(/\r/g, "\n") // Mac
196
.split(options.lineDelimiter || "\n");
198
itemDelimiter = options.itemDelimiter || (csv.indexOf('\t') !== -1 ? '\t' : ',');
200
each(lines, function (line, rowNo) {
201
var trimmed = self.trim(line),
202
isComment = trimmed.indexOf('#') === 0,
203
isBlank = trimmed === '',
206
if (rowNo >= startRow && rowNo <= endRow && !isComment && !isBlank) {
207
items = line.split(itemDelimiter);
208
each(items, function (item, colNo) {
209
if (colNo >= startColumn && colNo <= endColumn) {
210
if (!columns[colNo - startColumn]) {
211
columns[colNo - startColumn] = [];
214
columns[colNo - startColumn][activeRowNo] = item;
228
parseTable: function () {
229
var options = this.options,
230
table = options.table,
231
columns = this.columns,
232
startRow = options.startRow || 0,
233
endRow = options.endRow || Number.MAX_VALUE,
234
startColumn = options.startColumn || 0,
235
endColumn = options.endColumn || Number.MAX_VALUE;
239
if (typeof table === 'string') {
240
table = document.getElementById(table);
243
each(table.getElementsByTagName('tr'), function (tr, rowNo) {
244
if (rowNo >= startRow && rowNo <= endRow) {
245
each(tr.children, function (item, colNo) {
246
if ((item.tagName === 'TD' || item.tagName === 'TH') && colNo >= startColumn && colNo <= endColumn) {
247
if (!columns[colNo - startColumn]) {
248
columns[colNo - startColumn] = [];
251
columns[colNo - startColumn][rowNo - startRow] = item.innerHTML;
257
this.dataFound(); // continue
263
parseGoogleSpreadsheet: function () {
265
options = this.options,
266
googleSpreadsheetKey = options.googleSpreadsheetKey,
267
columns = this.columns,
268
startRow = options.startRow || 0,
269
endRow = options.endRow || Number.MAX_VALUE,
270
startColumn = options.startColumn || 0,
271
endColumn = options.endColumn || Number.MAX_VALUE,
275
if (googleSpreadsheetKey) {
278
url: 'https://spreadsheets.google.com/feeds/cells/' +
279
googleSpreadsheetKey + '/' + (options.googleSpreadsheetWorksheet || 'od6') +
280
'/public/values?alt=json-in-script&callback=?',
281
error: options.error,
282
success: function (json) {
283
// Prepare the data from the spreadsheat
284
var cells = json.feed.entry,
286
cellCount = cells.length,
291
// First, find the total number of columns and rows that
292
// are actually filled with data
293
for (i = 0; i < cellCount; i++) {
295
colCount = Math.max(colCount, cell.gs$cell.col);
296
rowCount = Math.max(rowCount, cell.gs$cell.row);
299
// Set up arrays containing the column data
300
for (i = 0; i < colCount; i++) {
301
if (i >= startColumn && i <= endColumn) {
302
// Create new columns with the length of either end-start or rowCount
303
columns[i - startColumn] = [];
305
// Setting the length to avoid jslint warning
306
columns[i - startColumn].length = Math.min(rowCount, endRow - startRow);
310
// Loop over the cells and assign the value to the right
311
// place in the column arrays
312
for (i = 0; i < cellCount; i++) {
314
gr = cell.gs$cell.row - 1; // rows start at 1
315
gc = cell.gs$cell.col - 1; // columns start at 1
317
// If both row and col falls inside start and end
318
// set the transposed cell value in the newly created columns
319
if (gc >= startColumn && gc <= endColumn &&
320
gr >= startRow && gr <= endRow) {
321
columns[gc - startColumn][gr - startRow] = cell.content.$t;
331
* Find the header row. For now, we just check whether the first row contains
332
* numbers or strings. Later we could loop down and find the first row with
335
findHeaderRow: function () {
337
each(this.columns, function (column) {
338
if (typeof column[0] !== 'string') {
346
* Trim a string from whitespace
348
trim: function (str) {
349
return typeof str === 'string' ? str.replace(/^\s+|\s+$/g, '') : str;
353
* Parse numeric cells in to number types and date types in to true dates.
355
parseTypes: function () {
356
var columns = this.columns,
357
col = columns.length,
365
row = columns[col].length;
367
val = columns[col][row];
368
floatVal = parseFloat(val);
369
trimVal = this.trim(val);
371
/*jslint eqeq: true*/
372
if (trimVal == floatVal) { // is numeric
373
/*jslint eqeq: false*/
374
columns[col][row] = floatVal;
376
// If the number is greater than milliseconds in a year, assume datetime
377
if (floatVal > 365 * 24 * 3600 * 1000) {
378
columns[col].isDatetime = true;
380
columns[col].isNumeric = true;
383
} else { // string, continue to determine if it is a date string or really a string
384
dateVal = this.parseDate(val);
386
if (col === 0 && typeof dateVal === 'number' && !isNaN(dateVal)) { // is date
387
columns[col][row] = dateVal;
388
columns[col].isDatetime = true;
391
columns[col][row] = trimVal === '' ? null : trimVal;
400
* A collection of available date formats, extendable from the outside to support
401
* custom date formats.
405
regex: '^([0-9]{4})-([0-9]{2})-([0-9]{2})$',
406
parser: function (match) {
407
return Date.UTC(+match[1], match[2] - 1, +match[3]);
413
* Parse a date and return it as a number. Overridable through options.parseDate.
415
parseDate: function (val) {
416
var parseDate = this.options.parseDate,
423
ret = parseDate(val);
426
if (typeof val === 'string') {
427
for (key in this.dateFormats) {
428
format = this.dateFormats[key];
429
match = val.match(format.regex);
431
ret = format.parser(match);
439
* Reorganize rows into columns
441
rowsToColumns: function (rows) {
450
rowsLength = rows.length;
451
for (row = 0; row < rowsLength; row++) {
452
colsLength = rows[row].length;
453
for (col = 0; col < colsLength; col++) {
457
columns[col][row] = rows[row][col];
465
* A hook for working directly on the parsed columns
467
parsed: function () {
468
if (this.options.parsed) {
469
this.options.parsed.call(this, this.columns);
474
* If a complete callback function is provided in the options, interpret the
475
* columns into a Highcharts options object.
477
complete: function () {
479
var columns = this.columns,
482
options = this.options,
491
if (options.complete) {
493
this.getColumnDistribution();
495
// Use first column for X data or categories?
496
if (columns.length > 1) {
497
firstCol = columns.shift();
498
if (this.headerRow === 0) {
499
firstCol.shift(); // remove the first cell
503
if (firstCol.isDatetime) {
505
} else if (!firstCol.isNumeric) {
510
// Get the names and shift the top row
511
for (i = 0; i < columns.length; i++) {
512
if (this.headerRow === 0) {
513
columns[i].name = columns[i].shift();
517
// Use the next columns for series
519
for (i = 0, seriesIndex = 0; i < columns.length; seriesIndex++) {
521
// This series' value count
522
valueCount = Highcharts.pick(this.valueCount.individual[seriesIndex], this.valueCount.global);
524
// Iterate down the cells of each column and add data to the series
527
// Only loop and fill the data series if there are columns available.
528
// We need this check to avoid reading outside the array bounds.
529
if (i + valueCount <= columns.length) {
530
for (j = 0; j < columns[i].length; j++) {
533
columns[i][j] !== undefined ? columns[i][j] : null
535
if (valueCount > 1) {
536
data[j].push(columns[i + 1][j] !== undefined ? columns[i + 1][j] : null);
538
if (valueCount > 2) {
539
data[j].push(columns[i + 2][j] !== undefined ? columns[i + 2][j] : null);
541
if (valueCount > 3) {
542
data[j].push(columns[i + 3][j] !== undefined ? columns[i + 3][j] : null);
544
if (valueCount > 4) {
545
data[j].push(columns[i + 4][j] !== undefined ? columns[i + 4][j] : null);
551
series[seriesIndex] = {
552
name: columns[i].name,
570
// Register the Data prototype and data function on Highcharts
571
Highcharts.Data = Data;
572
Highcharts.data = function (options, chartOptions) {
573
return new Data(options, chartOptions);
576
// Extend Chart.init so that the Chart constructor accepts a new configuration
577
// option group, data.
578
Highcharts.wrap(Highcharts.Chart.prototype, 'init', function (proceed, userOptions, callback) {
581
if (userOptions && userOptions.data) {
582
Highcharts.data(Highcharts.extend(userOptions.data, {
583
complete: function (dataOptions) {
585
// Merge series configs
586
if (userOptions.series) {
587
each(userOptions.series, function (series, i) {
588
userOptions.series[i] = Highcharts.merge(series, dataOptions.series[i]);
593
userOptions = Highcharts.merge(dataOptions, userOptions);
595
proceed.call(chart, userOptions, callback);
599
proceed.call(chart, userOptions, callback);