~jstys-z/helioviewer.org/timeline

« back to all changes in this revision

Viewing changes to timeline/js/modules/data.src.js

  • Committer: Jeff Stys
  • Date: 2014-04-21 12:46:26 UTC
  • Revision ID: jstys@sesda3.com-20140421124626-2332pb2dyjc33jxi
Proof-of-concept version of Data Coverage Timeline using Highchart/Highstock javascript library.  Changes to getDataCoverage API in order to feed the necessary data to the Timeline

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
/**
 
2
 * @license Data plugin for Highcharts
 
3
 *
 
4
 * (c) 2012-2014 Torstein Honsi
 
5
 *
 
6
 * License: www.highcharts.com/license
 
7
 */
 
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 the tab character "\t" if a tab character
 
53
 * is found in the CSV string, if not it defaults to ",".
 
54
 *
 
55
 * - lineDelimiter : String
 
56
 * Line delimiter for parsing CSV. Defaults to "\n".
 
57
 *
 
58
 * - parsed : Function
 
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.
 
61
 *
 
62
 * - parseDate : Function
 
63
 * A callback function to parse string representations of dates into JavaScript timestamps.
 
64
 * Return an integer on success.
 
65
 *
 
66
 * - rows : Array<Array<Mixed>>
 
67
 * The same as the columns input option, but defining rows intead of columns.
 
68
 *
 
69
 * - startColumn : Integer
 
70
 * In tabular input data, the first column (indexed by 0) to use. 
 
71
 *
 
72
 * - startRow : Integer
 
73
 * In tabular input data, the first row (indexed by 0) to use.
 
74
 *
 
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.
 
78
 *
 
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.
 
82
 */
 
83
 
 
84
// JSLint options:
 
85
/*global jQuery */
 
86
 
 
87
(function (Highcharts) {        
 
88
        
 
89
        // Utilities
 
90
        var each = Highcharts.each;
 
91
        
 
92
        
 
93
        // The Data constructor
 
94
        var Data = function (dataOptions, chartOptions) {
 
95
                this.init(dataOptions, chartOptions);
 
96
        };
 
97
        
 
98
        // Set the prototype properties
 
99
        Highcharts.extend(Data.prototype, {
 
100
                
 
101
        /**
 
102
         * Initialize the Data object with the given options
 
103
         */
 
104
        init: function (options, chartOptions) {
 
105
                this.options = options;
 
106
                this.chartOptions = chartOptions;
 
107
                this.columns = options.columns || this.rowsToColumns(options.rows) || [];
 
108
 
 
109
                // No need to parse or interpret anything
 
110
                if (this.columns.length) {
 
111
                        this.dataFound();
 
112
 
 
113
                // Parse and interpret
 
114
                } else {
 
115
 
 
116
                        // Parse a CSV string if options.csv is given
 
117
                        this.parseCSV();
 
118
                        
 
119
                        // Parse a HTML table if options.table is given
 
120
                        this.parseTable();
 
121
 
 
122
                        // Parse a Google Spreadsheet 
 
123
                        this.parseGoogleSpreadsheet();  
 
124
                }
 
125
 
 
126
        },
 
127
 
 
128
        /**
 
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.
 
132
         */
 
133
        getColumnDistribution: function () {
 
134
                var chartOptions = this.chartOptions,
 
135
                        getValueCount = function (type) {
 
136
                                return (Highcharts.seriesTypes[type || 'line'].prototype.pointArrayMap || [0]).length;
 
137
                        },
 
138
                        globalType = chartOptions && chartOptions.chart && chartOptions.chart.type,
 
139
                        individualCounts = [];
 
140
 
 
141
                each((chartOptions && chartOptions.series) || [], function (series) {
 
142
                        individualCounts.push(getValueCount(series.type || globalType));
 
143
                });
 
144
 
 
145
                this.valueCount = {
 
146
                        global: getValueCount(globalType),
 
147
                        individual: individualCounts
 
148
                };
 
149
        },
 
150
 
 
151
        /**
 
152
         * When the data is parsed into columns, either by CSV, table, GS or direct input,
 
153
         * continue with other operations.
 
154
         */
 
155
        dataFound: function () {
 
156
                
 
157
                if (this.options.switchRowsAndColumns) {
 
158
                        this.columns = this.rowsToColumns(this.columns);
 
159
                }
 
160
 
 
161
                // Interpret the values into right types
 
162
                this.parseTypes();
 
163
                
 
164
                // Use first row for series names?
 
165
                this.findHeaderRow();
 
166
                
 
167
                // Handle columns if a handleColumns callback is given
 
168
                this.parsed();
 
169
                
 
170
                // Complete if a complete callback is given
 
171
                this.complete();
 
172
                
 
173
        },
 
174
        
 
175
        /**
 
176
         * Parse a CSV input string
 
177
         */
 
178
        parseCSV: function () {
 
179
                var self = this,
 
180
                        options = this.options,
 
181
                        csv = options.csv,
 
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,
 
187
                        itemDelimiter,
 
188
                        lines,
 
189
                        activeRowNo = 0;
 
190
                        
 
191
                if (csv) {
 
192
                        
 
193
                        lines = csv
 
194
                                .replace(/\r\n/g, "\n") // Unix
 
195
                                .replace(/\r/g, "\n") // Mac
 
196
                                .split(options.lineDelimiter || "\n");
 
197
 
 
198
                        itemDelimiter = options.itemDelimiter || (csv.indexOf('\t') !== -1 ? '\t' : ',');
 
199
                        
 
200
                        each(lines, function (line, rowNo) {
 
201
                                var trimmed = self.trim(line),
 
202
                                        isComment = trimmed.indexOf('#') === 0,
 
203
                                        isBlank = trimmed === '',
 
204
                                        items;
 
205
                                
 
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] = [];                                      
 
212
                                                        }
 
213
                                                        
 
214
                                                        columns[colNo - startColumn][activeRowNo] = item;
 
215
                                                }
 
216
                                        });
 
217
                                        activeRowNo += 1;
 
218
                                }
 
219
                        });
 
