highcharts-rails 4.0.3 → 4.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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));