1 | /**
|
2 | * @license Highcharts JS v5.0.0 (2016-09-29)
|
3 | * Data module
|
4 | *
|
5 | * (c) 2012-2016 Torstein Honsi
|
6 | *
|
7 | * License: www.highcharts.com/license
|
8 | */
|
9 | (function(factory) {
|
10 | if (typeof module === 'object' && module.exports) {
|
11 | module.exports = factory;
|
12 | } else {
|
13 | factory(Highcharts);
|
14 | }
|
15 | }(function(Highcharts) {
|
16 | (function(Highcharts) {
|
17 | /**
|
18 | * Data module
|
19 | *
|
20 | * (c) 2012-2016 Torstein Honsi
|
21 | *
|
22 | * License: www.highcharts.com/license
|
23 | */
|
24 |
|
25 | /* global jQuery */
|
26 | ;
|
27 |
|
28 | // Utilities
|
29 | var win = Highcharts.win,
|
30 | doc = win.document,
|
31 | each = Highcharts.each,
|
32 | pick = Highcharts.pick,
|
33 | inArray = Highcharts.inArray,
|
34 | isNumber = Highcharts.isNumber,
|
35 | splat = Highcharts.splat,
|
36 | SeriesBuilder;
|
37 |
|
38 |
|
39 | // The Data constructor
|
40 | var Data = function(dataOptions, chartOptions) {
|
41 | this.init(dataOptions, chartOptions);
|
42 | };
|
43 |
|
44 | // Set the prototype properties
|
45 | Highcharts.extend(Data.prototype, {
|
46 |
|
47 | /**
|
48 | * Initialize the Data object with the given options
|
49 | */
|
50 | init: function(options, chartOptions) {
|
51 | this.options = options;
|
52 | this.chartOptions = chartOptions;
|
53 | this.columns = options.columns || this.rowsToColumns(options.rows) || [];
|
54 | this.firstRowAsNames = pick(options.firstRowAsNames, true);
|
55 | this.decimalRegex = options.decimalPoint && new RegExp('^(-?[0-9]+)' + options.decimalPoint + '([0-9]+)$');
|
56 |
|
57 | // This is a two-dimensional array holding the raw, trimmed string values
|
58 | // with the same organisation as the columns array. It makes it possible
|
59 | // for example to revert from interpreted timestamps to string-based
|
60 | // categories.
|
61 | this.rawColumns = [];
|
62 |
|
63 | // No need to parse or interpret anything
|
64 | if (this.columns.length) {
|
65 | this.dataFound();
|
66 |
|
67 | // Parse and interpret
|
68 | } else {
|
69 |
|
70 | // Parse a CSV string if options.csv is given
|
71 | this.parseCSV();
|
72 |
|
73 | // Parse a HTML table if options.table is given
|
74 | this.parseTable();
|
75 |
|
76 | // Parse a Google Spreadsheet
|
77 | this.parseGoogleSpreadsheet();
|
78 | }
|
79 |
|
80 | },
|
81 |
|
82 | /**
|
83 | * Get the column distribution. For example, a line series takes a single column for
|
84 | * Y values. A range series takes two columns for low and high values respectively,
|
85 | * and an OHLC series takes four columns.
|
86 | */
|
87 | getColumnDistribution: function() {
|
88 | var chartOptions = this.chartOptions,
|
89 | options = this.options,
|
90 | xColumns = [],
|
91 | getValueCount = function(type) {
|
92 | return (Highcharts.seriesTypes[type || 'line'].prototype.pointArrayMap || [0]).length;
|
93 | },
|
94 | getPointArrayMap = function(type) {
|
95 | return Highcharts.seriesTypes[type || 'line'].prototype.pointArrayMap;
|
96 | },
|
97 | globalType = chartOptions && chartOptions.chart && chartOptions.chart.type,
|
98 | individualCounts = [],
|
99 | seriesBuilders = [],
|
100 | seriesIndex = 0,
|
101 | i;
|
102 |
|
103 | each((chartOptions && chartOptions.series) || [], function(series) {
|
104 | individualCounts.push(getValueCount(series.type || globalType));
|
105 | });
|
106 |
|
107 | // Collect the x-column indexes from seriesMapping
|
108 | each((options && options.seriesMapping) || [], function(mapping) {
|
109 | xColumns.push(mapping.x || 0);
|
110 | });
|
111 |
|
112 | // If there are no defined series with x-columns, use the first column as x column
|
113 | if (xColumns.length === 0) {
|
114 | xColumns.push(0);
|
115 | }
|
116 |
|
117 | // Loop all seriesMappings and constructs SeriesBuilders from
|
118 | // the mapping options.
|
119 | each((options && options.seriesMapping) || [], function(mapping) {
|
120 | var builder = new SeriesBuilder(),
|
121 | name,
|
122 | numberOfValueColumnsNeeded = individualCounts[seriesIndex] || getValueCount(globalType),
|
123 | seriesArr = (chartOptions && chartOptions.series) || [],
|
124 | series = seriesArr[seriesIndex] || {},
|
125 | pointArrayMap = getPointArrayMap(series.type || globalType) || ['y'];
|
126 |
|
127 | // Add an x reader from the x property or from an undefined column
|
128 | // if the property is not set. It will then be auto populated later.
|
129 | builder.addColumnReader(mapping.x, 'x');
|
130 |
|
131 | // Add all column mappings
|
132 | for (name in mapping) {
|
133 | if (mapping.hasOwnProperty(name) && name !== 'x') {
|
134 | builder.addColumnReader(mapping[name], name);
|
135 | }
|
136 | }
|
137 |
|
138 | // Add missing columns
|
139 | for (i = 0; i < numberOfValueColumnsNeeded; i++) {
|
140 | if (!builder.hasReader(pointArrayMap[i])) {
|
141 | //builder.addNextColumnReader(pointArrayMap[i]);
|
142 | // Create and add a column reader for the next free column index
|
143 | builder.addColumnReader(undefined, pointArrayMap[i]);
|
144 | }
|
145 | }
|
146 |
|
147 | seriesBuilders.push(builder);
|
148 | seriesIndex++;
|
149 | });
|
150 |
|
151 | var globalPointArrayMap = getPointArrayMap(globalType);
|
152 | if (globalPointArrayMap === undefined) {
|
153 | globalPointArrayMap = ['y'];
|
154 | }
|
155 |
|
156 | this.valueCount = {
|
157 | global: getValueCount(globalType),
|
158 | xColumns: xColumns,
|
159 | individual: individualCounts,
|
160 | seriesBuilders: seriesBuilders,
|
161 | globalPointArrayMap: globalPointArrayMap
|
162 | };
|
163 | },
|
164 |
|
165 | /**
|
166 | * When the data is parsed into columns, either by CSV, table, GS or direct input,
|
167 | * continue with other operations.
|
168 | */
|
169 | dataFound: function() {
|
170 |
|
171 | if (this.options.switchRowsAndColumns) {
|
172 | this.columns = this.rowsToColumns(this.columns);
|
173 | }
|
174 |
|
175 | // Interpret the info about series and columns
|
176 | this.getColumnDistribution();
|
177 |
|
178 | // Interpret the values into right types
|
179 | this.parseTypes();
|
180 |
|
181 | // Handle columns if a handleColumns callback is given
|
182 | if (this.parsed() !== false) {
|
183 |
|
184 | // Complete if a complete callback is given
|
185 | this.complete();
|
186 | }
|
187 |
|
188 | },
|
189 |
|
190 | /**
|
191 | * Parse a CSV input string
|
192 | */
|
193 | parseCSV: function() {
|
194 | var self = this,
|
195 | options = this.options,
|
196 | csv = options.csv,
|
197 | columns = this.columns,
|
198 | startRow = options.startRow || 0,
|
199 | endRow = options.endRow || Number.MAX_VALUE,
|
200 | startColumn = options.startColumn || 0,
|
201 | endColumn = options.endColumn || Number.MAX_VALUE,
|
202 | itemDelimiter,
|
203 | lines,
|
204 | activeRowNo = 0;
|
205 |
|
206 | if (csv) {
|
207 |
|
208 | lines = csv
|
209 | .replace(/\r\n/g, '\n') // Unix
|
210 | .replace(/\r/g, '\n') // Mac
|
211 | .split(options.lineDelimiter || '\n');
|
212 |
|
213 | itemDelimiter = options.itemDelimiter || (csv.indexOf('\t') !== -1 ? '\t' : ',');
|
214 |
|
215 | each(lines, function(line, rowNo) {
|
216 | var trimmed = self.trim(line),
|
217 | isComment = trimmed.indexOf('#') === 0,
|
218 | isBlank = trimmed === '',
|
219 | items;
|
220 |
|
221 | if (rowNo >= startRow && rowNo <= endRow && !isComment && !isBlank) {
|
222 | items = line.split(itemDelimiter);
|
223 | each(items, function(item, colNo) {
|
224 | if (colNo >= startColumn && colNo <= endColumn) {
|
225 | if (!columns[colNo - startColumn]) {
|
226 | columns[colNo - startColumn] = [];
|
227 | }
|
228 |
|
229 | columns[colNo - startColumn][activeRowNo] = item;
|
230 | }
|
231 | });
|
232 | activeRowNo += 1;
|
233 | }
|
234 | });
|
235 |
|
236 | this.dataFound();
|
237 | }
|
238 | },
|
239 |
|
240 | /**
|
241 | * Parse a HTML table
|
242 | */
|
243 | parseTable: function() {
|
244 | var options = this.options,
|
245 | table = options.table,
|
246 | columns = this.columns,
|
247 | startRow = options.startRow || 0,
|
248 | endRow = options.endRow || Number.MAX_VALUE,
|
249 | startColumn = options.startColumn || 0,
|
250 | endColumn = options.endColumn || Number.MAX_VALUE;
|
251 |
|
252 | if (table) {
|
253 |
|
254 | if (typeof table === 'string') {
|
255 | table = doc.getElementById(table);
|
256 | }
|
257 |
|
258 | each(table.getElementsByTagName('tr'), function(tr, rowNo) {
|
259 | if (rowNo >= startRow && rowNo <= endRow) {
|
260 | each(tr.children, function(item, colNo) {
|
261 | if ((item.tagName === 'TD' || item.tagName === 'TH') && colNo >= startColumn && colNo <= endColumn) {
|
262 | if (!columns[colNo - startColumn]) {
|
263 | columns[colNo - startColumn] = [];
|
264 | }
|
265 |
|
266 | columns[colNo - startColumn][rowNo - startRow] = item.innerHTML;
|
267 | }
|
268 | });
|
269 | }
|
270 | });
|
271 |
|
272 | this.dataFound(); // continue
|
273 | }
|
274 | },
|
275 |
|
276 | /**
|
277 | */
|
278 | parseGoogleSpreadsheet: function() {
|
279 | var self = this,
|
280 | options = this.options,
|
281 | googleSpreadsheetKey = options.googleSpreadsheetKey,
|
282 | columns = this.columns,
|
283 | startRow = options.startRow || 0,
|
284 | endRow = options.endRow || Number.MAX_VALUE,
|
285 | startColumn = options.startColumn || 0,
|
286 | endColumn = options.endColumn || Number.MAX_VALUE,
|
287 | gr, // google row
|
288 | gc; // google column
|
289 |
|
290 | if (googleSpreadsheetKey) {
|
291 | jQuery.ajax({
|
292 | dataType: 'json',
|
293 | url: 'https://spreadsheets.google.com/feeds/cells/' +
|
294 | googleSpreadsheetKey + '/' + (options.googleSpreadsheetWorksheet || 'od6') +
|
295 | '/public/values?alt=json-in-script&callback=?',
|
296 | error: options.error,
|
297 | success: function(json) {
|
298 | // Prepare the data from the spreadsheat
|
299 | var cells = json.feed.entry,
|
300 | cell,
|
301 | cellCount = cells.length,
|
302 | colCount = 0,
|
303 | rowCount = 0,
|
304 | i;
|
305 |
|
306 | // First, find the total number of columns and rows that
|
307 | // are actually filled with data
|
308 | for (i = 0; i < cellCount; i++) {
|
309 | cell = cells[i];
|
310 | colCount = Math.max(colCount, cell.gs$cell.col);
|
311 | rowCount = Math.max(rowCount, cell.gs$cell.row);
|
312 | }
|
313 |
|
314 | // Set up arrays containing the column data
|
315 | for (i = 0; i < colCount; i++) {
|
316 | if (i >= startColumn && i <= endColumn) {
|
317 | // Create new columns with the length of either end-start or rowCount
|
318 | columns[i - startColumn] = [];
|
319 |
|
320 | // Setting the length to avoid jslint warning
|
321 | columns[i - startColumn].length = Math.min(rowCount, endRow - startRow);
|
322 | }
|
323 | }
|
324 |
|
325 | // Loop over the cells and assign the value to the right
|
326 | // place in the column arrays
|
327 | for (i = 0; i < cellCount; i++) {
|
328 | cell = cells[i];
|
329 | gr = cell.gs$cell.row - 1; // rows start at 1
|
330 | gc = cell.gs$cell.col - 1; // columns start at 1
|
331 |
|
332 | // If both row and col falls inside start and end
|
333 | // set the transposed cell value in the newly created columns
|
334 | if (gc >= startColumn && gc <= endColumn &&
|
335 | gr >= startRow && gr <= endRow) {
|
336 | columns[gc - startColumn][gr - startRow] = cell.content.$t;
|
337 | }
|
338 | }
|
339 |
|
340 | // Insert null for empty spreadsheet cells (#5298)
|
341 | each(columns, function(column) {
|
342 | for (i = 0; i < column.length; i++) {
|
343 | if (column[i] === undefined) {
|
344 | column[i] = null;
|
345 | }
|
346 | }
|
347 | });
|
348 |
|
349 | self.dataFound();
|
350 | }
|
351 | });
|
352 | }
|
353 | },
|
354 |
|
355 | /**
|
356 | * Trim a string from whitespace
|
357 | */
|
358 | trim: function(str, inside) {
|
359 | if (typeof str === 'string') {
|
360 | str = str.replace(/^\s+|\s+$/g, '');
|
361 |
|
362 | // Clear white space insdie the string, like thousands separators
|
363 | if (inside && /^[0-9\s]+$/.test(str)) {
|
364 | str = str.replace(/\s/g, '');
|
365 | }
|
366 |
|
367 | if (this.decimalRegex) {
|
368 | str = str.replace(this.decimalRegex, '$1.$2');
|
369 | }
|
370 | }
|
371 | return str;
|
372 | },
|
373 |
|
374 | /**
|
375 | * Parse numeric cells in to number types and date types in to true dates.
|
376 | */
|
377 | parseTypes: function() {
|
378 | var columns = this.columns,
|
379 | col = columns.length;
|
380 |
|
381 | while (col--) {
|
382 | this.parseColumn(columns[col], col);
|
383 | }
|
384 |
|
385 | },
|
386 |
|
387 | /**
|
388 | * Parse a single column. Set properties like .isDatetime and .isNumeric.
|
389 | */
|
390 | parseColumn: function(column, col) {
|
391 | var rawColumns = this.rawColumns,
|
392 | columns = this.columns,
|
393 | row = column.length,
|
394 | val,
|
395 | floatVal,
|
396 | trimVal,
|
397 | trimInsideVal,
|
398 | firstRowAsNames = this.firstRowAsNames,
|
399 | isXColumn = inArray(col, this.valueCount.xColumns) !== -1,
|
400 | dateVal,
|
401 | backup = [],
|
402 | diff,
|
403 | chartOptions = this.chartOptions,
|
404 | descending,
|
405 | columnTypes = this.options.columnTypes || [],
|
406 | columnType = columnTypes[col],
|
407 | forceCategory = isXColumn && ((chartOptions && chartOptions.xAxis && splat(chartOptions.xAxis)[0].type === 'category') || columnType === 'string');
|
408 |
|
409 | if (!rawColumns[col]) {
|
410 | rawColumns[col] = [];
|
411 | }
|
412 | while (row--) {
|
413 | val = backup[row] || column[row];
|
414 |
|
415 | trimVal = this.trim(val);
|
416 | trimInsideVal = this.trim(val, true);
|
417 | floatVal = parseFloat(trimInsideVal);
|
418 |
|
419 | // Set it the first time
|
420 | if (rawColumns[col][row] === undefined) {
|
421 | rawColumns[col][row] = trimVal;
|
422 | }
|
423 |
|
424 | // Disable number or date parsing by setting the X axis type to category
|
425 | if (forceCategory || (row === 0 && firstRowAsNames)) {
|
426 | column[row] = trimVal;
|
427 |
|
428 | } else if (+trimInsideVal === floatVal) { // is numeric
|
429 |
|
430 | column[row] = floatVal;
|
431 |
|
432 | // If the number is greater than milliseconds in a year, assume datetime
|
433 | if (floatVal > 365 * 24 * 3600 * 1000 && columnType !== 'float') {
|
434 | column.isDatetime = true;
|
435 | } else {
|
436 | column.isNumeric = true;
|
437 | }
|
438 |
|
439 | if (column[row + 1] !== undefined) {
|
440 | descending = floatVal > column[row + 1];
|
441 | }
|
442 |
|
443 | // String, continue to determine if it is a date string or really a string
|
444 | } else {
|
445 | dateVal = this.parseDate(val);
|
446 | // Only allow parsing of dates if this column is an x-column
|
447 | if (isXColumn && isNumber(dateVal) && columnType !== 'float') { // is date
|
448 | backup[row] = val;
|
449 | column[row] = dateVal;
|
450 | column.isDatetime = true;
|
451 |
|
452 | // Check if the dates are uniformly descending or ascending. If they
|
453 | // are not, chances are that they are a different time format, so check
|
454 | // for alternative.
|
455 | if (column[row + 1] !== undefined) {
|
456 | diff = dateVal > column[row + 1];
|
457 | if (diff !== descending && descending !== undefined) {
|
458 | if (this.alternativeFormat) {
|
459 | this.dateFormat = this.alternativeFormat;
|
460 | row = column.length;
|
461 | this.alternativeFormat = this.dateFormats[this.dateFormat].alternative;
|
462 | } else {
|
463 | column.unsorted = true;
|
464 | }
|
465 | }
|
466 | descending = diff;
|
467 | }
|
468 |
|
469 | } else { // string
|
470 | column[row] = trimVal === '' ? null : trimVal;
|
471 | if (row !== 0 && (column.isDatetime || column.isNumeric)) {
|
472 | column.mixed = true;
|
473 | }
|
474 | }
|
475 | }
|
476 | }
|
477 |
|
478 | // If strings are intermixed with numbers or dates in a parsed column, it is an indication
|
479 | // that parsing went wrong or the data was not intended to display as numbers or dates and
|
480 | // parsing is too aggressive. Fall back to categories. Demonstrated in the
|
481 | // highcharts/demo/column-drilldown sample.
|
482 | if (isXColumn && column.mixed) {
|
483 | columns[col] = rawColumns[col];
|
484 | }
|
485 |
|
486 | // If the 0 column is date or number and descending, reverse all columns.
|
487 | if (isXColumn && descending && this.options.sort) {
|
488 | for (col = 0; col < columns.length; col++) {
|
489 | columns[col].reverse();
|
490 | if (firstRowAsNames) {
|
491 | columns[col].unshift(columns[col].pop());
|
492 | }
|
493 | }
|
494 | }
|
495 | },
|
496 |
|
497 | /**
|
498 | * A collection of available date formats, extendable from the outside to support
|
499 | * custom date formats.
|
500 | */
|
501 | dateFormats: {
|
502 | 'YYYY-mm-dd': {
|
503 | regex: /^([0-9]{4})[\-\/\.]([0-9]{2})[\-\/\.]([0-9]{2})$/,
|
504 | parser: function(match) {
|
505 | return Date.UTC(+match[1], match[2] - 1, +match[3]);
|
506 | }
|
507 | },
|
508 | 'dd/mm/YYYY': {
|
509 | regex: /^([0-9]{1,2})[\-\/\.]([0-9]{1,2})[\-\/\.]([0-9]{4})$/,
|
510 | parser: function(match) {
|
511 | return Date.UTC(+match[3], match[2] - 1, +match[1]);
|
512 | },
|
513 | alternative: 'mm/dd/YYYY' // different format with the same regex
|
514 | },
|
515 | 'mm/dd/YYYY': {
|
516 | regex: /^([0-9]{1,2})[\-\/\.]([0-9]{1,2})[\-\/\.]([0-9]{4})$/,
|
517 | parser: function(match) {
|
518 | return Date.UTC(+match[3], match[1] - 1, +match[2]);
|
519 | }
|
520 | },
|
521 | 'dd/mm/YY': {
|
522 | regex: /^([0-9]{1,2})[\-\/\.]([0-9]{1,2})[\-\/\.]([0-9]{2})$/,
|
523 | parser: function(match) {
|
524 | return Date.UTC(+match[3] + 2000, match[2] - 1, +match[1]);
|
525 | },
|
526 | alternative: 'mm/dd/YY' // different format with the same regex
|
527 | },
|
528 | 'mm/dd/YY': {
|
529 | regex: /^([0-9]{1,2})[\-\/\.]([0-9]{1,2})[\-\/\.]([0-9]{2})$/,
|
530 | parser: function(match) {
|
531 | return Date.UTC(+match[3] + 2000, match[1] - 1, +match[2]);
|
532 | }
|
533 | }
|
534 | },
|
535 |
|
536 | /**
|
537 | * Parse a date and return it as a number. Overridable through options.parseDate.
|
538 | */
|
539 | parseDate: function(val) {
|
540 | var parseDate = this.options.parseDate,
|
541 | ret,
|
542 | key,
|
543 | format,
|
544 | dateFormat = this.options.dateFormat || this.dateFormat,
|
545 | match;
|
546 |
|
547 | if (parseDate) {
|
548 | ret = parseDate(val);
|
549 |
|
550 | } else if (typeof val === 'string') {
|
551 | // Auto-detect the date format the first time
|
552 | if (!dateFormat) {
|
553 | for (key in this.dateFormats) {
|
554 | format = this.dateFormats[key];
|
555 | match = val.match(format.regex);
|
556 | if (match) {
|
557 | this.dateFormat = dateFormat = key;
|
558 | this.alternativeFormat = format.alternative;
|
559 | ret = format.parser(match);
|
560 | break;
|
561 | }
|
562 | }
|
563 | // Next time, use the one previously found
|
564 | } else {
|
565 | format = this.dateFormats[dateFormat];
|
566 | match = val.match(format.regex);
|
567 | if (match) {
|
568 | ret = format.parser(match);
|
569 | }
|
570 | }
|
571 | // Fall back to Date.parse
|
572 | if (!match) {
|
573 | match = Date.parse(val);
|
574 | // External tools like Date.js and MooTools extend Date object and
|
575 | // returns a date.
|
576 | if (typeof match === 'object' && match !== null && match.getTime) {
|
577 | ret = match.getTime() - match.getTimezoneOffset() * 60000;
|
578 |
|
579 | // Timestamp
|
580 | } else if (isNumber(match)) {
|
581 | ret = match - (new Date(match)).getTimezoneOffset() * 60000;
|
582 | }
|
583 | }
|
584 | }
|
585 | return ret;
|
586 | },
|
587 |
|
588 | /**
|
589 | * Reorganize rows into columns
|
590 | */
|
591 | rowsToColumns: function(rows) {
|
592 | var row,
|
593 | rowsLength,
|
594 | col,
|
595 | colsLength,
|
596 | columns;
|
597 |
|
598 | if (rows) {
|
599 | columns = [];
|
600 | rowsLength = rows.length;
|
601 | for (row = 0; row < rowsLength; row++) {
|
602 | colsLength = rows[row].length;
|
603 | for (col = 0; col < colsLength; col++) {
|
604 | if (!columns[col]) {
|
605 | columns[col] = [];
|
606 | }
|
607 | columns[col][row] = rows[row][col];
|
608 | }
|
609 | }
|
610 | }
|
611 | return columns;
|
612 | },
|
613 |
|
614 | /**
|
615 | * A hook for working directly on the parsed columns
|
616 | */
|
617 | parsed: function() {
|
618 | if (this.options.parsed) {
|
619 | return this.options.parsed.call(this, this.columns);
|
620 | }
|
621 | },
|
622 |
|
623 | getFreeIndexes: function(numberOfColumns, seriesBuilders) {
|
624 | var s,
|
625 | i,
|
626 | freeIndexes = [],
|
627 | freeIndexValues = [],
|
628 | referencedIndexes;
|
629 |
|
630 | // Add all columns as free
|
631 | for (i = 0; i < numberOfColumns; i = i + 1) {
|
632 | freeIndexes.push(true);
|
633 | }
|
634 |
|
635 | // Loop all defined builders and remove their referenced columns
|
636 | for (s = 0; s < seriesBuilders.length; s = s + 1) {
|
637 | referencedIndexes = seriesBuilders[s].getReferencedColumnIndexes();
|
638 |
|
639 | for (i = 0; i < referencedIndexes.length; i = i + 1) {
|
640 | freeIndexes[referencedIndexes[i]] = false;
|
641 | }
|
642 | }
|
643 |
|
644 | // Collect the values for the free indexes
|
645 | for (i = 0; i < freeIndexes.length; i = i + 1) {
|
646 | if (freeIndexes[i]) {
|
647 | freeIndexValues.push(i);
|
648 | }
|
649 | }
|
650 |
|
651 | return freeIndexValues;
|
652 | },
|
653 |
|
654 | /**
|
655 | * If a complete callback function is provided in the options, interpret the
|
656 | * columns into a Highcharts options object.
|
657 | */
|
658 | complete: function() {
|
659 |
|
660 | var columns = this.columns,
|
661 | xColumns = [],
|
662 | type,
|
663 | options = this.options,
|
664 | series,
|
665 | data,
|
666 | i,
|
667 | j,
|
668 | r,
|
669 | seriesIndex,
|
670 | chartOptions,
|
671 | allSeriesBuilders = [],
|
672 | builder,
|
673 | freeIndexes,
|
674 | typeCol,
|
675 | index;
|
676 |
|
677 | xColumns.length = columns.length;
|
678 | if (options.complete || options.afterComplete) {
|
679 |
|
680 | // Get the names and shift the top row
|
681 | for (i = 0; i < columns.length; i++) {
|
682 | if (this.firstRowAsNames) {
|
683 | columns[i].name = columns[i].shift();
|
684 | }
|
685 | }
|
686 |
|
687 | // Use the next columns for series
|
688 | series = [];
|
689 | freeIndexes = this.getFreeIndexes(columns.length, this.valueCount.seriesBuilders);
|
690 |
|
691 | // Populate defined series
|
692 | for (seriesIndex = 0; seriesIndex < this.valueCount.seriesBuilders.length; seriesIndex++) {
|
693 | builder = this.valueCount.seriesBuilders[seriesIndex];
|
694 |
|
695 | // If the builder can be populated with remaining columns, then add it to allBuilders
|
696 | if (builder.populateColumns(freeIndexes)) {
|
697 | allSeriesBuilders.push(builder);
|
698 | }
|
699 | }
|
700 |
|
701 | // Populate dynamic series
|
702 | while (freeIndexes.length > 0) {
|
703 | builder = new SeriesBuilder();
|
704 | builder.addColumnReader(0, 'x');
|
705 |
|
706 | // Mark index as used (not free)
|
707 | index = inArray(0, freeIndexes);
|
708 | if (index !== -1) {
|
709 | freeIndexes.splice(index, 1);
|
710 | }
|
711 |
|
712 | for (i = 0; i < this.valueCount.global; i++) {
|
713 | // Create and add a column reader for the next free column index
|
714 | builder.addColumnReader(undefined, this.valueCount.globalPointArrayMap[i]);
|
715 | }
|
716 |
|
717 | // If the builder can be populated with remaining columns, then add it to allBuilders
|
718 | if (builder.populateColumns(freeIndexes)) {
|
719 | allSeriesBuilders.push(builder);
|
720 | }
|
721 | }
|
722 |
|
723 | // Get the data-type from the first series x column
|
724 | if (allSeriesBuilders.length > 0 && allSeriesBuilders[0].readers.length > 0) {
|
725 | typeCol = columns[allSeriesBuilders[0].readers[0].columnIndex];
|
726 | if (typeCol !== undefined) {
|
727 | if (typeCol.isDatetime) {
|
728 | type = 'datetime';
|
729 | } else if (!typeCol.isNumeric) {
|
730 | type = 'category';
|
731 | }
|
732 | }
|
733 | }
|
734 | // Axis type is category, then the "x" column should be called "name"
|
735 | if (type === 'category') {
|
736 | for (seriesIndex = 0; seriesIndex < allSeriesBuilders.length; seriesIndex++) {
|
737 | builder = allSeriesBuilders[seriesIndex];
|
738 | for (r = 0; r < builder.readers.length; r++) {
|
739 | if (builder.readers[r].configName === 'x') {
|
740 | builder.readers[r].configName = 'name';
|
741 | }
|
742 | }
|
743 | }
|
744 | }
|
745 |
|
746 | // Read data for all builders
|
747 | for (seriesIndex = 0; seriesIndex < allSeriesBuilders.length; seriesIndex++) {
|
748 | builder = allSeriesBuilders[seriesIndex];
|
749 |
|
750 | // Iterate down the cells of each column and add data to the series
|
751 | data = [];
|
752 | for (j = 0; j < columns[0].length; j++) {
|
753 | data[j] = builder.read(columns, j);
|
754 | }
|
755 |
|
756 | // Add the series
|
757 | series[seriesIndex] = {
|
758 | data: data
|
759 | };
|
760 | if (builder.name) {
|
761 | series[seriesIndex].name = builder.name;
|
762 | }
|
763 | if (type === 'category') {
|
764 | series[seriesIndex].turboThreshold = 0;
|
765 | }
|
766 | }
|
767 |
|
768 |
|
769 |
|
770 | // Do the callback
|
771 | chartOptions = {
|
772 | series: series
|
773 | };
|
774 | if (type) {
|
775 | chartOptions.xAxis = {
|
776 | type: type
|
777 | };
|
778 | if (type === 'category') {
|
779 | chartOptions.xAxis.nameToX = false;
|
780 | }
|
781 | }
|
782 |
|
783 | if (options.complete) {
|
784 | options.complete(chartOptions);
|
785 | }
|
786 |
|
787 | // The afterComplete hook is used internally to avoid conflict with the externally
|
788 | // available complete option.
|
789 | if (options.afterComplete) {
|
790 | options.afterComplete(chartOptions);
|
791 | }
|
792 | }
|
793 | }
|
794 | });
|
795 |
|
796 | // Register the Data prototype and data function on Highcharts
|
797 | Highcharts.Data = Data;
|
798 | Highcharts.data = function(options, chartOptions) {
|
799 | return new Data(options, chartOptions);
|
800 | };
|
801 |
|
802 | // Extend Chart.init so that the Chart constructor accepts a new configuration
|
803 | // option group, data.
|
804 | Highcharts.wrap(Highcharts.Chart.prototype, 'init', function(proceed, userOptions, callback) {
|
805 | var chart = this;
|
806 |
|
807 | if (userOptions && userOptions.data) {
|
808 | Highcharts.data(Highcharts.extend(userOptions.data, {
|
809 |
|
810 | afterComplete: function(dataOptions) {
|
811 | var i, series;
|
812 |
|
813 | // Merge series configs
|
814 | if (userOptions.hasOwnProperty('series')) {
|
815 | if (typeof userOptions.series === 'object') {
|
816 | i = Math.max(userOptions.series.length, dataOptions.series.length);
|
817 | while (i--) {
|
818 | series = userOptions.series[i] || {};
|
819 | userOptions.series[i] = Highcharts.merge(series, dataOptions.series[i]);
|
820 | }
|
821 | } else { // Allow merging in dataOptions.series (#2856)
|
822 | delete userOptions.series;
|
823 | }
|
824 | }
|
825 |
|
826 | // Do the merge
|
827 | userOptions = Highcharts.merge(dataOptions, userOptions);
|
828 |
|
829 | proceed.call(chart, userOptions, callback);
|
830 | }
|
831 | }), userOptions);
|
832 | } else {
|
833 | proceed.call(chart, userOptions, callback);
|
834 | }
|
835 | });
|
836 |
|
837 | /**
|
838 | * Creates a new SeriesBuilder. A SeriesBuilder consists of a number
|
839 | * of ColumnReaders that reads columns and give them a name.
|
840 | * Ex: A series builder can be constructed to read column 3 as 'x' and
|
841 | * column 7 and 8 as 'y1' and 'y2'.
|
842 | * The output would then be points/rows of the form {x: 11, y1: 22, y2: 33}
|
843 | *
|
844 | * The name of the builder is taken from the second column. In the above
|
845 | * example it would be the column with index 7.
|
846 | * @constructor
|
847 | */
|
848 | SeriesBuilder = function() {
|
849 | this.readers = [];
|
850 | this.pointIsArray = true;
|
851 | };
|
852 |
|
853 | /**
|
854 | * Populates readers with column indexes. A reader can be added without
|
855 | * a specific index and for those readers the index is taken sequentially
|
856 | * from the free columns (this is handled by the ColumnCursor instance).
|
857 | * @returns {boolean}
|
858 | */
|
859 | SeriesBuilder.prototype.populateColumns = function(freeIndexes) {
|
860 | var builder = this,
|
861 | enoughColumns = true;
|
862 |
|
863 | // Loop each reader and give it an index if its missing.
|
864 | // The freeIndexes.shift() will return undefined if there
|
865 | // are no more columns.
|
866 | each(builder.readers, function(reader) {
|
867 | if (reader.columnIndex === undefined) {
|
868 | reader.columnIndex = freeIndexes.shift();
|
869 | }
|
870 | });
|
871 |
|
872 | // Now, all readers should have columns mapped. If not
|
873 | // then return false to signal that this series should
|
874 | // not be added.
|
875 | each(builder.readers, function(reader) {
|
876 | if (reader.columnIndex === undefined) {
|
877 | enoughColumns = false;
|
878 | }
|
879 | });
|
880 |
|
881 | return enoughColumns;
|
882 | };
|
883 |
|
884 | /**
|
885 | * Reads a row from the dataset and returns a point or array depending
|
886 | * on the names of the readers.
|
887 | * @param columns
|
888 | * @param rowIndex
|
889 | * @returns {Array | Object}
|
890 | */
|
891 | SeriesBuilder.prototype.read = function(columns, rowIndex) {
|
892 | var builder = this,
|
893 | pointIsArray = builder.pointIsArray,
|
894 | point = pointIsArray ? [] : {},
|
895 | columnIndexes;
|
896 |
|
897 | // Loop each reader and ask it to read its value.
|
898 | // Then, build an array or point based on the readers names.
|
899 | each(builder.readers, function(reader) {
|
900 | var value = columns[reader.columnIndex][rowIndex];
|
901 | if (pointIsArray) {
|
902 | point.push(value);
|
903 | } else {
|
904 | point[reader.configName] = value;
|
905 | }
|
906 | });
|
907 |
|
908 | // The name comes from the first column (excluding the x column)
|
909 | if (this.name === undefined && builder.readers.length >= 2) {
|
910 | columnIndexes = builder.getReferencedColumnIndexes();
|
911 | if (columnIndexes.length >= 2) {
|
912 | // remove the first one (x col)
|
913 | columnIndexes.shift();
|
914 |
|
915 | // Sort the remaining
|
916 | columnIndexes.sort();
|
917 |
|
918 | // Now use the lowest index as name column
|
919 | this.name = columns[columnIndexes.shift()].name;
|
920 | }
|
921 | }
|
922 |
|
923 | return point;
|
924 | };
|
925 |
|
926 | /**
|
927 | * Creates and adds ColumnReader from the given columnIndex and configName.
|
928 | * ColumnIndex can be undefined and in that case the reader will be given
|
929 | * an index when columns are populated.
|
930 | * @param columnIndex {Number | undefined}
|
931 | * @param configName
|
932 | */
|
933 | SeriesBuilder.prototype.addColumnReader = function(columnIndex, configName) {
|
934 | this.readers.push({
|
935 | columnIndex: columnIndex,
|
936 | configName: configName
|
937 | });
|
938 |
|
939 | if (!(configName === 'x' || configName === 'y' || configName === undefined)) {
|
940 | this.pointIsArray = false;
|
941 | }
|
942 | };
|
943 |
|
944 | /**
|
945 | * Returns an array of column indexes that the builder will use when
|
946 | * reading data.
|
947 | * @returns {Array}
|
948 | */
|
949 | SeriesBuilder.prototype.getReferencedColumnIndexes = function() {
|
950 | var i,
|
951 | referencedColumnIndexes = [],
|
952 | columnReader;
|
953 |
|
954 | for (i = 0; i < this.readers.length; i = i + 1) {
|
955 | columnReader = this.readers[i];
|
956 | if (columnReader.columnIndex !== undefined) {
|
957 | referencedColumnIndexes.push(columnReader.columnIndex);
|
958 | }
|
959 | }
|
960 |
|
961 | return referencedColumnIndexes;
|
962 | };
|
963 |
|
964 | /**
|
965 | * Returns true if the builder has a reader for the given configName.
|
966 | * @param configName
|
967 | * @returns {boolean}
|
968 | */
|
969 | SeriesBuilder.prototype.hasReader = function(configName) {
|
970 | var i, columnReader;
|
971 | for (i = 0; i < this.readers.length; i = i + 1) {
|
972 | columnReader = this.readers[i];
|
973 | if (columnReader.configName === configName) {
|
974 | return true;
|
975 | }
|
976 | }
|
977 | // Else return undefined
|
978 | };
|
979 |
|
980 | }(Highcharts));
|
981 | }));
|