220
 
 
221
                        this.dataFound();
 
222
                }
 
223
        },
 
224
        
 
225
        /**
 
226
         * Parse a HTML table
 
227
         */
 
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;
 
236
 
 
237
                if (table) {
 
238
                        
 
239
                        if (typeof table === 'string') {
 
240
                                table = document.getElementById(table);
 
241
                        }
 
242
                        
 
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] = [];                                      
 
249
                                                        }
 
250
                                                        
 
251
                                                        columns[colNo - startColumn][rowNo - startRow] = item.innerHTML;
 
252
                                                }
 
253
                                        });
 
254
                                }
 
255
                        });
 
256
 
 
257
                        this.dataFound(); // continue
 
258
                }
 
259
        },
 
260
 
 
261
        /**
 
262
         */
 
263
        parseGoogleSpreadsheet: function () {
 
264
                var self = this,
 
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,
 
272
                        gr, // google row
 
273
                        gc; // google column
 
274
 
 
275
                if (googleSpreadsheetKey) {
 
276
                        jQuery.ajax({
 
277
                                dataType: 'json', 
 
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,
 
285
                                                cell,
 
286
                                                cellCount = cells.length,
 
287
                                                colCount = 0,
 
288
                                                rowCount = 0,
 
289
                                                i;
 
290
                                
 
291
                                        // First, find the total number of columns and rows that 
 
292
                                        // are actually filled with data
 
293
                                        for (i = 0; i < cellCount; i++) {
 
294
                                                cell = cells[i];
 
295
                                                colCount = Math.max(colCount, cell.gs$cell.col);
 
296
                                                rowCount = Math.max(rowCount, cell.gs$cell.row);                        
 
297
                                        }
 
298
                                
 
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] = [];
 
304
 
 
305
                                                        // Setting the length to avoid jslint warning
 
306
                                                        columns[i - startColumn].length = Math.min(rowCount, endRow - startRow);
 
307
                                                }
 
308
                                        }
 
309
                                        
 
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++) {
 
313
                                                cell = cells[i];
 
314
                                                gr = cell.gs$cell.row - 1; // rows start at 1
 
315
                                                gc = cell.gs$cell.col - 1; // columns start at 1
 
316
 
 
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;
 
322
                                                }
 
323
                                        }
 
324
                                        self.dataFound();
 
