highcharts-rails 4.0.3 → 4.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @license Highcharts JS v4.0.3 (2014-07-03)
2
+ * @license Highcharts JS v4.0.4 (2014-09-02)
3
3
  *
4
4
  * Standalone Highcharts Framework
5
5
  *
@@ -15,6 +15,7 @@ var UNDEFINED,
15
15
  emptyArray = [],
16
16
  timers = [],
17
17
  timerId,
18
+ animSetters = {},
18
19
  Fx;
19
20
 
20
21
  Math.easeInOutSine = function (t, b, c, d) {
@@ -184,6 +185,7 @@ function augment(obj) {
184
185
 
185
186
 
186
187
  return {
188
+
187
189
  /**
188
190
  * Initialize the adapter. This is run once as Highcharts is first run.
189
191
  */
@@ -292,10 +294,14 @@ return {
292
294
  elem = this.elem,
293
295
  elemelem = elem.element; // if destroyed, it is null
294
296
 
297
+ // Animation setter defined from outside
298
+ if (animSetters[this.prop]) {
299
+ animSetters[this.prop](this);
300
+
295
301
  // Animating a path definition on SVGElement
296
- if (paths && elemelem) {
302
+ } else if (paths && elemelem) {
297
303
  elem.attr('d', pathAnim.step(paths[0], paths[1], this.now, this.toD));
298
-
304
+
299
305
  // Other animations on SVGElement
300
306
  } else if (elem.attr) {
301
307
  if (elemelem) {
@@ -440,7 +446,7 @@ return {
440
446
  }
441
447
 
442
448
  if (!end) {
443
- end = parseFloat(prop[name]);
449
+ end = prop[name];
444
450
  }
445
451
  fx.custom(start, end, unit);
446
452
  }
@@ -454,6 +460,13 @@ return {
454
460
  return window.getComputedStyle(el, undefined).getPropertyValue(prop);
455
461
  },
456
462
 
463
+ /**
464
+ * Add an animation setter for a specific property
465
+ */
466
+ addAnimSetter: function (prop, fn) {
467
+ animSetters[prop] = fn;
468
+ },
469
+
457
470
  /**
458
471
  * Downloads a script and executes a callback when done.
459
472
  * @param {String} scriptLocation
@@ -2,7 +2,7 @@
2
2
  // @compilation_level SIMPLE_OPTIMIZATIONS
3
3
 
4
4
  /**
5
- * @license Highcharts JS v4.0.3 (2014-07-03)
5
+ * @license Highcharts JS v4.0.4 (2014-09-02)
6
6
  *
7
7
  * (c) 2009-2013 Torstein Hønsi
8
8
  *
@@ -75,6 +75,11 @@ function perspective(points, angle2, angle1, origin) {
75
75
  ////// HELPER METHODS //////
76
76
  var dFactor = (4 * (Math.sqrt(2) - 1) / 3) / (PI / 2);
77
77
 
78
+ function defined(obj) {
79
+ return obj !== undefined && obj !== null;
80
+ }
81
+
82
+
78
83
  function curveTo(cx, cy, rx, ry, start, end, dx, dy) {
79
84
  var result = [];
80
85
  if ((end > start) && (end - start > PI / 2 + 0.0001)) {
@@ -150,7 +155,7 @@ Highcharts.SVGRenderer.prototype.cuboid = function (shapeArgs) {
150
155
  };
151
156
 
152
157
  result.attr = function (args) {
153
- if (args.shapeArgs || args.x) {
158
+ if (args.shapeArgs || defined(args.x)) {
154
159
  var shapeArgs = args.shapeArgs || args;
155
160
  var paths = this.renderer.cuboidPath(shapeArgs);
156
161
  this.front.attr({d: paths[0], zIndex: paths[3]});
@@ -164,7 +169,7 @@ Highcharts.SVGRenderer.prototype.cuboid = function (shapeArgs) {
164
169
  };
165
170
 
166
171
  result.animate = function (args, duration, complete) {
167
- if (args.x && args.y) {
172
+ if (defined(args.x) && defined(args.y)) {
168
173
  var paths = this.renderer.cuboidPath(args);
169
174
  this.front.attr({zIndex: paths[3]}).animate({d: paths[0]}, duration, complete);
170
175
  this.top.attr({zIndex: paths[4]}).animate({d: paths[1]}, duration, complete);
@@ -333,7 +338,7 @@ Highcharts.SVGRenderer.prototype.arc3d = function (shapeArgs) {
333
338
  };
334
339
 
335
340
  result.animate = function (args, duration, complete) {
336
- if (args.end || args.start) {
341
+ if (defined(args.end) || defined(args.start)) {
337
342
  this._shapeArgs = this.shapeArgs;
338
343
 
339
344
  Highcharts.SVGElement.prototype.animate.call(this, {
@@ -348,6 +353,10 @@ Highcharts.SVGRenderer.prototype.arc3d = function (shapeArgs) {
348
353
  end = fx.end,
349
354
  pos = fx.pos,
350
355
  sA = Highcharts.merge(start, {
356
+ x: start.x + ((end.x - start.x) * pos),
357
+ y: start.y + ((end.y - start.y) * pos),
358
+ r: start.r + ((end.r - start.r) * pos),
359
+ innerR: start.innerR + ((end.innerR - start.innerR) * pos),
351
360
  start: start.start + ((end.start - start.start) * pos),
352
361
  end: start.end + ((end.end - start.end) * pos)
353
362
  });
@@ -518,7 +527,20 @@ defaultChartOptions.chart.options3d = {
518
527
  back: { size: 1, color: 'rgba(255,255,255,0)' }
519
528
  }
520
529
  };
521
- defaultChartOptions.plotOptions.pie.borderColor = undefined;
530
+
531
+ Highcharts.wrap(Highcharts.Chart.prototype, 'init', function (proceed) {
532
+ var args = [].slice.call(arguments, 1),
533
+ plotOptions,
534
+ pieOptions;
535
+
536
+ if (args[0].chart.options3d && args[0].chart.options3d.enabled) {
537
+ plotOptions = args[0].plotOptions || {};
538
+ pieOptions = plotOptions.pie || {};
539
+
540
+ pieOptions.borderColor = Highcharts.pick(pieOptions.borderColor, undefined);
541
+ }
542
+ proceed.apply(this, args);
543
+ });
522
544
 
523
545
  Highcharts.wrap(Highcharts.Chart.prototype, 'setChartSize', function (proceed) {
524
546
  proceed.apply(this, [].slice.call(arguments, 1));
@@ -851,20 +873,21 @@ Highcharts.wrap(Highcharts.seriesTypes.column.prototype, 'translate', function (
851
873
  z += (seriesOptions.groupZPadding || 1);
852
874
 
853
875
  Highcharts.each(series.data, function (point) {
854
- var shapeArgs = point.shapeArgs,
855
- tooltipPos = point.tooltipPos;
856
-
857
- point.shapeType = 'cuboid';
858
- shapeArgs.alpha = alpha;
859
- shapeArgs.beta = beta;
860
- shapeArgs.z = z;
861
- shapeArgs.origin = origin;
862
- shapeArgs.depth = depth;
876
+ if (point.y !== null) {
877
+ var shapeArgs = point.shapeArgs,
878
+ tooltipPos = point.tooltipPos;
863
879
 
864
- // Translate the tooltip position in 3d space
865
- tooltipPos = perspective([{ x: tooltipPos[0], y: tooltipPos[1], z: z }], alpha, beta, origin)[0];
866
- point.tooltipPos = [tooltipPos.x, tooltipPos.y];
880
+ point.shapeType = 'cuboid';
881
+ shapeArgs.alpha = alpha;
882
+ shapeArgs.beta = beta;
883
+ shapeArgs.z = z;
884
+ shapeArgs.origin = origin;
885
+ shapeArgs.depth = depth;
867
886
 
887
+ // Translate the tooltip position in 3d space
888
+ tooltipPos = perspective([{ x: tooltipPos[0], y: tooltipPos[1], z: z }], alpha, beta, origin)[0];
889
+ point.tooltipPos = [tooltipPos.x, tooltipPos.y];
890
+ }
868
891
  });
869
892
  });
870
893
 
@@ -881,25 +904,29 @@ Highcharts.wrap(Highcharts.seriesTypes.column.prototype, 'animate', function (pr
881
904
  if (Highcharts.svg) { // VML is too slow anyway
882
905
  if (init) {
883
906
  Highcharts.each(series.data, function (point) {
884
- point.height = point.shapeArgs.height;
885
- point.shapey = point.shapeArgs.y; //#2968
886
- point.shapeArgs.height = 1;
887
- if (!reversed) {
888
- if (point.stackY) {
889
- point.shapeArgs.y = point.plotY + yAxis.translate(point.stackY);
890
- } else {
891
- point.shapeArgs.y = point.plotY + (point.negative ? -point.height : point.height);
907
+ if (point.y !== null) {
908
+ point.height = point.shapeArgs.height;
909
+ point.shapey = point.shapeArgs.y; //#2968
910
+ point.shapeArgs.height = 1;
911
+ if (!reversed) {
912
+ if (point.stackY) {
913
+ point.shapeArgs.y = point.plotY + yAxis.translate(point.stackY);
914
+ } else {
915
+ point.shapeArgs.y = point.plotY + (point.negative ? -point.height : point.height);
916
+ }
892
917
  }
893
918
  }
894
919
  });
895
920
 
896
921
  } else { // run the animation
897
922
  Highcharts.each(series.data, function (point) {
898
- point.shapeArgs.height = point.height;
899
- point.shapeArgs.y = point.shapey; //#2968
900
- // null value do not have a graphic
901
- if (point.graphic) {
902
- point.graphic.animate(point.shapeArgs, series.options.animation);
923
+ if (point.y !== null) {
924
+ point.shapeArgs.height = point.height;
925
+ point.shapeArgs.y = point.shapey; //#2968
926
+ // null value do not have a graphic
927
+ if (point.graphic) {
928
+ point.graphic.animate(point.shapeArgs, series.options.animation);
929
+ }
903
930
  }
904
931
  });
905
932
 
@@ -951,14 +978,16 @@ function draw3DPoints(proceed) {
951
978
  this.borderWidth = options.borderWidth = options.edgeWidth || 1;
952
979
 
953
980
  Highcharts.each(this.data, function (point) {
954
- var pointAttr = point.pointAttr;
981
+ if (point.y !== null) {
982
+ var pointAttr = point.pointAttr;
955
983
 
956
- // Set the border color to the fill color to provide a smooth edge
957
- this.borderColor = Highcharts.pick(options.edgeColor, pointAttr[''].fill);
984
+ // Set the border color to the fill color to provide a smooth edge
985
+ this.borderColor = Highcharts.pick(options.edgeColor, pointAttr[''].fill);
958
986
 
959
- pointAttr[''].stroke = this.borderColor;
960
- pointAttr.hover.stroke = Highcharts.pick(states.hover.edgeColor, this.borderColor);
961
- pointAttr.select.stroke = Highcharts.pick(states.select.edgeColor, this.borderColor);
987
+ pointAttr[''].stroke = this.borderColor;
988
+ pointAttr.hover.stroke = Highcharts.pick(states.hover.edgeColor, this.borderColor);
989
+ pointAttr.select.stroke = Highcharts.pick(states.select.edgeColor, this.borderColor);
990
+ }
962
991
  });
963
992
  }
964
993
 
@@ -1108,7 +1137,8 @@ Highcharts.wrap(Highcharts.seriesTypes.pie.prototype, 'translate', function (pro
1108
1137
  });
1109
1138
 
1110
1139
  Highcharts.wrap(Highcharts.seriesTypes.pie.prototype.pointClass.prototype, 'haloPath', function (proceed) {
1111
- return this.series.chart.is3d() ? [] : proceed.call(this);
1140
+ var args = arguments;
1141
+ return this.series.chart.is3d() ? [] : proceed.call(this, args[1]);
1112
1142
  });
1113
1143
 
1114
1144
  Highcharts.wrap(Highcharts.seriesTypes.pie.prototype, 'drawPoints', function (proceed) {
@@ -1119,6 +1149,7 @@ Highcharts.wrap(Highcharts.seriesTypes.pie.prototype, 'drawPoints', function (pr
1119
1149
 
1120
1150
  // Set the border color to the fill color to provide a smooth edge
1121
1151
  this.borderWidth = options.borderWidth = options.edgeWidth || 1;
1152
+ this.borderColor = options.edgeColor = Highcharts.pick(options.edgeColor, options.borderColor, undefined);
1122
1153
 
1123
1154
  states.hover.borderColor = Highcharts.pick(states.hover.edgeColor, this.borderColor);
1124
1155
  states.hover.borderWidth = Highcharts.pick(states.hover.edgeWidth, this.borderWidth);
@@ -2,7 +2,7 @@
2
2
  // @compilation_level SIMPLE_OPTIMIZATIONS
3
3
 
4
4
  /**
5
- * @license Highcharts JS v4.0.3 (2014-07-03)
5
+ * @license Highcharts JS v4.0.4 (2014-09-02)
6
6
  *
7
7
  * (c) 2009-2014 Torstein Honsi
8
8
  *
@@ -1671,11 +1671,10 @@ seriesTypes.waterfall = extendClass(seriesTypes.column, {
1671
1671
  */
1672
1672
  toYData: function (pt) {
1673
1673
  if (pt.isSum) {
1674
- return "sum";
1674
+ return (pt.x === 0 ? null : "sum"); //#3245 Error when first element is Sum or Intermediate Sum
1675
1675
  } else if (pt.isIntermediateSum) {
1676
- return "intermediateSum";
1676
+ return (pt.x === 0 ? null : "intermediateSum"); //#3245
1677
1677
  }
1678
-
1679
1678
  return pt.y;
1680
1679
  },
1681
1680
 
@@ -2908,7 +2908,7 @@ if (CanvasRenderingContext2D) {
2908
2908
  });
2909
2909
  }
2910
2910
  }/**
2911
- * @license Highcharts JS v4.0.3 (2014-07-03)
2911
+ * @license Highcharts JS v4.0.4 (2014-09-02)
2912
2912
  * CanVGRenderer Extension module
2913
2913
  *
2914
2914
  * (c) 2011-2012 Torstein Honsi, Erik Olsson
@@ -32,6 +32,11 @@
32
32
  * A comma delimited string to be parsed. Related options are startRow, endRow, startColumn
33
33
  * and endColumn to delimit what part of the table is used. The lineDelimiter and
34
34
  * itemDelimiter options define the CSV delimiter formats.
35
+ *
36
+ * - dateFormat: String
37
+ * Which of the predefined date formats in Date.prototype.dateFormats to use to parse date
38
+ * columns, for example "dd/mm/YYYY" or "YYYY-mm-dd". Defaults to a best guess based on
39
+ * what format gives valid dates, and prefers ordered dates.
35
40
  *
36
41
  * - endColumn : Integer
37
42
  * In tabular input data, the first row (indexed by 0) to use. Defaults to the last
@@ -60,7 +65,8 @@
60
65
  * A callback function to access the parsed columns, the two-dimentional input data
61
66
  * array directly, before they are interpreted into series data and categories. See also
62
67
  * the complete callback, that goes in on a later stage where the raw columns are interpreted
63
- * into a Highcharts option structure.
68
+ * into a Highcharts option structure. Return false to stop completion, or call this.complete()
69
+ * to continue async.
64
70
  *
65
71
  * - parseDate : Function
66
72
  * A callback function to parse string representations of dates into JavaScript timestamps.
@@ -69,6 +75,10 @@
69
75
  * - rows : Array<Array<Mixed>>
70
76
  * The same as the columns input option, but defining rows intead of columns.
71
77
  *
78
+ * - seriesMapping : Array<Object>
79
+ * An array containing object with Point property names along with what column id the
80
+ * property should be taken from.
81
+ *
72
82
  * - startColumn : Integer
73
83
  * In tabular input data, the first column (indexed by 0) to use.
74
84
  *
@@ -84,13 +94,23 @@
84
94
  * endRow, startColumn and endColumn to delimit what part of the table is used.
85
95
  */
86
96
 
97
+ /*
98
+ * TODO:
99
+ * - Handle various date formats
100
+ * - http://jsfiddle.net/highcharts/114wejdx/
101
+ * - http://jsfiddle.net/highcharts/ryv67bkq/
102
+ */
103
+
87
104
  // JSLint options:
88
- /*global jQuery */
105
+ /*global jQuery, HighchartsAdapter */
89
106
 
90
107
  (function (Highcharts) { // docs
91
108
 
92
109
  // Utilities
93
- var each = Highcharts.each;
110
+ var each = Highcharts.each,
111
+ inArray = HighchartsAdapter.inArray,
112
+ splat = Highcharts.splat,
113
+ SeriesBuilder;
94
114
 
95
115
 
96
116
  // The Data constructor
@@ -109,6 +129,12 @@
109
129
  this.chartOptions = chartOptions;
110
130
  this.columns = options.columns || this.rowsToColumns(options.rows) || [];
111
131
 
132
+ // This is a two-dimensional array holding the raw, trimmed string values
133
+ // with the same organisation as the columns array. It makes it possible
134
+ // for example to revert from interpreted timestamps to string-based
135
+ // categories.
136
+ this.rawColumns = [];
137
+
112
138
  // No need to parse or interpret anything
113
139
  if (this.columns.length) {
114
140
  this.dataFound();
@@ -135,19 +161,79 @@
135
161
  */
136
162
  getColumnDistribution: function () {
137
163
  var chartOptions = this.chartOptions,
164
+ options = this.options,
165
+ xColumns = [],
138
166
  getValueCount = function (type) {
139
167
  return (Highcharts.seriesTypes[type || 'line'].prototype.pointArrayMap || [0]).length;
140
168
  },
169
+ getPointArrayMap = function (type) {
170
+ return Highcharts.seriesTypes[type || 'line'].prototype.pointArrayMap;
171
+ },
141
172
  globalType = chartOptions && chartOptions.chart && chartOptions.chart.type,
142
- individualCounts = [];
173
+ individualCounts = [],
174
+ seriesBuilders = [],
175
+ seriesIndex,
176
+ i;
143
177
 
144
178
  each((chartOptions && chartOptions.series) || [], function (series) {
145
179
  individualCounts.push(getValueCount(series.type || globalType));
146
180
  });
147
181
 
182
+ // Collect the x-column indexes from seriesMapping
183
+ each((options && options.seriesMapping) || [], function (mapping) {
184
+ xColumns.push(mapping.x || 0);
185
+ });
186
+
187
+ // If there are no defined series with x-columns, use the first column as x column
188
+ if (xColumns.length === 0) {
189
+ xColumns.push(0);
190
+ }
191
+
192
+ // Loop all seriesMappings and constructs SeriesBuilders from
193
+ // the mapping options.
194
+ each((options && options.seriesMapping) || [], function (mapping) {
195
+ var builder = new SeriesBuilder(),
196
+ name,
197
+ numberOfValueColumnsNeeded = individualCounts[seriesIndex] || getValueCount(globalType),
198
+ seriesArr = (chartOptions && chartOptions.series) || [],
199
+ series = seriesArr[seriesIndex] || {},
200
+ pointArrayMap = getPointArrayMap(series.type || globalType) || ['y'];
201
+
202
+ // Add an x reader from the x property or from an undefined column
203
+ // if the property is not set. It will then be auto populated later.
204
+ builder.addColumnReader(mapping.x, 'x');
205
+
206
+ // Add all column mappings
207
+ for (name in mapping) {
208
+ if (mapping.hasOwnProperty(name) && name !== 'x') {
209
+ builder.addColumnReader(mapping[name], name);
210
+ }
211
+ }
212
+
213
+ // Add missing columns
214
+ for (i = 0; i < numberOfValueColumnsNeeded; i++) {
215
+ if (!builder.hasReader(pointArrayMap[i])) {
216
+ //builder.addNextColumnReader(pointArrayMap[i]);
217
+ // Create and add a column reader for the next free column index
218
+ builder.addColumnReader(undefined, pointArrayMap[i]);
219
+ }
220
+ }
221
+
222
+ seriesBuilders.push(builder);
223
+ seriesIndex++;
224
+ });
225
+
226
+ var globalPointArrayMap = getPointArrayMap(globalType);
227
+ if (globalPointArrayMap === undefined) {
228
+ globalPointArrayMap = ['y'];
229
+ }
230
+
148
231
  this.valueCount = {
149
232
  global: getValueCount(globalType),
150
- individual: individualCounts
233
+ xColumns: xColumns,
234
+ individual: individualCounts,
235
+ seriesBuilders: seriesBuilders,
236
+ globalPointArrayMap: globalPointArrayMap
151
237
  };
152
238
  },
153
239
 
@@ -161,6 +247,9 @@
161
247
  this.columns = this.rowsToColumns(this.columns);
162
248
  }
163
249
 
250
+ // Interpret the info about series and columns
251
+ this.getColumnDistribution();
252
+
164
253
  // Interpret the values into right types
165
254
  this.parseTypes();
166
255
 
@@ -168,10 +257,11 @@
168
257
  this.findHeaderRow();
169
258
 
170
259
  // Handle columns if a handleColumns callback is given
171
- this.parsed();
260
+ if (this.parsed() !== false) {
172
261
 
173
- // Complete if a complete callback is given
174
- this.complete();
262
+ // Complete if a complete callback is given
263
+ this.complete();
264
+ }
175
265
 
176
266
  },
177
267
 
@@ -338,11 +428,11 @@
338
428
  findHeaderRow: function () {
339
429
  var headerRow = 0;
340
430
  each(this.columns, function (column) {
341
- if (typeof column[0] !== 'string') {
431
+ if (column.isNumeric && typeof column[0] !== 'string') {
342
432
  headerRow = null;
343
433
  }
344
434
  });
345
- this.headerRow = 0;
435
+ this.headerRow = headerRow;
346
436
  },
347
437
 
348
438
  /**
@@ -357,22 +447,37 @@
357
447
  */
358
448
  parseTypes: function () {
359
449
  var columns = this.columns,
450
+ rawColumns = this.rawColumns,
360
451
  col = columns.length,
361
452
  row,
362
453
  val,
363
454
  floatVal,
364
455
  trimVal,
365
- dateVal;
366
-
456
+ isXColumn,
457
+ dateVal,
458
+ descending,
459
+ backup = [],
460
+ diff,
461
+ hasHeaderRow,
462
+ forceCategory,
463
+ chartOptions = this.chartOptions;
464
+
367
465
  while (col--) {
368
466
  row = columns[col].length;
467
+ rawColumns[col] = [];
468
+ isXColumn = inArray(col, this.valueCount.xColumns) !== -1;
469
+ forceCategory = isXColumn && chartOptions && chartOptions.xAxis && splat(chartOptions.xAxis)[0].type === 'category';
369
470
  while (row--) {
370
- val = columns[col][row];
471
+ val = backup[row] || columns[col][row];
371
472
  floatVal = parseFloat(val);
372
- trimVal = this.trim(val);
473
+ trimVal = rawColumns[col][row] = this.trim(val);
474
+
475
+ // Disable number or date parsing by setting the X axis type to category
476
+ if (forceCategory) {
477
+ columns[col][row] = trimVal;
373
478
 
374
479
  /*jslint eqeq: true*/
375
- if (trimVal == floatVal) { // is numeric
480
+ } else if (trimVal == floatVal) { // is numeric
376
481
  /*jslint eqeq: false*/
377
482
  columns[col][row] = floatVal;
378
483
 
@@ -385,16 +490,56 @@
385
490
 
386
491
  } else { // string, continue to determine if it is a date string or really a string
387
492
  dateVal = this.parseDate(val);
388
-
389
- if (col === 0 && typeof dateVal === 'number' && !isNaN(dateVal)) { // is date
493
+ // Only allow parsing of dates if this column is an x-column
494
+ if (isXColumn && typeof dateVal === 'number' && !isNaN(dateVal)) { // is date
495
+ backup[row] = val;
390
496
  columns[col][row] = dateVal;
391
497
  columns[col].isDatetime = true;
498
+
499
+ // Check if the dates are uniformly descending or ascending. If they
500
+ // are not, chances are that they are a different time format, so check
501
+ // for alternative.
502
+ if (columns[col][row + 1] !== undefined) {
503
+ diff = dateVal > columns[col][row + 1];
504
+ if (diff !== descending && descending !== undefined) {
505
+ if (this.alternativeFormat) {
506
+ this.dateFormat = this.alternativeFormat;
507
+ row = columns[col].length;
508
+ this.alternativeFormat = this.dateFormats[this.dateFormat].alternative;
509
+ } else {
510
+ columns[col].unsorted = true;
511
+ }
512
+ }
513
+ descending = diff;
514
+ }
392
515
 
393
516
  } else { // string
394
517
  columns[col][row] = trimVal === '' ? null : trimVal;
518
+ if (row !== 0 && (columns[col].isDatetime || columns[col].isNumeric)) {
519
+ columns[col].mixed = true;
520
+ }
395
521
  }
396
522
  }
397
-
523
+ }
524
+
525
+ // If strings are intermixed with numbers or dates in a parsed column, it is an indication
526
+ // that parsing went wrong or the data was not intended to display as numbers or dates and
527
+ // parsing is too aggressive. Fall back to categories. Demonstrated in the
528
+ // highcharts/demo/column-drilldown sample.
529
+ if (isXColumn && columns[col].mixed) {
530
+ columns[col] = rawColumns[col];
531
+ }
532
+ }
533
+
534
+ // If the 0 column is date and descending, reverse all columns.
535
+ // TODO: probably this should apply to xColumns, not 0 column alone.
536
+ if (columns[0].isDatetime && descending) {
537
+ hasHeaderRow = typeof columns[0][0] !== 'number';
538
+ for (col = 0; col < columns.length; col++) {
539
+ columns[col].reverse();
540
+ if (hasHeaderRow) {
541
+ columns[col].unshift(columns[col].pop());
542
+ }
398
543
  }
399
544
  }
400
545
  },
@@ -405,10 +550,37 @@
405
550
  */
406
551
  dateFormats: {
407
552
  'YYYY-mm-dd': {
408
- regex: '^([0-9]{4})-([0-9]{2})-([0-9]{2})$',
553
+ regex: /^([0-9]{4})[\-\/\.]([0-9]{2})[\-\/\.]([0-9]{2})$/,
409
554
  parser: function (match) {
410
555
  return Date.UTC(+match[1], match[2] - 1, +match[3]);
411
556
  }
557
+ },
558
+ 'dd/mm/YYYY': {
559
+ regex: /^([0-9]{1,2})[\-\/\.]([0-9]{1,2})[\-\/\.]([0-9]{4})$/,
560
+ parser: function (match) {
561
+ return Date.UTC(+match[3], match[2] - 1, +match[1]);
562
+ },
563
+ alternative: 'mm/dd/YYYY' // different format with the same regex
564
+ },
565
+ 'mm/dd/YYYY': {
566
+ regex: /^([0-9]{1,2})[\-\/\.]([0-9]{1,2})[\-\/\.]([0-9]{4})$/,
567
+ parser: function (match) {
568
+ return Date.UTC(+match[3], match[1] - 1, +match[2]);
569
+ }
570
+ },
571
+ 'dd/mm/YY': {
572
+ regex: /^([0-9]{1,2})[\-\/\.]([0-9]{1,2})[\-\/\.]([0-9]{2})$/,
573
+ parser: function (match) {
574
+ return Date.UTC(+match[3] + 2000, match[2] - 1, +match[1]);
575
+ },
576
+ alternative: 'mm/dd/YY' // different format with the same regex
577
+ },
578
+ 'mm/dd/YY': {
579
+ regex: /^([0-9]{1,2})[\-\/\.]([0-9]{1,2})[\-\/\.]([0-9]{2})$/,
580
+ parser: function (match) {
581
+ console.log(match)
582
+ return Date.UTC(+match[3] + 2000, match[1] - 1, +match[2]);
583
+ }
412
584
  }
413
585
  },
414
586
 
@@ -420,20 +592,47 @@
420
592
  ret,
421
593
  key,
422
594
  format,
595
+ dateFormat = this.options.dateFormat || this.dateFormat,
423
596
  match;
424
597
 
425
598
  if (parseDate) {
426
599
  ret = parseDate(val);
427
600
  }
428
-
601
+
429
602
  if (typeof val === 'string') {
430
- for (key in this.dateFormats) {
431
- format = this.dateFormats[key];
603
+ // Auto-detect the date format the first time
604
+ if (!dateFormat) {
605
+ for (key in this.dateFormats) {
606
+ format = this.dateFormats[key];
607
+ match = val.match(format.regex);
608
+ if (match) {
609
+ this.dateFormat = dateFormat = key;
610
+ this.alternativeFormat = format.alternative;
611
+ ret = format.parser(match);
612
+ break;
613
+ }
614
+ }
615
+ // Next time, use the one previously found
616
+ } else {
617
+ format = this.dateFormats[dateFormat];
432
618
  match = val.match(format.regex);
433
619
  if (match) {
434
620
  ret = format.parser(match);
435
621
  }
436
622
  }
623
+ // Fall back to Date.parse
624
+ if (!match) {
625
+ match = Date.parse(val);
626
+ // External tools like Date.js and MooTools extend Date object and
627
+ // returns a date.
628
+ if (typeof match === 'object' && match !== null && match.getTime) {
629
+ ret = match.getTime() - match.getTimezoneOffset() * 60000;
630
+
631
+ // Timestamp
632
+ } else if (typeof match === 'number' && !isNaN(match)) {
633
+ ret = match - (new Date(match)).getTimezoneOffset() * 60000;
634
+ }
635
+ }
437
636
  }
438
637
  return ret;
439
638
  },
@@ -469,9 +668,40 @@
469
668
  */
470
669
  parsed: function () {
471
670
  if (this.options.parsed) {
472
- this.options.parsed.call(this, this.columns);
671
+ return this.options.parsed.call(this, this.columns);
473
672
  }
474
673
  },
674
+
675
+ getFreeIndexes: function (numberOfColumns, seriesBuilders) {
676
+ var s,
677
+ i,
678
+ freeIndexes = [],
679
+ freeIndexValues = [],
680
+ referencedIndexes;
681
+
682
+ // Add all columns as free
683
+ for (i = 0; i < numberOfColumns; i = i + 1) {
684
+ freeIndexes.push(true);
685
+ }
686
+
687
+ // Loop all defined builders and remove their referenced columns
688
+ for (s = 0; s < seriesBuilders.length; s = s + 1) {
689
+ referencedIndexes = seriesBuilders[s].getReferencedColumnIndexes();
690
+
691
+ for (i = 0; i < referencedIndexes.length; i = i + 1) {
692
+ freeIndexes[referencedIndexes[i]] = false;
693
+ }
694
+ }
695
+
696
+ // Collect the values for the free indexes
697
+ for (i = 0; i < freeIndexes.length; i = i + 1) {
698
+ if (freeIndexes[i]) {
699
+ freeIndexValues.push(i);
700
+ }
701
+ }
702
+
703
+ return freeIndexValues;
704
+ },
475
705
 
476
706
  /**
477
707
  * If a complete callback function is provided in the options, interpret the
@@ -480,36 +710,24 @@
480
710
  complete: function () {
481
711
 
482
712
  var columns = this.columns,
483
- firstCol,
713
+ xColumns = [],
484
714
  type,
485
715
  options = this.options,
486
- valueCount,
487
716
  series,
488
717
  data,
489
718
  i,
490
719
  j,
720
+ r,
491
721
  seriesIndex,
492
- chartOptions;
493
-
494
-
495
- if (options.complete || options.afterComplete) {
722
+ chartOptions,
723
+ allSeriesBuilders = [],
724
+ builder,
725
+ freeIndexes,
726
+ typeCol,
727
+ index;
496
728
 
497
- this.getColumnDistribution();
498
-
499
- // Use first column for X data or categories?
500
- if (columns.length > 1) {
501
- firstCol = columns.shift();
502
- if (this.headerRow === 0) {
503
- firstCol.shift(); // remove the first cell
504
- }
505
-
506
-
507
- if (firstCol.isDatetime) {
508
- type = 'datetime';
509
- } else if (!firstCol.isNumeric) {
510
- type = 'category';
511
- }
512
- }
729
+ xColumns.length = columns.length;
730
+ if (options.complete || options.afterComplete) {
513
731
 
514
732
  // Get the names and shift the top row
515
733
  for (i = 0; i < columns.length; i++) {
@@ -520,46 +738,84 @@
520
738
 
521
739
  // Use the next columns for series
522
740
  series = [];
523
- for (i = 0, seriesIndex = 0; i < columns.length; seriesIndex++) {
741
+ freeIndexes = this.getFreeIndexes(columns.length, this.valueCount.seriesBuilders);
742
+
743
+ // Populate defined series
744
+ for (seriesIndex = 0; seriesIndex < this.valueCount.seriesBuilders.length; seriesIndex++) {
745
+ builder = this.valueCount.seriesBuilders[seriesIndex];
524
746
 
525
- // This series' value count
526
- valueCount = Highcharts.pick(this.valueCount.individual[seriesIndex], this.valueCount.global);
747
+ // If the builder can be populated with remaining columns, then add it to allBuilders
748
+ if (builder.populateColumns(freeIndexes)) {
749
+ allSeriesBuilders.push(builder);
750
+ }
751
+ }
752
+
753
+ // Populate dynamic series
754
+ while (freeIndexes.length > 0) {
755
+ builder = new SeriesBuilder();
756
+ builder.addColumnReader(0, 'x');
527
757
 
528
- // Iterate down the cells of each column and add data to the series
529
- data = [];
758
+ // Mark index as used (not free)
759
+ index = inArray(0, freeIndexes);
760
+ if (index !== -1) {
761
+ freeIndexes.splice(index, 1);
762
+ }
530
763
 
531
- // Only loop and fill the data series if there are columns available.
532
- // We need this check to avoid reading outside the array bounds.
533
- if (i + valueCount <= columns.length) {
534
- for (j = 0; j < columns[i].length; j++) {
535
- data[j] = [
536
- firstCol[j],
537
- columns[i][j] !== undefined ? columns[i][j] : null
538
- ];
539
- if (valueCount > 1) {
540
- data[j].push(columns[i + 1][j] !== undefined ? columns[i + 1][j] : null);
541
- }
542
- if (valueCount > 2) {
543
- data[j].push(columns[i + 2][j] !== undefined ? columns[i + 2][j] : null);
544
- }
545
- if (valueCount > 3) {
546
- data[j].push(columns[i + 3][j] !== undefined ? columns[i + 3][j] : null);
547
- }
548
- if (valueCount > 4) {
549
- data[j].push(columns[i + 4][j] !== undefined ? columns[i + 4][j] : null);
764
+ for (i = 0; i < this.valueCount.global; i++) {
765
+ // Create and add a column reader for the next free column index
766
+ builder.addColumnReader(undefined, this.valueCount.globalPointArrayMap[i]);
767
+ }
768
+
769
+ // If the builder can be populated with remaining columns, then add it to allBuilders
770
+ if (builder.populateColumns(freeIndexes)) {
771
+ allSeriesBuilders.push(builder);
772
+ }
773
+ }
774
+
775
+ // Get the data-type from the first series x column
776
+ if (allSeriesBuilders.length > 0 && allSeriesBuilders[0].readers.length > 0) {
777
+ typeCol = columns[allSeriesBuilders[0].readers[0].columnIndex];
778
+ if (typeCol !== undefined) {
779
+ if (typeCol.isDatetime) {
780
+ type = 'datetime';
781
+ } else if (!typeCol.isNumeric) {
782
+ type = 'category';
783
+ }
784
+ }
785
+ }
786
+ // Axis type is category, then the "x" column should be called "name"
787
+ if (type === 'category') {
788
+ for (seriesIndex = 0; seriesIndex < allSeriesBuilders.length; seriesIndex++) {
789
+ builder = allSeriesBuilders[seriesIndex];
790
+ for (r = 0; r < builder.readers.length; r++) {
791
+ if (builder.readers[r].configName === 'x') {
792
+ builder.readers[r].configName = 'name';
550
793
  }
551
794
  }
552
795
  }
796
+ }
797
+
798
+ // Read data for all builders
799
+ for (seriesIndex = 0; seriesIndex < allSeriesBuilders.length; seriesIndex++) {
800
+ builder = allSeriesBuilders[seriesIndex];
801
+
802
+ // Iterate down the cells of each column and add data to the series
803
+ data = [];
804
+ for (j = 0; j < columns[0].length; j++) { // TODO: which column's length should we use here
805
+ data[j] = builder.read(columns, j);
806
+ }
553
807
 
554
808
  // Add the series
555
809
  series[seriesIndex] = {
556
- name: columns[i].name,
557
810
  data: data
558
811
  };
559
-
560
- i += valueCount;
812
+ if (builder.name) {
813
+ series[seriesIndex].name = builder.name;
814
+ }
561
815
  }
562
-
816
+
817
+
818
+
563
819
  // Do the callback
564
820
  chartOptions = {
565
821
  xAxis: {
@@ -570,6 +826,7 @@
570
826
  if (options.complete) {
571
827
  options.complete(chartOptions);
572
828
  }
829
+
573
830
  // The afterComplete hook is used internally to avoid conflict with the externally
574
831
  // available complete option.
575
832
  if (options.afterComplete) {
@@ -592,6 +849,7 @@
592
849
 
593
850
  if (userOptions && userOptions.data) {
594
851
  Highcharts.data(Highcharts.extend(userOptions.data, {
852
+
595
853
  afterComplete: function (dataOptions) {
596
854
  var i, series;
597
855
 
@@ -619,4 +877,149 @@
619
877
  }
620
878
  });
621
879
 
880
+ /**
881
+ * Creates a new SeriesBuilder. A SeriesBuilder consists of a number
882
+ * of ColumnReaders that reads columns and give them a name.
883
+ * Ex: A series builder can be constructed to read column 3 as 'x' and
884
+ * column 7 and 8 as 'y1' and 'y2'.
885
+ * The output would then be points/rows of the form {x: 11, y1: 22, y2: 33}
886
+ *
887
+ * The name of the builder is taken from the second column. In the above
888
+ * example it would be the column with index 7.
889
+ * @constructor
890
+ */
891
+ SeriesBuilder = function () {
892
+ this.readers = [];
893
+ this.pointIsArray = true;
894
+ };
895
+
896
+ /**
897
+ * Populates readers with column indexes. A reader can be added without
898
+ * a specific index and for those readers the index is taken sequentially
899
+ * from the free columns (this is handled by the ColumnCursor instance).
900
+ * @returns {boolean}
901
+ */
902
+ SeriesBuilder.prototype.populateColumns = function (freeIndexes) {
903
+ var builder = this,
904
+ enoughColumns = true;
905
+
906
+ // Loop each reader and give it an index if its missing.
907
+ // The freeIndexes.shift() will return undefined if there
908
+ // are no more columns.
909
+ each(builder.readers, function (reader) {
910
+ if (reader.columnIndex === undefined) {
911
+ reader.columnIndex = freeIndexes.shift();
912
+ }
913
+ });
914
+
915
+ // Now, all readers should have columns mapped. If not
916
+ // then return false to signal that this series should
917
+ // not be added.
918
+ each(builder.readers, function (reader) {
919
+ if (reader.columnIndex === undefined) {
920
+ enoughColumns = false;
921
+ }
922
+ });
923
+
924
+ return enoughColumns;
925
+ };
926
+
927
+ /**
928
+ * Reads a row from the dataset and returns a point or array depending
929
+ * on the names of the readers.
930
+ * @param columns
931
+ * @param rowIndex
932
+ * @returns {Array | Object}
933
+ */
934
+ SeriesBuilder.prototype.read = function (columns, rowIndex) {
935
+ var builder = this,
936
+ pointIsArray = builder.pointIsArray,
937
+ point = pointIsArray ? [] : {},
938
+ columnIndexes;
939
+
940
+ // Loop each reader and ask it to read its value.
941
+ // Then, build an array or point based on the readers names.
942
+ each(builder.readers, function (reader) {
943
+ var value = columns[reader.columnIndex][rowIndex];
944
+ if (pointIsArray) {
945
+ point.push(value);
946
+ } else {
947
+ point[reader.configName] = value;
948
+ }
949
+ });
950
+
951
+ // The name comes from the first column (excluding the x column)
952
+ if (this.name === undefined && builder.readers.length >= 2) {
953
+ columnIndexes = builder.getReferencedColumnIndexes();
954
+ if (columnIndexes.length >= 2) {
955
+ // remove the first one (x col)
956
+ columnIndexes.shift();
957
+
958
+ // Sort the remaining
959
+ columnIndexes.sort();
960
+
961
+ // Now use the lowest index as name column
962
+ this.name = columns[columnIndexes.shift()].name;
963
+ }
964
+ }
965
+
966
+ return point;
967
+ };
968
+
969
+ /**
970
+ * Creates and adds ColumnReader from the given columnIndex and configName.
971
+ * ColumnIndex can be undefined and in that case the reader will be given
972
+ * an index when columns are populated.
973
+ * @param columnIndex {Number | undefined}
974
+ * @param configName
975
+ */
976
+ SeriesBuilder.prototype.addColumnReader = function (columnIndex, configName) {
977
+ this.readers.push({
978
+ columnIndex: columnIndex,
979
+ configName: configName
980
+ });
981
+
982
+ if (!(configName === 'x' || configName === 'y' || configName === undefined)) {
983
+ this.pointIsArray = false;
984
+ }
985
+ };
986
+
987
+ /**
988
+ * Returns an array of column indexes that the builder will use when
989
+ * reading data.
990
+ * @returns {Array}
991
+ */
992
+ SeriesBuilder.prototype.getReferencedColumnIndexes = function () {
993
+ var i,
994
+ referencedColumnIndexes = [],
995
+ columnReader;
996
+
997
+ for (i = 0; i < this.readers.length; i = i + 1) {
998
+ columnReader = this.readers[i];
999
+ if (columnReader.columnIndex !== undefined) {
1000
+ referencedColumnIndexes.push(columnReader.columnIndex);
1001
+ }
1002
+ }
1003
+
1004
+ return referencedColumnIndexes;
1005
+ };
1006
+
1007
+ /**
1008
+ * Returns true if the builder has a reader for the given configName.
1009
+ * @param configName
1010
+ * @returns {boolean}
1011
+ */
1012
+ SeriesBuilder.prototype.hasReader = function (configName) {
1013
+ var i, columnReader;
1014
+ for (i = 0; i < this.readers.length; i = i + 1) {
1015
+ columnReader = this.readers[i];
1016
+ if (columnReader.configName === configName) {
1017
+ return true;
1018
+ }
1019
+ }
1020
+ // Else return undefined
1021
+ };
1022
+
1023
+
1024
+
622
1025
  }(Highcharts));