325
                                }
 
326
                        });
 
327
                }
 
328
        },
 
329
        
 
330
        /**
 
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 
 
333
         * numbers.
 
334
         */
 
335
        findHeaderRow: function () {
 
336
                var headerRow = 0;
 
337
                each(this.columns, function (column) {
 
338
                        if (typeof column[0] !== 'string') {
 
339
                                headerRow = null;
 
340
                        }
 
341
                });
 
342
                this.headerRow = 0;                     
 
343
        },
 
344
        
 
345
        /**
 
346
         * Trim a string from whitespace
 
347
         */
 
348
        trim: function (str) {
 
349
                return typeof str === 'string' ? str.replace(/^\s+|\s+$/g, '') : str;
 
350
        },
 
351
        
 
352
        /**
 
353
         * Parse numeric cells in to number types and date types in to true dates.
 
354
         */
 
355
        parseTypes: function () {
 
356
                var columns = this.columns,
 
357
                        col = columns.length, 
 
358
                        row,
 
359
                        val,
 
360
                        floatVal,
 
361
                        trimVal,
 
362
                        dateVal;
 
363
                        
 
364
                while (col--) {
 
365
                        row = columns[col].length;
 
366
                        while (row--) {
 
367
                                val = columns[col][row];
 
368
                                floatVal = parseFloat(val);
 
369
                                trimVal = this.trim(val);
 
370
 
 
371
                                /*jslint eqeq: true*/
 
372
                                if (trimVal == floatVal) { // is numeric
 
373
                                /*jslint eqeq: false*/
 
374
                                        columns[col][row] = floatVal;
 
375
                                        
 
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;
 
379
                                        } else {
 
380
                                                columns[col].isNumeric = true;
 
381
                                        }                                       
 
382
                                
 
383
                                } else { // string, continue to determine if it is a date string or really a string
 
384
                                        dateVal = this.parseDate(val);
 
385
                                        
 
386
                                        if (col === 0 && typeof dateVal === 'number' && !isNaN(dateVal)) { // is date
 
387
                                                columns[col][row] = dateVal;
 
388
                                                columns[col].isDatetime = true;
 
389
                                        
 
390
                                        } else { // string
 
391
                                                columns[col][row] = trimVal === '' ? null : trimVal;
 
392
                                        }
 
393
                                }
 
394
                                
 
395
                        }
 
396
                }
 
397
        },
 
398
        
 
399
        /**
 
400
         * A collection of available date formats, extendable from the outside to support
 
401
         * custom date formats.
 
402
         */
 
403
        dateFormats: {
 
404
                'YYYY-mm-dd': {
 
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]);
 
408
                        }
 
409
                }
 
410
        },
 
411
        
 
412
        /**
 
413
         * Parse a date and return it as a number. Overridable through options.parseDate.
 
414
         */
 
415
        parseDate: function (val) {
 
416
                var parseDate = this.options.parseDate,
 
417
                        ret,
 
418
                        key,
 
419
                        format,
 
420
                        match;
 
421
 
 
422
                if (parseDate) {
 
423
                        ret = parseDate(val);
 
424
                }
 
425
                        
 
426
                if (typeof val === 'string') {
 
427
                        for (key in this.dateFormats) {
 
428
                                format = this.dateFormats[key];
 
429
                                match = val.match(format.regex);
 
430
                                if (match) {
 
431
                                        ret = format.parser(match);
 
432
                                }
 
433
                        }
 
434
                }
 
435
                return ret;
 
436
        },
 
437
        
 
438
        /**
 
439
         * Reorganize rows into columns
 
440
         */
 
441
        rowsToColumns: function (rows) {
 
442
                var row,
 
443
                        rowsLength,
 
444
                        col,
 
445
                        colsLength,
 
446
                        columns;
 
447
 
 
448
                if (rows) {
 
449
                        columns = [];
 
450
                        rowsLength = rows.length;
 
451
                        for (row = 0; row < rowsLength; row++) {
 
452
                                colsLength = rows[row].length;
 
453
                                for (col = 0; col < colsLength; col++) {
 
454
                                        if (!columns[col]) {
 
455
                                                columns[col] = [];
 
456
                                        }
 
457
                                        columns[col][row] = rows[row][col];
 
458
                                }
 
459
                        }
 
460
                }
 
461
                return columns;
 
462
        },
 
463
        
 
464
        /**
 
465
         * A hook for working directly on the parsed columns
 
466
         */
 
467
        parsed: function () {
 
468
                if (this.options.parsed) {
 
469
                        this.options.parsed.call(this, this.columns);
 
470
                }
 
471
        },
 
472
        
 
473
        /**
 
474
         * If a complete callback function is provided in the options, interpret the 
 
475
         * columns into a Highcharts options object.
 
476
         */
 
477
        complete: function () {
 
478
                
 
479
                var columns = this.columns,
 
480
                        firstCol,
 
481
                        type,
 
482
                        options = this.options,
 
483
                        valueCount,
 
484
                        series,
 
485
                        data,
 
486
                        i,
 
487
                        j,
 
488
                        seriesIndex;
 
489
                        
 
490
                
 
491
                if (options.complete) {
 
492
 
 
493
                        this.getColumnDistribution();
 
494
                        
 
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
 
500
                                }
 
501
                                
 
502
                                
 
503
                                if (firstCol.isDatetime) {
 
504
                                        type = 'datetime';
 
505
                                } else if (!firstCol.isNumeric) {
 
506
                                        type = 'category';
 
507
                                }
 
508
                        }
 
509
 
 
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();
 
514
                                }
 
515
                        }
 
516
                        
 
517
                        // Use the next columns for series
 
518
                        series = [];
 
519
                        for (i = 0, seriesIndex = 0; i < columns.length; seriesIndex++) {
 
520
 
 
521
                                // This series' value count
 
522
                                valueCount = Highcharts.pick(this.valueCount.individual[seriesIndex], this.valueCount.global);
 
523
                                
 
524
                                // Iterate down the cells of each column and add data to the series
 
525
                                data = [];
 
526
 
 
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++) {
 
531
                                                data[j] = [
 
532
                                                        firstCol[j],
 
533
                                                        columns[i][j] !== undefined ? columns[i][j] : null
 
534
                                                ];
 
535
                                                if (valueCount > 1) {
 
536
                                                        data[j].push(columns[i + 1][j] !== undefined ? columns[i + 1][j] : null);
 
537
                                                }
 
538
                                                if (valueCount > 2) {
 
539
                                                        data[j].push(columns[i + 2][j] !== undefined ? columns[i + 2][j] : null);
 
540
                                                }
 
541
                                                if (valueCount > 3) {
 
542
                                                        data[j].push(columns[i + 3][j] !== undefined ? columns[i + 3][j] : null);
 
543
                                                }
 
544
                                                if (valueCount > 4) {
 
545
                                                        data[j].push(columns[i + 4][j] !== undefined ? columns[i + 4][j] : null);
 
546
                                                }
 
547
                                        }
 
548
                                }
 
549
 
 
550
                                // Add the series
 
551
                                series[seriesIndex] = {
 
552
                                        name: columns[i].name,
 
553
                                        data: data
 
554
                                };
 
555
 
 
556
                                i += valueCount;
 
557
                        }
 
558
                        
 
559
                        // Do the callback
 
560
                        options.complete({
 
561
                                xAxis: {
 
562
                                        type: type
 
563
                                },
 
564
                                series: series
 
565
                        });
 
566
                }
 
567
        }
 
568
        });
 
569
        
 
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);
 
574
        };
 
575
 
 
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) {
 
579
                var chart = this;
 
580
 
 
581
                if (userOptions && userOptions.data) {
 
582
                        Highcharts.data(Highcharts.extend(userOptions.data, {
 
583
                                complete: function (dataOptions) {
 
584
                                        
 
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]);
 
589
                                                });
 
590
                                        }
 
591
 
 
592
                                        // Do the merge
 
593
                                        userOptions = Highcharts.merge(dataOptions, userOptions);
 
594
 
 
595
                                        proceed.call(chart, userOptions, callback);
 
596
                                }
 
597
                        }), userOptions);
 
598
                } else {
 
599
                        proceed.call(chart, userOptions, callback);
 
600
                }
 
601
        });
 
602
 
 
603
}(Highcharts));