highcharts-rails 2.2.0 → 2.2.1

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.
@@ -0,0 +1,4 @@
1
+ ## 2.2.1 (2012-04-15)
2
+
3
+ * Updated Highcharts to 2.2.1
4
+ * Added the skies theme (graphic hardcoded to `/assets/highcharts/skies.jpg`)
data/README.markdown CHANGED
@@ -7,9 +7,13 @@ Highcharts is not free for commercial use, so make sure you have a [valid licens
7
7
 
8
8
  Add the gem to the Gemfile
9
9
 
10
- gem "highcharts-rails", "~> 2.1.9"
10
+ gem "highcharts-rails", "~> 2.2.1"
11
11
  # The gem version mirrors the included version of Highcharts
12
12
 
13
+ ## Changes
14
+
15
+ Refer to the [Highcharts changelog](http://www.highcharts.com/documentation/changelog#highcharts)
16
+
13
17
  ## Usage
14
18
 
15
19
  In your JavaScript manifest (e.g. `application.js`)
@@ -31,6 +35,7 @@ Or one of the themes
31
35
  //= require highcharts/themes/dark-green
32
36
  //= require highcharts/themes/gray
33
37
  //= require highcharts/themes/grid
38
+ //= require highcharts/themes/skies
34
39
 
35
40
  Other than that, refer to the [Highcharts documentation](http://highcharts.com/documentation/how-to-use)
36
41
 
@@ -1,3 +1,3 @@
1
1
  module Highcharts
2
- VERSION = "2.2.0"
2
+ VERSION = "2.2.1"
3
3
  end
@@ -2,7 +2,7 @@
2
2
  // @compilation_level SIMPLE_OPTIMIZATIONS
3
3
 
4
4
  /**
5
- * @license Highcharts JS v2.2.0 (2012-02-16)
5
+ * @license Highcharts JS v2.2.1 (2012-03-15)
6
6
  *
7
7
  * (c) 2009-2011 Torstein Hønsi
8
8
  *
@@ -368,6 +368,16 @@ function numberFormat(number, decimals, decPoint, thousandsSep) {
368
368
  (c ? d + mathAbs(n - i).toFixed(c).slice(2) : "");
369
369
  }
370
370
 
371
+ /**
372
+ * Pad a string to a given length by adding 0 to the beginning
373
+ * @param {Number} number
374
+ * @param {Number} length
375
+ */
376
+ function pad(number, length) {
377
+ // Create an array of the remaining length +1 and join it with 0's
378
+ return new Array((length || 2) + 1 - String(number).length).join(0) + number;
379
+ }
380
+
371
381
  /**
372
382
  * Based on http://www.php.net/manual/en/function.strftime.php
373
383
  * @param {String} format
@@ -375,16 +385,6 @@ function numberFormat(number, decimals, decPoint, thousandsSep) {
375
385
  * @param {Boolean} capitalize
376
386
  */
377
387
  dateFormat = function (format, timestamp, capitalize) {
378
- function pad(number, length) {
379
- // two digits
380
- number = number.toString().replace(/^([0-9])$/, '0$1');
381
- // three digits
382
- if (length === 3) {
383
- number = number.toString().replace(/^([0-9]{2})$/, '0$1');
384
- }
385
- return number;
386
- }
387
-
388
388
  if (!defined(timestamp) || isNaN(timestamp)) {
389
389
  return 'Invalid date';
390
390
  }
@@ -440,7 +440,7 @@ dateFormat = function (format, timestamp, capitalize) {
440
440
  'p': hours < 12 ? 'AM' : 'PM', // Upper case AM or PM
441
441
  'P': hours < 12 ? 'am' : 'pm', // Lower case AM or PM
442
442
  'S': pad(date.getSeconds()), // Two digits seconds, 00 through 59
443
- 'L': pad(timestamp % 1000, 3) // Milliseconds (naming from Ruby)
443
+ 'L': pad(mathRound(timestamp % 1000), 3) // Milliseconds (naming from Ruby)
444
444
  };
445
445
 
446
446
 
@@ -595,9 +595,10 @@ function getTimeTicks(normalizedInterval, min, max, startOfWeek) {
595
595
  interval = normalizedInterval.unitRange,
596
596
  count = normalizedInterval.count;
597
597
 
598
- minDate.setMilliseconds(0);
598
+
599
599
 
600
600
  if (interval >= timeUnits[SECOND]) { // second
601
+ minDate.setMilliseconds(0);
601
602
  minDate.setSeconds(interval >= timeUnits[MINUTE] ? 0 :
602
603
  count * mathFloor(minDate.getSeconds() / count));
603
604
  }
@@ -1269,7 +1270,7 @@ defaultOptions = {
1269
1270
  },
1270
1271
  global: {
1271
1272
  useUTC: true,
1272
- canvasToolsURL: 'http://code.highcharts.com/2.2.0/modules/canvas-tools.js'
1273
+ canvasToolsURL: 'http://code.highcharts.com/2.2.1/modules/canvas-tools.js'
1273
1274
  },
1274
1275
  chart: {
1275
1276
  //animation: true,
@@ -1386,6 +1387,12 @@ defaultOptions = {
1386
1387
  formatter: function () {
1387
1388
  return this.y;
1388
1389
  }
1390
+ // backgroundColor: undefined,
1391
+ // borderColor: undefined,
1392
+ // borderRadius: undefined,
1393
+ // borderWidth: undefined,
1394
+ // padding: 3,
1395
+ // shadow: false
1389
1396
  }),
1390
1397
  cropThreshold: 300, // draw points outside the plot area when the number of points is less than this
1391
1398
  pointRange: 0,
@@ -1666,7 +1673,8 @@ defaultBottomAxisOptions = { // horizontal axis
1666
1673
  labels: {
1667
1674
  align: 'center',
1668
1675
  x: 0,
1669
- y: 14
1676
+ y: 14,
1677
+ overflow: 'justify' // docs
1670
1678
  // staggerLines: null
1671
1679
  },
1672
1680
  title: {
@@ -1675,7 +1683,8 @@ defaultBottomAxisOptions = { // horizontal axis
1675
1683
  },
1676
1684
  defaultTopAxisOptions = merge(defaultBottomAxisOptions, {
1677
1685
  labels: {
1678
- y: -5
1686
+ y: -5,
1687
+ overflow: 'justify'
1679
1688
  // staggerLines: null
1680
1689
  }
1681
1690
  });
@@ -1741,7 +1750,8 @@ defaultPlotOptions.bar = merge(defaultPlotOptions.column, {
1741
1750
  dataLabels: {
1742
1751
  align: 'left',
1743
1752
  x: 5,
1744
- y: 0
1753
+ y: null,
1754
+ verticalAlign: 'middle'
1745
1755
  }
1746
1756
  });
1747
1757
  defaultPlotOptions.pie = merge(defaultSeriesOptions, {
@@ -2436,6 +2446,7 @@ SVGElement.prototype = {
2436
2446
  }
2437
2447
 
2438
2448
  var wrapper = this,
2449
+ renderer = wrapper.renderer,
2439
2450
  elem = wrapper.element,
2440
2451
  translateX = wrapper.translateX || 0,
2441
2452
  translateY = wrapper.translateY || 0,
@@ -2465,7 +2476,7 @@ SVGElement.prototype = {
2465
2476
  // apply inversion
2466
2477
  if (wrapper.inverted) { // wrapper is a group
2467
2478
  each(elem.childNodes, function (child) {
2468
- wrapper.renderer.invertChild(child, elem);
2479
+ renderer.invertChild(child, elem);
2469
2480
  });
2470
2481
  }
2471
2482
 
@@ -2473,7 +2484,7 @@ SVGElement.prototype = {
2473
2484
 
2474
2485
  var width, height,
2475
2486
  rotation = wrapper.rotation,
2476
- lineHeight,
2487
+ baseline,
2477
2488
  radians = 0,
2478
2489
  costheta = 1,
2479
2490
  sintheta = 0,
@@ -2516,14 +2527,14 @@ SVGElement.prototype = {
2516
2527
  }
2517
2528
 
2518
2529
  // correct x and y
2519
- lineHeight = mathRound((pInt(elem.style.fontSize) || 12) * 1.2);
2530
+ baseline = renderer.fontMetrics(elem.style.fontSize).b;
2520
2531
  xCorr = costheta < 0 && -width;
2521
2532
  yCorr = sintheta < 0 && -height;
2522
2533
 
2523
- // correct for lineHeight and corners spilling out after rotation
2534
+ // correct for baseline and corners spilling out after rotation
2524
2535
  quad = costheta * sintheta < 0;
2525
- xCorr += sintheta * lineHeight * (quad ? 1 - alignCorrection : alignCorrection);
2526
- yCorr -= costheta * lineHeight * (rotation ? (quad ? alignCorrection : 1 - alignCorrection) : 1);
2536
+ xCorr += sintheta * baseline * (quad ? 1 - alignCorrection : alignCorrection);
2537
+ yCorr -= costheta * baseline * (rotation ? (quad ? alignCorrection : 1 - alignCorrection) : 1);
2527
2538
 
2528
2539
  // correct for the length/height of the text
2529
2540
  if (nonLeft) {
@@ -2934,7 +2945,7 @@ SVGRenderer.prototype = {
2934
2945
  renderer.boxWrapper = boxWrapper;
2935
2946
  renderer.alignedObjects = [];
2936
2947
  renderer.url = isIE ? '' : loc.href.replace(/#.*?$/, '')
2937
- .replace(/\(/g, '\\(').replace(/\)/g, '\\)'); // Page url used for internal references. #24, #672.
2948
+ .replace(/([\('\)])/g, '\\$1'); // Page url used for internal references. #24, #672.
2938
2949
  renderer.defs = this.createElement('defs').add();
2939
2950
  renderer.forExport = forExport;
2940
2951
  renderer.gradients = {}; // Object where gradient SvgElements are stored
@@ -3849,6 +3860,23 @@ SVGRenderer.prototype = {
3849
3860
  return wrapper;
3850
3861
  },
3851
3862
 
3863
+ /**
3864
+ * Utility to return the baseline offset and total line height from the font size
3865
+ */
3866
+ fontMetrics: function (fontSize) {
3867
+ fontSize = pInt(fontSize || 11);
3868
+
3869
+ // Empirical values found by comparing font size and bounding box height.
3870
+ // Applies to the default font family. http://jsfiddle.net/highcharts/7xvn7/
3871
+ var lineHeight = fontSize < 24 ? fontSize + 4 : mathRound(fontSize * 1.2),
3872
+ baseline = mathRound(lineHeight * 0.8);
3873
+
3874
+ return {
3875
+ h: lineHeight,
3876
+ b: baseline
3877
+ };
3878
+ },
3879
+
3852
3880
  /**
3853
3881
  * Add a label, a text item that can hold a colored or gradient background
3854
3882
  * as well as a border and shadow.
@@ -3859,8 +3887,10 @@ SVGRenderer.prototype = {
3859
3887
  * @param {Number} anchorX In case the shape has a pointer, like a flag, this is the
3860
3888
  * coordinates it should be pinned to
3861
3889
  * @param {Number} anchorY
3890
+ * @param {Boolean} baseline Whether to position the label relative to the text baseline,
3891
+ * like renderer.text, or to the upper border of the rectangle.
3862
3892
  */
3863
- label: function (str, x, y, shape, anchorX, anchorY, useHTML) {
3893
+ label: function (str, x, y, shape, anchorX, anchorY, useHTML, baseline) {
3864
3894
 
3865
3895
  var renderer = this,
3866
3896
  wrapper = renderer.g(),
@@ -3879,6 +3909,7 @@ SVGRenderer.prototype = {
3879
3909
  wrapperY,
3880
3910
  crispAdjust = 0,
3881
3911
  deferredAttr = {},
3912
+ baselineOffset,
3882
3913
  attrSetters = wrapper.attrSetters;
3883
3914
 
3884
3915
  /**
@@ -3887,16 +3918,25 @@ SVGRenderer.prototype = {
3887
3918
  * box and reflect it in the border box.
3888
3919
  */
3889
3920
  function updateBoxSize() {
3921
+ var boxY,
3922
+ style = text.element.style;
3923
+
3890
3924
  bBox = (width === undefined || height === undefined || wrapper.styles.textAlign) &&
3891
3925
  text.getBBox(true);
3892
3926
  wrapper.width = (width || bBox.width) + 2 * padding;
3893
3927
  wrapper.height = (height || bBox.height) + 2 * padding;
3894
3928
 
3929
+ // update the label-scoped y offset
3930
+ baselineOffset = padding + renderer.fontMetrics(style && style.fontSize).b;
3931
+
3932
+
3895
3933
  // create the border box if it is not already present
3896
3934
  if (!box) {
3935
+ boxY = baseline ? -baselineOffset : 0;
3936
+
3897
3937
  wrapper.box = box = shape ?
3898
- renderer.symbol(shape, 0, 0, wrapper.width, wrapper.height) :
3899
- renderer.rect(0, 0, wrapper.width, wrapper.height, 0, deferredAttr[STROKE_WIDTH]);
3938
+ renderer.symbol(shape, 0, boxY, wrapper.width, wrapper.height) :
3939
+ renderer.rect(0, boxY, wrapper.width, wrapper.height, 0, deferredAttr[STROKE_WIDTH]);
3900
3940
  box.add(wrapper);
3901
3941
  }
3902
3942
 
@@ -3915,8 +3955,10 @@ SVGRenderer.prototype = {
3915
3955
  var styles = wrapper.styles,
3916
3956
  textAlign = styles && styles.textAlign,
3917
3957
  x = padding,
3918
- style = wrapper.element.style,
3919
- y = padding + mathRound(pInt((style && style.fontSize) || 11) * 1.2);
3958
+ y;
3959
+
3960
+ // determin y based on the baseline
3961
+ y = baseline ? 0 : baselineOffset;
3920
3962
 
3921
3963
  // compensate for alignment
3922
3964
  if (defined(width) && (textAlign === 'center' || textAlign === 'right')) {
@@ -3979,8 +4021,10 @@ SVGRenderer.prototype = {
3979
4021
  return false;
3980
4022
  };
3981
4023
  attrSetters.padding = function (value) {
3982
- padding = value;
3983
- updateTextPadding();
4024
+ if (defined(value) && value !== padding) {
4025
+ padding = value;
4026
+ updateTextPadding();
4027
+ }
3984
4028
 
3985
4029
  return false;
3986
4030
  };
@@ -4022,15 +4066,15 @@ SVGRenderer.prototype = {
4022
4066
 
4023
4067
  // rename attributes
4024
4068
  attrSetters.x = function (value) {
4025
- wrapperX = value;
4026
- wrapperX -= { left: 0, center: 0.5, right: 1 }[align] * ((width || bBox.width) + padding);
4069
+ value -= { left: 0, center: 0.5, right: 1 }[align] * ((width || bBox.width) + padding);
4070
+ wrapperX = wrapper.x = mathRound(value); // wrapper.x is for animation getter
4027
4071
 
4028
- wrapper.attr('translateX', mathRound(wrapperX));
4072
+ wrapper.attr('translateX', wrapperX);
4029
4073
  return false;
4030
4074
  };
4031
4075
  attrSetters.y = function (value) {
4032
- wrapperY = value;
4033
- wrapper.attr('translateY', mathRound(value));
4076
+ wrapperY = wrapper.y = mathRound(value);
4077
+ wrapper.attr('translateY', value);
4034
4078
  return false;
4035
4079
  };
4036
4080
 
@@ -4111,7 +4155,7 @@ if (!hasSVG && !useCanVG) {
4111
4155
  /**
4112
4156
  * The VML element wrapper.
4113
4157
  */
4114
- var VMLElementExtension = {
4158
+ var VMLElement = {
4115
4159
 
4116
4160
  /**
4117
4161
  * Initialize a new VML element wrapper. It builds the markup as a string
@@ -4179,7 +4223,7 @@ var VMLElementExtension = {
4179
4223
  // align text after adding to be able to read offset
4180
4224
  wrapper.added = true;
4181
4225
  if (wrapper.alignOnAdd && !wrapper.deferUpdateTransform) {
4182
- wrapper.htmlUpdateTransform();
4226
+ wrapper.updateTransform();
4183
4227
  }
4184
4228
 
4185
4229
  // fire an event for internal hooks
@@ -4208,6 +4252,11 @@ var VMLElementExtension = {
4208
4252
  }
4209
4253
  },
4210
4254
 
4255
+ /**
4256
+ * VML always uses htmlUpdateTransform
4257
+ */
4258
+ updateTransform: SVGElement.prototype.htmlUpdateTransform,
4259
+
4211
4260
  /**
4212
4261
  * Get or set attributes
4213
4262
  */
@@ -4267,7 +4316,6 @@ var VMLElementExtension = {
4267
4316
  // check all the others only once for each call to an element's
4268
4317
  // .attr() method
4269
4318
  if (!hasSetSymbolSize) {
4270
-
4271
4319
  wrapper.symbolAttr(hash);
4272
4320
 
4273
4321
  hasSetSymbolSize = true;
@@ -4397,7 +4445,7 @@ var VMLElementExtension = {
4397
4445
  // translation for animation
4398
4446
  } else if (key === 'translateX' || key === 'translateY' || key === 'rotation') {
4399
4447
  wrapper[key] = value;
4400
- wrapper.htmlUpdateTransform();
4448
+ wrapper.updateTransform();
4401
4449
 
4402
4450
  skipAttr = true;
4403
4451
 
@@ -4563,13 +4611,13 @@ var VMLElementExtension = {
4563
4611
  return this;
4564
4612
 
4565
4613
  }
4566
- },
4567
- VMLElement = extendClass(SVGElement, VMLElementExtension),
4614
+ };
4615
+ VMLElement = extendClass(SVGElement, VMLElement);
4568
4616
 
4569
4617
  /**
4570
4618
  * The VML renderer
4571
4619
  */
4572
- VMLRendererExtension = { // inherit SVGRenderer
4620
+ var VMLRendererExtension = { // inherit SVGRenderer
4573
4621
 
4574
4622
  Element: VMLElement,
4575
4623
  isIE8: userAgent.indexOf('MSIE 8.0') > -1,
@@ -4583,16 +4631,19 @@ VMLRendererExtension = { // inherit SVGRenderer
4583
4631
  */
4584
4632
  init: function (container, width, height) {
4585
4633
  var renderer = this,
4586
- boxWrapper;
4634
+ boxWrapper,
4635
+ box;
4587
4636
 
4588
4637
  renderer.alignedObjects = [];
4589
4638
 
4590
4639
  boxWrapper = renderer.createElement(DIV);
4640
+ box = boxWrapper.element;
4641
+ box.style.position = RELATIVE; // for freeform drawing using renderer directly
4591
4642
  container.appendChild(boxWrapper.element);
4592
4643
 
4593
4644
 
4594
4645
  // generate the containing box
4595
- renderer.box = boxWrapper.element;
4646
+ renderer.box = box;
4596
4647
  renderer.boxWrapper = boxWrapper;
4597
4648
 
4598
4649
 
@@ -4727,7 +4778,7 @@ VMLRendererExtension = { // inherit SVGRenderer
4727
4778
  // are reversed.
4728
4779
  markup = ['<fill colors="0% ', color1, ',100% ', color2, '" angle="', angle,
4729
4780
  '" opacity="', opacity2, '" o:opacity2="', opacity1,
4730
- '" type="gradient" focus="100%" method="any" />'];
4781
+ '" type="gradient" focus="100%" method="sigma" />'];
4731
4782
  createElement(this.prepVML(markup), null, null, elem);
4732
4783
 
4733
4784
  // Gradients are not supported for VML stroke, return the first color. #722.
@@ -4909,13 +4960,12 @@ VMLRendererExtension = { // inherit SVGRenderer
4909
4960
  cosEnd = mathCos(end),
4910
4961
  sinEnd = mathSin(end),
4911
4962
  innerRadius = options.innerR,
4912
- circleCorrection = 0.07 / radius,
4913
- innerCorrection = (innerRadius && 0.1 / innerRadius) || 0;
4963
+ circleCorrection = 0.08 / radius, // #760
4964
+ innerCorrection = (innerRadius && 0.25 / innerRadius) || 0;
4914
4965
 
4915
4966
  if (end - start === 0) { // no angle, don't show it.
4916
4967
  return ['x'];
4917
4968
 
4918
- //} else if (end - start == 2 * mathPI) { // full circle
4919
4969
  } else if (2 * mathPI - end + start < circleCorrection) { // full circle
4920
4970
  // empirical correction found by trying out the limits for different radii
4921
4971
  cosEnd = -circleCorrection;
@@ -5117,13 +5167,14 @@ Renderer = VMLRenderer || CanVGRenderer || SVGRenderer;
5117
5167
  * @param {Object} options
5118
5168
  * @param {Function} callback Function to run when the chart has loaded
5119
5169
  */
5120
- function Chart(options, callback) {
5170
+ function Chart(userOptions, callback) {
5121
5171
 
5122
5172
  // Handle regular options
5123
- var seriesOptions = options.series; // skip merging data points to increase performance
5124
- options.series = null;
5125
- options = merge(defaultOptions, options); // do the merge
5126
- options.series = seriesOptions; // set back the series data
5173
+ var options,
5174
+ seriesOptions = userOptions.series; // skip merging data points to increase performance
5175
+ userOptions.series = null;
5176
+ options = merge(defaultOptions, userOptions); // do the merge
5177
+ options.series = userOptions.series = seriesOptions; // set back the series data
5127
5178
 
5128
5179
  var optionsChart = options.chart,
5129
5180
  optionsMargin = optionsChart.margin,
@@ -5174,7 +5225,6 @@ function Chart(options, callback) {
5174
5225
  plotWidth,
5175
5226
  tracker,
5176
5227
  trackerGroup,
5177
- placeTrackerGroup,
5178
5228
  legend,
5179
5229
  legendWidth,
5180
5230
  legendHeight,
@@ -5403,7 +5453,76 @@ function Chart(options, callback) {
5403
5453
  return label ?
5404
5454
  ((this.labelBBox = label.getBBox()))[horiz ? 'height' : 'width'] :
5405
5455
  0;
5406
- },
5456
+ },
5457
+
5458
+ /**
5459
+ * Find how far the labels extend to the right and left of the tick's x position. Used for anti-collision
5460
+ * detection with overflow logic.
5461
+ */
5462
+ getLabelSides: function () {
5463
+ var bBox = this.labelBBox, // assume getLabelSize has run at this point
5464
+ labelOptions = options.labels,
5465
+ width = bBox.width,
5466
+ leftSide = width * { left: 0, center: 0.5, right: 1 }[labelOptions.align] - labelOptions.x;
5467
+
5468
+ return [-leftSide, width - leftSide];
5469
+ },
5470
+
5471
+ /**
5472
+ * Handle the label overflow by adjusting the labels to the left and right edge, or
5473
+ * hide them if they collide into the neighbour label.
5474
+ */
5475
+ handleOverflow: function (index) {
5476
+ var show = true,
5477
+ isFirst = this.isFirst,
5478
+ isLast = this.isLast,
5479
+ label = this.label,
5480
+ x = label.x;
5481
+
5482
+ if (isFirst || isLast) {
5483
+
5484
+ var sides = this.getLabelSides(),
5485
+ leftSide = sides[0],
5486
+ rightSide = sides[1],
5487
+ plotLeft = chart.plotLeft,
5488
+ plotRight = plotLeft + axis.len,
5489
+ neighbour = ticks[tickPositions[index + (isFirst ? 1 : -1)]],
5490
+ neighbourEdge = neighbour && neighbour.label.x + neighbour.getLabelSides()[isFirst ? 0 : 1];
5491
+
5492
+ if ((isFirst && !reversed) || (isLast && reversed)) {
5493
+ // Is the label spilling out to the left of the plot area?
5494
+ if (x + leftSide < plotLeft) {
5495
+
5496
+ // Align it to plot left
5497
+ x = plotLeft - leftSide;
5498
+
5499
+ // Hide it if it now overlaps the neighbour label
5500
+ if (neighbour && x + rightSide > neighbourEdge) {
5501
+ show = false;
5502
+ }
5503
+ }
5504
+
5505
+ } else {
5506
+ // Is the label spilling out to the right of the plot area?
5507
+ if (x + rightSide > plotRight) {
5508
+
5509
+ // Align it to plot right
5510
+ x = plotRight - rightSide;
5511
+
5512
+ // Hide it if it now overlaps the neighbour label
5513
+ if (neighbour && x + leftSide < neighbourEdge) {
5514
+ show = false;
5515
+ }
5516
+
5517
+ }
5518
+ }
5519
+
5520
+ // Set the modified x position of the label
5521
+ label.x = x;
5522
+ }
5523
+ return show;
5524
+ },
5525
+
5407
5526
  /**
5408
5527
  * Put everything in place
5409
5528
  *
@@ -5432,6 +5551,7 @@ function Chart(options, callback) {
5432
5551
  step = labelOptions.step,
5433
5552
  cHeight = (old && oldChartHeight) || chartHeight,
5434
5553
  attribs,
5554
+ show = true,
5435
5555
  x,
5436
5556
  y;
5437
5557
 
@@ -5527,29 +5647,42 @@ function Chart(options, callback) {
5527
5647
  y += (index / (step || 1) % staggerLines) * 16;
5528
5648
  }
5529
5649
 
5650
+ // Cache x and y to be able to read final position before animation
5651
+ label.x = x;
5652
+ label.y = y;
5653
+
5530
5654
  // apply show first and show last
5531
5655
  if ((tick.isFirst && !pick(options.showFirstLabel, 1)) ||
5532
5656
  (tick.isLast && !pick(options.showLastLabel, 1))) {
5533
- label.hide();
5534
- } else {
5535
- // show those that may have been previously hidden, either by show first/last, or by step
5536
- label.show();
5657
+ show = false;
5658
+
5659
+ // Handle label overflow and show or hide accordingly
5660
+ } else if (!staggerLines && horiz && labelOptions.overflow === 'justify' && !tick.handleOverflow(index)) {
5661
+ show = false;
5537
5662
  }
5538
5663
 
5539
5664
  // apply step
5540
5665
  if (step && index % step) {
5541
5666
  // show those indices dividable by step
5542
- label.hide();
5667
+ show = false;
5543
5668
  }
5544
5669
 
5545
- label[tick.isNew ? 'attr' : 'animate']({
5546
- x: x,
5547
- y: y
5548
- });
5670
+ // Set the new position, and show or hide
5671
+ if (show) {
5672
+ label[tick.isNew ? 'attr' : 'animate']({
5673
+ x: label.x,
5674
+ y: label.y
5675
+ });
5676
+ label.show();
5677
+ tick.isNew = false;
5678
+ } else {
5679
+ label.hide();
5680
+ }
5549
5681
  }
5550
5682
 
5551
- tick.isNew = false;
5683
+
5552
5684
  },
5685
+
5553
5686
  /**
5554
5687
  * Destructor for the tick prototype
5555
5688
  */
@@ -5995,7 +6128,7 @@ function Chart(options, callback) {
5995
6128
  }
5996
6129
 
5997
6130
  // Adjust to threshold
5998
- if (threshold !== null) {
6131
+ if (defined(threshold)) {
5999
6132
  if (dataMin >= threshold) {
6000
6133
  dataMin = threshold;
6001
6134
  ignoreMinPadding = true;
@@ -6349,8 +6482,8 @@ function Chart(options, callback) {
6349
6482
  }
6350
6483
 
6351
6484
  if (isLog) {
6352
- if (!secondPass && min <= 0) {
6353
- error(10); // Can't plot negative values on log axis
6485
+ if (!secondPass && mathMin(min, dataMin) <= 0) {
6486
+ error(10, 1); // Can't plot negative values on log axis
6354
6487
  }
6355
6488
  min = log2lin(min);
6356
6489
  max = log2lin(max);
@@ -6450,12 +6583,6 @@ function Chart(options, callback) {
6450
6583
  }
6451
6584
  }
6452
6585
 
6453
- // post process positions, used in ordinal axes in Highstock.
6454
- // TODO: combine with getNonLinearTimeTicks
6455
- fireEvent(axis, 'afterSetTickPositions', {
6456
- tickPositions: tickPositions
6457
- });
6458
-
6459
6586
  if (!isLinked) {
6460
6587
 
6461
6588
  // reset min/max or remove extremes based on start/end on tick
@@ -6526,7 +6653,8 @@ function Chart(options, callback) {
6526
6653
  function setScale() {
6527
6654
  var type,
6528
6655
  i,
6529
- isDirtyData;
6656
+ isDirtyData,
6657
+ isDirtyAxisLength;
6530
6658
 
6531
6659
  oldMin = min;
6532
6660
  oldMax = max;
@@ -6534,6 +6662,7 @@ function Chart(options, callback) {
6534
6662
 
6535
6663
  // set the new axisLength
6536
6664
  axisLength = horiz ? axisWidth : axisHeight;
6665
+ isDirtyAxisLength = axisLength !== oldAxisLength;
6537
6666
 
6538
6667
  // is there new data?
6539
6668
  each(axis.series, function (series) {
@@ -6544,7 +6673,7 @@ function Chart(options, callback) {
6544
6673
  });
6545
6674
 
6546
6675
  // do we really need to go through all this?
6547
- if (axisLength !== oldAxisLength || isDirtyData || isLinked ||
6676
+ if (isDirtyAxisLength || isDirtyData || isLinked ||
6548
6677
  userMin !== oldUserMin || userMax !== oldUserMax) {
6549
6678
 
6550
6679
  // get data extremes if needed
@@ -6568,7 +6697,7 @@ function Chart(options, callback) {
6568
6697
 
6569
6698
  // Mark as dirty if it is not already set to dirty and extremes have changed. #595.
6570
6699
  if (!axis.isDirty) {
6571
- axis.isDirty = chart.isDirtyBox || min !== oldMin || max !== oldMax;
6700
+ axis.isDirty = isDirtyAxisLength || min !== oldMin || max !== oldMax;
6572
6701
  }
6573
6702
  }
6574
6703
  }
@@ -6580,20 +6709,28 @@ function Chart(options, callback) {
6580
6709
  * @param {Boolean} redraw
6581
6710
  * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
6582
6711
  * configuration
6712
+ * @param {Object} eventArguments
6583
6713
  *
6584
6714
  */
6585
- function setExtremes(newMin, newMax, redraw, animation) {
6715
+ function setExtremes(newMin, newMax, redraw, animation, eventArguments) {
6586
6716
 
6587
6717
  redraw = pick(redraw, true); // defaults to true
6588
6718
 
6589
- fireEvent(axis, 'setExtremes', { // fire an event to enable syncing of multiple charts
6719
+ // Extend the arguments with min and max
6720
+ eventArguments = extend(eventArguments, {
6590
6721
  min: newMin,
6591
6722
  max: newMax
6592
- }, function () { // the default event handler
6723
+ });
6724
+
6725
+ // Fire the event
6726
+ fireEvent(axis, 'setExtremes', eventArguments, function () { // the default event handler
6593
6727
 
6594
6728
  userMin = newMin;
6595
6729
  userMax = newMax;
6596
6730
 
6731
+ // Mark for running afterSetExtremes
6732
+ axis.isDirtyExtremes = true;
6733
+
6597
6734
  // redraw
6598
6735
  if (redraw) {
6599
6736
  chart.redraw(animation);
@@ -6716,6 +6853,7 @@ function Chart(options, callback) {
6716
6853
  var hasData = axis.series.length && defined(min) && defined(max),
6717
6854
  showAxis = hasData || pick(options.showEmpty, true),
6718
6855
  titleOffset = 0,
6856
+ titleOffsetOption,
6719
6857
  titleMargin = 0,
6720
6858
  axisTitleOptions = options.title,
6721
6859
  labelOptions = options.labels,
@@ -6790,6 +6928,7 @@ function Chart(options, callback) {
6790
6928
  if (showAxis) {
6791
6929
  titleOffset = axisTitle.getBBox()[horiz ? 'height' : 'width'];
6792
6930
  titleMargin = pick(axisTitleOptions.margin, horiz ? 5 : 10);
6931
+ titleOffsetOption = axisTitleOptions.offset;
6793
6932
  }
6794
6933
 
6795
6934
  // hide or show the title depending on whether showEmpty is set
@@ -6802,7 +6941,7 @@ function Chart(options, callback) {
6802
6941
  offset = directionFactor * pick(options.offset, axisOffset[side]);
6803
6942
 
6804
6943
  axisTitleMargin =
6805
- pick(axisTitleOptions.offset,
6944
+ pick(titleOffsetOption,
6806
6945
  labelOffset + titleMargin +
6807
6946
  (side !== 2 && labelOffset && directionFactor * options.labels[horiz ? 'y' : 'x'])
6808
6947
  );
@@ -6853,8 +6992,13 @@ function Chart(options, callback) {
6853
6992
  });
6854
6993
  }
6855
6994
 
6856
- // major ticks
6857
- each(tickPositions, function (pos, i) {
6995
+ // Major ticks. Pull out the first item and render it last so that
6996
+ // we can get the position of the neighbour label. #808.
6997
+ each(tickPositions.slice(1).concat([tickPositions[0]]), function (pos, i) {
6998
+
6999
+ // Reorganize the indices
7000
+ i = (i === tickPositions.length - 1) ? 0 : i + 1;
7001
+
6858
7002
  // linked axes need an extra check to find out if
6859
7003
  if (!isLinked || (pos >= min && pos <= max)) {
6860
7004
 
@@ -7448,8 +7592,10 @@ function Chart(options, callback) {
7448
7592
  while (i--) {
7449
7593
  axis = point.series[i ? 'yAxis' : 'xAxis'];
7450
7594
  if (crosshairsOptions[i] && axis) {
7451
- path = axis
7452
- .getPlotLinePath(point[i ? 'y' : 'x'], 1);
7595
+ path = axis.getPlotLinePath(
7596
+ i ? pick(point.stackY, point.y) : point.x, // #814
7597
+ 1
7598
+ );
7453
7599
  if (crosshairs[i]) {
7454
7600
  crosshairs[i].attr({ d: path, visibility: VISIBLE });
7455
7601
 
@@ -7942,7 +8088,9 @@ function Chart(options, callback) {
7942
8088
 
7943
8089
 
7944
8090
  if (!hasDragged) {
7945
- if (hoverPoint && attr(e.target, 'isTracker')) {
8091
+
8092
+ // Detect clicks on trackers or tracker groups, #783
8093
+ if (hoverPoint && (attr(e.target, 'isTracker') || attr(e.target.parentNode, 'isTracker'))) {
7946
8094
  var plotX = hoverPoint.plotX,
7947
8095
  plotY = hoverPoint.plotY;
7948
8096
 
@@ -7993,33 +8141,15 @@ function Chart(options, callback) {
7993
8141
  container.onclick = container.onmousedown = container.onmousemove = container.ontouchstart = container.ontouchend = container.ontouchmove = null;
7994
8142
  }
7995
8143
 
7996
- /**
7997
- * Create the image map that listens for mouseovers
7998
- */
7999
- placeTrackerGroup = function () {
8000
8144
 
8001
- // first create - plot positions is not final at this stage
8002
- if (!trackerGroup) {
8003
- chart.trackerGroup = trackerGroup = renderer.g('tracker')
8004
- .attr({ zIndex: 9 })
8005
- .add();
8006
-
8007
- // then position - this happens on load and after resizing and changing
8008
- // axis or box positions
8009
- } else {
8010
- trackerGroup.translate(plotLeft, plotTop);
8011
- if (inverted) {
8012
- trackerGroup.attr({
8013
- width: chart.plotWidth,
8014
- height: chart.plotHeight
8015
- }).invert();
8016
- }
8017
- }
8018
- };
8145
+ // Run MouseTracker
8019
8146
 
8147
+ if (!trackerGroup) {
8148
+ chart.trackerGroup = trackerGroup = renderer.g('tracker')
8149
+ .attr({ zIndex: 9 })
8150
+ .add();
8151
+ }
8020
8152
 
8021
- // Run MouseTracker
8022
- placeTrackerGroup();
8023
8153
  if (options.enabled) {
8024
8154
  chart.tooltip = tooltip = Tooltip(options);
8025
8155
 
@@ -8066,14 +8196,16 @@ function Chart(options, callback) {
8066
8196
  itemHiddenStyle = merge(itemStyle, options.itemHiddenStyle),
8067
8197
  padding = options.padding || pInt(style.padding),
8068
8198
  ltr = !options.rtl,
8199
+ itemMarginTop = options.itemMarginTop || 0,
8200
+ itemMarginBottom = options.itemMarginBottom || 0,
8069
8201
  y = 18,
8202
+ maxItemWidth = 0,
8070
8203
  initialItemX = 4 + padding + symbolWidth + symbolPadding,
8204
+ initialItemY = padding + itemMarginTop + y - 5, // 5 is the number of pixels above the text
8071
8205
  itemX,
8072
8206
  itemY,
8073
8207
  lastItemY,
8074
8208
  itemHeight = 0,
8075
- itemMarginTop = options.itemMarginTop || 0,
8076
- itemMarginBottom = options.itemMarginBottom || 0,
8077
8209
  box,
8078
8210
  legendBorderWidth = options.borderWidth,
8079
8211
  legendBackgroundColor = options.backgroundColor,
@@ -8364,7 +8496,17 @@ function Chart(options, callback) {
8364
8496
  itemX = initialItemX;
8365
8497
  itemY += itemMarginTop + itemHeight + itemMarginBottom;
8366
8498
  }
8367
- lastItemY = itemY + itemMarginBottom;
8499
+
8500
+ // If the item exceeds the height, start a new column
8501
+ if (!horizontal && itemY + options.y + itemHeight > chartHeight - spacingTop - spacingBottom) {
8502
+ itemY = initialItemY;
8503
+ itemX += maxItemWidth;
8504
+ maxItemWidth = 0;
8505
+ }
8506
+
8507
+ // Set the edge positions
8508
+ maxItemWidth = mathMax(maxItemWidth, itemWidth);
8509
+ lastItemY = mathMax(lastItemY, itemY + itemMarginBottom);
8368
8510
 
8369
8511
  // cache the position of the newly generated or reordered items
8370
8512
  item._legendItemPos = [itemX, itemY];
@@ -8378,7 +8520,7 @@ function Chart(options, callback) {
8378
8520
 
8379
8521
  // the width of the widest item
8380
8522
  offsetWidth = widthOption || mathMax(
8381
- horizontal ? itemX - initialItemX : itemWidth,
8523
+ (itemX - initialItemX) + (horizontal ? 0 : itemWidth),
8382
8524
  offsetWidth
8383
8525
  );
8384
8526
 
@@ -8391,13 +8533,16 @@ function Chart(options, callback) {
8391
8533
  */
8392
8534
  function renderLegend() {
8393
8535
  itemX = initialItemX;
8394
- itemY = padding + itemMarginTop + y - 5; // 5 is the number of pixels above the text
8536
+ itemY = initialItemY;
8395
8537
  offsetWidth = 0;
8396
8538
  lastItemY = 0;
8397
8539
 
8398
8540
  if (!legendGroup) {
8399
8541
  legendGroup = renderer.g('legend')
8400
- .attr({ zIndex: 10 }) // in front of trackers, #414
8542
+ // #414, #759. Trackers will be drawn above the legend, but we have
8543
+ // to sacrifice that because tooltips need to be above the legend
8544
+ // and trackers above tooltips
8545
+ .attr({ zIndex: 7 })
8401
8546
  .add();
8402
8547
  }
8403
8548
 
@@ -8680,9 +8825,16 @@ function Chart(options, callback) {
8680
8825
 
8681
8826
  // redraw axes
8682
8827
  each(axes, function (axis) {
8683
- fireEvent(axis, 'afterSetExtremes', axis.getExtremes()); // #747, #751
8684
- if (axis.isDirty) {
8828
+
8829
+ // Fire 'afterSetExtremes' only if extremes are set
8830
+ if (axis.isDirtyExtremes) { // #821
8831
+ axis.isDirtyExtremes = false;
8832
+ fireEvent(axis, 'afterSetExtremes', axis.getExtremes()); // #747, #751
8833
+ }
8834
+
8835
+ if (axis.isDirty || isDirtyBox) {
8685
8836
  axis.redraw();
8837
+ isDirtyBox = true; // #792
8686
8838
  }
8687
8839
  });
8688
8840
 
@@ -8692,7 +8844,6 @@ function Chart(options, callback) {
8692
8844
  // the plot areas size has changed
8693
8845
  if (isDirtyBox) {
8694
8846
  drawChartBox();
8695
- placeTrackerGroup();
8696
8847
 
8697
8848
  // move clip rect
8698
8849
  if (clipRect) {
@@ -8919,8 +9070,7 @@ function Chart(options, callback) {
8919
9070
  zoom = function (event) {
8920
9071
 
8921
9072
  // add button to reset selection
8922
- var animate = chart.pointCount < 100,
8923
- hasZoomed;
9073
+ var hasZoomed;
8924
9074
 
8925
9075
  if (chart.resetZoomEnabled !== false && !chart.resetZoomButton) { // hook for Stock charts etc.
8926
9076
  showResetZoom();
@@ -8948,7 +9098,9 @@ function Chart(options, callback) {
8948
9098
 
8949
9099
  // Redraw
8950
9100
  if (hasZoomed) {
8951
- redraw(true, animate);
9101
+ redraw(
9102
+ pick(optionsChart.animation, chart.pointCount < 100) // animation
9103
+ );
8952
9104
  }
8953
9105
  };
8954
9106
 
@@ -9017,7 +9169,7 @@ function Chart(options, callback) {
9017
9169
  .attr({
9018
9170
  align: chartTitleOptions.align,
9019
9171
  'class': PREFIX + name,
9020
- zIndex: 1
9172
+ zIndex: chartTitleOptions.zIndex || 4
9021
9173
  })
9022
9174
  .css(chartTitleOptions.style)
9023
9175
  .add()
@@ -9238,7 +9390,7 @@ function Chart(options, callback) {
9238
9390
  function reflow(e) {
9239
9391
  var width = optionsChart.width || renderTo.offsetWidth,
9240
9392
  height = optionsChart.height || renderTo.offsetHeight,
9241
- target = e.target;
9393
+ target = e ? e.target : win; // #805 - MooTools doesn't supply e
9242
9394
 
9243
9395
  // Width and height checks for display:none. Target is doc in IE8 and Opera,
9244
9396
  // win in Firefox, Chrome and IE9.
@@ -9587,17 +9739,9 @@ function Chart(options, callback) {
9587
9739
  .align(credits.position);
9588
9740
  }
9589
9741
 
9590
- placeTrackerGroup();
9591
-
9592
9742
  // Set flag
9593
9743
  chart.hasRendered = true;
9594
9744
 
9595
- // If the chart was rendered outside the top container, put it back in
9596
- if (renderToClone) {
9597
- renderTo.appendChild(container);
9598
- discardElement(renderToClone);
9599
- //updatePosition(container);
9600
- }
9601
9745
  }
9602
9746
 
9603
9747
  /**
@@ -9750,6 +9894,13 @@ function Chart(options, callback) {
9750
9894
  fn.apply(chart, [chart]);
9751
9895
  });
9752
9896
 
9897
+
9898
+ // If the chart was rendered outside the top container, put it back in
9899
+ if (renderToClone) {
9900
+ renderTo.appendChild(container);
9901
+ discardElement(renderToClone);
9902
+ }
9903
+
9753
9904
  fireEvent(chart, 'load');
9754
9905
 
9755
9906
  }
@@ -10068,29 +10219,37 @@ Point.prototype = {
10068
10219
  split = String(point.y).split('.'),
10069
10220
  originalDecimals = split[1] ? split[1].length : 0,
10070
10221
  match = pointFormat.match(/\{(series|point)\.[a-zA-Z]+\}/g),
10071
- splitter = /[\.}]/,
10222
+ splitter = /[{\.}]/,
10072
10223
  obj,
10073
10224
  key,
10074
10225
  replacement,
10226
+ parts,
10227
+ prop,
10075
10228
  i;
10076
10229
 
10077
10230
  // loop over the variables defined on the form {series.name}, {point.y} etc
10078
10231
  for (i in match) {
10079
10232
  key = match[i];
10080
-
10081
10233
  if (isString(key) && key !== pointFormat) { // IE matches more than just the variables
10082
- obj = key.indexOf('point') === 1 ? point : series;
10083
10234
 
10084
- if (key === '{point.y}') { // add some preformatting
10235
+ // Split it further into parts
10236
+ parts = (' ' + key).split(splitter); // add empty string because IE and the rest handles it differently
10237
+ obj = { 'point': point, 'series': series }[parts[1]];
10238
+ prop = parts[2];
10239
+
10240
+ // Add some preformatting
10241
+ if (obj === point && (prop === 'y' || prop === 'open' || prop === 'high' ||
10242
+ prop === 'low' || prop === 'close')) {
10085
10243
  replacement = (seriesTooltipOptions.valuePrefix || seriesTooltipOptions.yPrefix || '') +
10086
- numberFormat(point.y, pick(seriesTooltipOptions.valueDecimals, seriesTooltipOptions.yDecimals, originalDecimals)) +
10244
+ numberFormat(point[prop], pick(seriesTooltipOptions.valueDecimals, seriesTooltipOptions.yDecimals, originalDecimals)) +
10087
10245
  (seriesTooltipOptions.valueSuffix || seriesTooltipOptions.ySuffix || '');
10088
10246
 
10089
- } else { // automatic replacement
10090
- replacement = obj[match[i].split(splitter)[1]];
10247
+ // Automatic replacement
10248
+ } else {
10249
+ replacement = obj[prop];
10091
10250
  }
10092
10251
 
10093
- pointFormat = pointFormat.replace(match[i], replacement);
10252
+ pointFormat = pointFormat.replace(key, replacement);
10094
10253
  }
10095
10254
  }
10096
10255
 
@@ -10282,7 +10441,7 @@ Point.prototype = {
10282
10441
 
10283
10442
  // apply hover styles to the existing point
10284
10443
  if (point.graphic) {
10285
- radius = point.graphic.symbolName && pointAttr[state].r;
10444
+ radius = markerOptions && point.graphic.symbolName && pointAttr[state].r;
10286
10445
  point.graphic.attr(merge(
10287
10446
  pointAttr[state],
10288
10447
  radius ? { // new symbol attributes (#507, #612)
@@ -10350,6 +10509,7 @@ Series.prototype = {
10350
10509
  isCartesian: true,
10351
10510
  type: 'line',
10352
10511
  pointClass: Point,
10512
+ sorted: true, // requires the data to be sorted
10353
10513
  pointAttrToOptions: { // mapping between SVG attributes and the corresponding options
10354
10514
  stroke: 'lineColor',
10355
10515
  'stroke-width': 'lineWidth',
@@ -10484,7 +10644,9 @@ Series.prototype = {
10484
10644
  points.splice(i, 1);
10485
10645
  }
10486
10646
  }
10487
- segments = [points];
10647
+ if (points.length) {
10648
+ segments = [points];
10649
+ }
10488
10650
 
10489
10651
  // else, split on null points
10490
10652
  } else {
@@ -10584,15 +10746,19 @@ Series.prototype = {
10584
10746
 
10585
10747
  setAnimation(animation, chart);
10586
10748
 
10587
- if (graph && shift) { // make graph animate sideways
10749
+ // Make graph animate sideways
10750
+ if (graph && shift) {
10588
10751
  graph.shift = currentShift + 1;
10589
10752
  }
10590
10753
  if (area) {
10591
- area.shift = currentShift + 1;
10592
- area.isArea = true;
10754
+ if (shift) { // #780
10755
+ area.shift = currentShift + 1;
10756
+ }
10757
+ area.isArea = true; // needed in animation, both with and without shift
10593
10758
  }
10594
- redraw = pick(redraw, true);
10595
10759
 
10760
+ // Optional redraw, defaults to true
10761
+ redraw = pick(redraw, true);
10596
10762
 
10597
10763
  // Get options and push the point to xData, yData and series.options. In series.generatePoints
10598
10764
  // the Point instance will be created on demand and pushed to the series.data array.
@@ -10778,16 +10944,17 @@ Series.prototype = {
10778
10944
  xAxis = series.xAxis,
10779
10945
  i, // loop variable
10780
10946
  options = series.options,
10781
- cropThreshold = options.cropThreshold;
10947
+ cropThreshold = options.cropThreshold,
10948
+ isCartesian = series.isCartesian;
10782
10949
 
10783
10950
  // If the series data or axes haven't changed, don't go through this. Return false to pass
10784
10951
  // the message on to override methods like in data grouping.
10785
- if (series.isCartesian && !series.isDirty && !xAxis.isDirty && !series.yAxis.isDirty && !force) {
10952
+ if (isCartesian && !series.isDirty && !xAxis.isDirty && !series.yAxis.isDirty && !force) {
10786
10953
  return false;
10787
10954
  }
10788
10955
 
10789
10956
  // optionally filter out points outside the plot area
10790
- if (!cropThreshold || dataLength > cropThreshold || series.forceCrop) {
10957
+ if (isCartesian && series.sorted && (!cropThreshold || dataLength > cropThreshold || series.forceCrop)) {
10791
10958
  var extremes = xAxis.getExtremes(),
10792
10959
  min = extremes.min,
10793
10960
  max = extremes.max;
@@ -10825,7 +10992,7 @@ Series.prototype = {
10825
10992
  // Find the closest distance between processed points
10826
10993
  for (i = processedXData.length - 1; i > 0; i--) {
10827
10994
  distance = processedXData[i] - processedXData[i - 1];
10828
- if (closestPointRange === UNDEFINED || distance < closestPointRange) {
10995
+ if (distance > 0 && (closestPointRange === UNDEFINED || distance < closestPointRange)) {
10829
10996
  closestPointRange = distance;
10830
10997
  }
10831
10998
  }
@@ -10966,6 +11133,7 @@ Series.prototype = {
10966
11133
 
10967
11134
  point.percentage = pointStackTotal ? point.y * 100 / pointStackTotal : 0;
10968
11135
  point.stackTotal = pointStackTotal;
11136
+ point.stackY = yValue;
10969
11137
  }
10970
11138
 
10971
11139
  // Set translated yBottom or remove it
@@ -10978,10 +11146,10 @@ Series.prototype = {
10978
11146
  yValue = series.modifyValue(yValue, point);
10979
11147
  }
10980
11148
 
10981
- // set the y value
10982
- if (yValue !== null) {
10983
- point.plotY = mathRound(yAxis.translate(yValue, 0, 1, 0, 1) * 10) / 10; // Math.round fixes #591
10984
- }
11149
+ // Set the the plotY value, reset it for redraws
11150
+ point.plotY = (typeof yValue === 'number') ?
11151
+ mathRound(yAxis.translate(yValue, 0, 1, 0, 1) * 10) / 10 : // Math.round fixes #591
11152
+ UNDEFINED;
10985
11153
 
10986
11154
  // set client related positions for mouse tracking
10987
11155
  point.clientX = chart.inverted ?
@@ -11473,9 +11641,18 @@ Series.prototype = {
11473
11641
  isBarLike = seriesType === 'column' || seriesType === 'bar',
11474
11642
  vAlignIsNull = options.verticalAlign === null,
11475
11643
  yIsNull = options.y === null,
11476
- dataLabel;
11644
+ fontMetrics = renderer.fontMetrics(options.style.fontSize), // height and baseline
11645
+ fontLineHeight = fontMetrics.h,
11646
+ fontBaseline = fontMetrics.b,
11647
+ dataLabel,
11648
+ enabled;
11477
11649
 
11478
11650
  if (isBarLike) {
11651
+ var defaultYs = {
11652
+ top: fontBaseline,
11653
+ middle: fontBaseline - fontLineHeight / 2,
11654
+ bottom: -fontLineHeight + fontBaseline
11655
+ };
11479
11656
  if (stacking) {
11480
11657
  // In stacked series the default label placement is inside the bars
11481
11658
  if (vAlignIsNull) {
@@ -11484,13 +11661,18 @@ Series.prototype = {
11484
11661
 
11485
11662
  // If no y delta is specified, try to create a good default
11486
11663
  if (yIsNull) {
11487
- options = merge(options, {y: {top: 14, middle: 4, bottom: -6}[options.verticalAlign]});
11664
+ options = merge(options, { y: defaultYs[options.verticalAlign]});
11488
11665
  }
11489
11666
  } else {
11490
11667
  // In non stacked series the default label placement is on top of the bars
11491
11668
  if (vAlignIsNull) {
11492
11669
  options = merge(options, {verticalAlign: 'top'});
11670
+
11671
+ // If no y delta is specified, try to create a good default (like default bar)
11672
+ } else if (yIsNull) {
11673
+ options = merge(options, { y: defaultYs[options.verticalAlign]});
11493
11674
  }
11675
+
11494
11676
  }
11495
11677
  }
11496
11678
 
@@ -11521,28 +11703,39 @@ Series.prototype = {
11521
11703
  if (pointOptions && pointOptions.dataLabels) {
11522
11704
  options = merge(options, pointOptions.dataLabels);
11523
11705
  }
11706
+ enabled = options.enabled;
11707
+
11708
+ // Get the positions
11709
+ if (enabled) {
11710
+ var plotX = (point.barX && point.barX + point.barW / 2) || pick(point.plotX, -999),
11711
+ plotY = pick(point.plotY, -999),
11712
+
11713
+ // if options.y is null, which happens by default on column charts, set the position
11714
+ // above or below the column depending on the threshold
11715
+ individualYDelta = options.y === null ?
11716
+ (point.y >= seriesOptions.threshold ?
11717
+ -fontLineHeight + fontBaseline : // below the threshold
11718
+ fontBaseline) : // above the threshold
11719
+ options.y;
11720
+
11721
+ x = (inverted ? chart.plotWidth - plotY : plotX) + options.x;
11722
+ y = mathRound((inverted ? chart.plotHeight - plotX : plotY) + individualYDelta);
11524
11723
 
11525
- // If the point is outside the plot area, destroy it. #678
11526
- if (dataLabel && series.isCartesian && !chart.isInsidePlot(point.plotX, point.plotY)) {
11724
+ }
11725
+
11726
+ // If the point is outside the plot area, destroy it. #678, #820
11727
+ if (dataLabel && series.isCartesian && (!chart.isInsidePlot(x, y) || !enabled)) {
11527
11728
  point.dataLabel = dataLabel.destroy();
11528
11729
 
11529
11730
  // Individual labels are disabled if the are explicitly disabled
11530
11731
  // in the point options, or if they fall outside the plot area.
11531
- } else if (options.enabled) {
11732
+ } else if (enabled) {
11733
+
11734
+ var align = options.align;
11532
11735
 
11533
11736
  // Get the string
11534
11737
  str = options.formatter.call(point.getLabelConfig(), options);
11535
11738
 
11536
- var barX = point.barX,
11537
- plotX = (barX && barX + point.barW / 2) || point.plotX || -999,
11538
- plotY = pick(point.plotY, -999),
11539
- align = options.align,
11540
- individualYDelta = yIsNull ? (point.y >= 0 ? -6 : 12) : options.y;
11541
-
11542
- // Postprocess the positions
11543
- x = (inverted ? chart.plotWidth - plotY : plotX) + options.x;
11544
- y = (inverted ? chart.plotHeight - plotX : plotY) + individualYDelta;
11545
-
11546
11739
  // in columns, align the string to the column
11547
11740
  if (seriesType === 'column') {
11548
11741
  x += { left: -1, right: 1 }[align] * point.barW / 2 || 0;
@@ -11560,9 +11753,6 @@ Series.prototype = {
11560
11753
  // update existing label
11561
11754
  if (dataLabel) {
11562
11755
  // vertically centered
11563
- if (inverted && !options.y) {
11564
- y = y + pInt(dataLabel.styles.lineHeight) * 0.9 - dataLabel.getBBox().height / 2;
11565
- }
11566
11756
  dataLabel
11567
11757
  .attr({
11568
11758
  text: str
@@ -11572,29 +11762,34 @@ Series.prototype = {
11572
11762
  });
11573
11763
  // create new label
11574
11764
  } else if (defined(str)) {
11575
- dataLabel = point.dataLabel = renderer.text(
11765
+ dataLabel = point.dataLabel = renderer[options.rotation ? 'text' : 'label']( // labels don't support rotation
11576
11766
  str,
11577
11767
  x,
11578
11768
  y,
11579
- options.useHTML
11769
+ null,
11770
+ null,
11771
+ null,
11772
+ options.useHTML,
11773
+ true // baseline for backwards compat
11580
11774
  )
11581
11775
  .attr({
11582
11776
  align: align,
11777
+ fill: options.backgroundColor,
11778
+ stroke: options.borderColor,
11779
+ 'stroke-width': options.borderWidth,
11780
+ r: options.borderRadius,
11583
11781
  rotation: options.rotation,
11782
+ padding: options.padding,
11584
11783
  zIndex: 1
11585
11784
  })
11586
11785
  .css(options.style)
11587
- .add(dataLabelsGroup);
11588
- // vertically centered
11589
- if (inverted && !options.y) {
11590
- dataLabel.attr({
11591
- y: y + pInt(dataLabel.styles.lineHeight) * 0.9 - dataLabel.getBBox().height / 2
11592
- });
11593
- }
11786
+ .add(dataLabelsGroup)
11787
+ .shadow(options.shadow);
11594
11788
  }
11595
11789
 
11596
11790
  if (isBarLike && seriesOptions.stacking && dataLabel) {
11597
- var barY = point.barY,
11791
+ var barX = point.barX,
11792
+ barY = point.barY,
11598
11793
  barW = point.barW,
11599
11794
  barH = point.barH;
11600
11795
 
@@ -11759,6 +11954,42 @@ Series.prototype = {
11759
11954
  }
11760
11955
  },
11761
11956
 
11957
+ /**
11958
+ * Initialize and perform group inversion on series.group and series.trackerGroup
11959
+ */
11960
+ invertGroups: function () {
11961
+ var series = this,
11962
+ group = series.group,
11963
+ trackerGroup = series.trackerGroup,
11964
+ chart = series.chart;
11965
+
11966
+ // A fixed size is needed for inversion to work
11967
+ function setInvert() {
11968
+ var size = {
11969
+ width: series.yAxis.len,
11970
+ height: series.xAxis.len
11971
+ };
11972
+
11973
+ // Set the series.group size
11974
+ group.attr(size).invert();
11975
+
11976
+ // Set the tracker group size
11977
+ if (trackerGroup) {
11978
+ trackerGroup.attr(size).invert();
11979
+ }
11980
+ }
11981
+
11982
+ addEvent(chart, 'resize', setInvert); // do it on resize
11983
+ addEvent(series, 'destroy', function () {
11984
+ removeEvent(chart, 'resize', setInvert);
11985
+ });
11986
+
11987
+ // Do it now
11988
+ setInvert(); // do it now
11989
+
11990
+ // On subsequent render and redraw, just do setInvert without setting up events again
11991
+ series.invertGroups = setInvert;
11992
+ },
11762
11993
 
11763
11994
  /**
11764
11995
  * Render the graph and markers
@@ -11767,7 +11998,6 @@ Series.prototype = {
11767
11998
  var series = this,
11768
11999
  chart = series.chart,
11769
12000
  group,
11770
- setInvert,
11771
12001
  options = series.options,
11772
12002
  doClip = options.clip !== false,
11773
12003
  animation = options.animation,
@@ -11797,24 +12027,6 @@ Series.prototype = {
11797
12027
  if (!series.group) {
11798
12028
  group = series.group = renderer.g('series');
11799
12029
 
11800
- if (chart.inverted) {
11801
- setInvert = function () {
11802
- group.attr({
11803
- width: chart.plotWidth,
11804
- height: chart.plotHeight
11805
- }).invert();
11806
- };
11807
-
11808
- setInvert(); // do it now
11809
- addEvent(chart, 'resize', setInvert); // do it on resize
11810
- addEvent(series, 'destroy', function () {
11811
- removeEvent(chart, 'resize', setInvert);
11812
- });
11813
- }
11814
-
11815
- if (doClip) {
11816
- group.clip(clipRect);
11817
- }
11818
12030
  group.attr({
11819
12031
  visibility: series.visible ? VISIBLE : HIDDEN,
11820
12032
  zIndex: options.zIndex
@@ -11846,6 +12058,20 @@ Series.prototype = {
11846
12058
  series.drawTracker();
11847
12059
  }
11848
12060
 
12061
+ // Handle inverted series and tracker groups
12062
+ if (chart.inverted) {
12063
+ series.invertGroups();
12064
+ }
12065
+
12066
+ // Do the initial clipping. This must be done after inverting for VML.
12067
+ if (doClip && !series.hasRendered) {
12068
+ group.clip(clipRect);
12069
+ if (series.trackerGroup) {
12070
+ series.trackerGroup.clip(chart.clipRect);
12071
+ }
12072
+ }
12073
+
12074
+
11849
12075
  // run the animation
11850
12076
  if (doAnimation) {
11851
12077
  series.animate();
@@ -11865,7 +12091,7 @@ Series.prototype = {
11865
12091
 
11866
12092
  series.isDirty = series.isDirtyData = false; // means data is in accordance with what you see
11867
12093
  // (See #322) series.isDirty = series.isDirtyData = false; // means data is in accordance with what you see
11868
-
12094
+ series.hasRendered = true;
11869
12095
  },
11870
12096
 
11871
12097
  /**
@@ -12038,6 +12264,32 @@ Series.prototype = {
12038
12264
  fireEvent(series, selected ? 'select' : 'unselect');
12039
12265
  },
12040
12266
 
12267
+ /**
12268
+ * Create a group that holds the tracking object or objects. This allows for
12269
+ * individual clipping and placement of each series tracker.
12270
+ */
12271
+ drawTrackerGroup: function () {
12272
+ var trackerGroup = this.trackerGroup,
12273
+ chart = this.chart;
12274
+
12275
+ if (this.isCartesian) {
12276
+
12277
+ // Generate it on first call
12278
+ if (!trackerGroup) {
12279
+ this.trackerGroup = trackerGroup = chart.renderer.g()
12280
+ .attr({
12281
+ zIndex: this.options.zIndex || 1
12282
+ })
12283
+ .add(chart.trackerGroup);
12284
+
12285
+ }
12286
+ // Place it on first and subsequent (redraw) calls
12287
+ trackerGroup.translate(this.xAxis.left, this.yAxis.top);
12288
+
12289
+ }
12290
+
12291
+ return trackerGroup;
12292
+ },
12041
12293
 
12042
12294
  /**
12043
12295
  * Draw the tracker object that sits above all data labels and markers to
@@ -12057,7 +12309,7 @@ Series.prototype = {
12057
12309
  cursor = options.cursor,
12058
12310
  css = cursor && { cursor: cursor },
12059
12311
  singlePoints = series.singlePoints,
12060
- group,
12312
+ trackerGroup = series.drawTrackerGroup(),
12061
12313
  singlePoint,
12062
12314
  i;
12063
12315
 
@@ -12089,9 +12341,6 @@ Series.prototype = {
12089
12341
  tracker.attr({ d: trackerPath });
12090
12342
 
12091
12343
  } else { // create
12092
- group = renderer.g()
12093
- .clip(chart.clipRect)
12094
- .add(chart.trackerGroup);
12095
12344
 
12096
12345
  series.tracker = renderer.path(trackerPath)
12097
12346
  .attr({
@@ -12100,8 +12349,7 @@ Series.prototype = {
12100
12349
  fill: NONE,
12101
12350
  'stroke-linejoin': 'bevel',
12102
12351
  'stroke-width' : options.lineWidth + 2 * snap,
12103
- visibility: series.visible ? VISIBLE : HIDDEN,
12104
- zIndex: options.zIndex || 1
12352
+ visibility: series.visible ? VISIBLE : HIDDEN
12105
12353
  })
12106
12354
  .on(hasTouch ? 'touchstart' : 'mouseover', function () {
12107
12355
  if (chart.hoverSeries !== series) {
@@ -12114,7 +12362,7 @@ Series.prototype = {
12114
12362
  }
12115
12363
  })
12116
12364
  .css(css)
12117
- .add(group);
12365
+ .add(trackerGroup);
12118
12366
  }
12119
12367
 
12120
12368
  }
@@ -12311,7 +12559,7 @@ var ColumnSeries = extendClass(Series, {
12311
12559
  optionPointWidth = options.pointWidth,
12312
12560
  pointPadding = defined(optionPointWidth) ? (pointOffsetWidth - optionPointWidth) / 2 :
12313
12561
  pointOffsetWidth * options.pointPadding,
12314
- pointWidth = mathCeil(mathMax(pick(optionPointWidth, pointOffsetWidth - 2 * pointPadding), 1)),
12562
+ pointWidth = mathCeil(mathMax(pick(optionPointWidth, pointOffsetWidth - 2 * pointPadding), 1 + 2 * borderWidth)),
12315
12563
  colIndex = (reversedXAxis ? columnCount -
12316
12564
  series.columnIndex : series.columnIndex) || 0,
12317
12565
  pointXOffset = pointPadding + (groupPadding + colIndex *
@@ -12324,7 +12572,7 @@ var ColumnSeries = extendClass(Series, {
12324
12572
  // record the new values
12325
12573
  each(points, function (point) {
12326
12574
  var plotY = point.plotY,
12327
- yBottom = point.yBottom || translatedThreshold,
12575
+ yBottom = pick(point.yBottom, translatedThreshold),
12328
12576
  barX = point.plotX + pointXOffset,
12329
12577
  barY = mathCeil(mathMin(plotY, yBottom)),
12330
12578
  barH = mathCeil(mathMax(plotY, yBottom) - barY),
@@ -12356,15 +12604,15 @@ var ColumnSeries = extendClass(Series, {
12356
12604
 
12357
12605
  // create shape type and shape args that are reused in drawPoints and drawTracker
12358
12606
  point.shapeType = 'rect';
12359
- shapeArgs = extend(chart.renderer.Element.prototype.crisp.apply({}, [
12360
- borderWidth,
12361
- barX,
12362
- barY,
12363
- pointWidth,
12364
- barH
12365
- ]), {
12366
- r: options.borderRadius
12367
- });
12607
+ shapeArgs = {
12608
+ x: barX,
12609
+ y: barY,
12610
+ width: pointWidth,
12611
+ height: barH,
12612
+ r: options.borderRadius,
12613
+ strokeWidth: borderWidth
12614
+ };
12615
+
12368
12616
  if (borderWidth % 2) { // correct for shorting in crisp method, visible in stacked columns with 1px border
12369
12617
  shapeArgs.y -= 1;
12370
12618
  shapeArgs.height += 1;
@@ -12409,13 +12657,20 @@ var ColumnSeries = extendClass(Series, {
12409
12657
  shapeArgs = point.shapeArgs;
12410
12658
  if (graphic) { // update
12411
12659
  stop(graphic);
12412
- graphic.animate(shapeArgs);
12660
+ graphic.animate(renderer.Element.prototype.crisp.apply({}, [
12661
+ shapeArgs.strokeWidth,
12662
+ shapeArgs.x,
12663
+ shapeArgs.y,
12664
+ shapeArgs.width,
12665
+ shapeArgs.height
12666
+ ]));
12413
12667
 
12414
12668
  } else {
12415
12669
  point.graphic = graphic = renderer[point.shapeType](shapeArgs)
12416
12670
  .attr(point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE])
12417
12671
  .add(series.group)
12418
12672
  .shadow(options.shadow);
12673
+
12419
12674
  }
12420
12675
 
12421
12676
  }
@@ -12435,16 +12690,9 @@ var ColumnSeries = extendClass(Series, {
12435
12690
  options = series.options,
12436
12691
  cursor = options.cursor,
12437
12692
  css = cursor && { cursor: cursor },
12438
- group,
12693
+ trackerGroup = series.drawTrackerGroup(),
12439
12694
  rel;
12440
12695
 
12441
- // Add a series specific group to allow clipping the trackers
12442
- if (series.isCartesian) {
12443
- group = renderer.g()
12444
- .clip(chart.clipRect)
12445
- .add(chart.trackerGroup);
12446
- }
12447
-
12448
12696
  each(series.points, function (point) {
12449
12697
  tracker = point.tracker;
12450
12698
  shapeArgs = point.trackerArgs || point.shapeArgs;
@@ -12459,8 +12707,7 @@ var ColumnSeries = extendClass(Series, {
12459
12707
  .attr({
12460
12708
  isTracker: trackerLabel,
12461
12709
  fill: TRACKER_FILL,
12462
- visibility: series.visible ? VISIBLE : HIDDEN,
12463
- zIndex: options.zIndex || 1
12710
+ visibility: series.visible ? VISIBLE : HIDDEN
12464
12711
  })
12465
12712
  .on(hasTouch ? 'touchstart' : 'mouseover', function (event) {
12466
12713
  rel = event.relatedTarget || event.fromElement;
@@ -12479,7 +12726,7 @@ var ColumnSeries = extendClass(Series, {
12479
12726
  }
12480
12727
  })
12481
12728
  .css(css)
12482
- .add(point.group || group); // pies have point group - see issue #118
12729
+ .add(point.group || trackerGroup); // pies have point group - see issue #118
12483
12730
  }
12484
12731
  }
12485
12732
  });
@@ -12569,7 +12816,7 @@ seriesTypes.bar = BarSeries;
12569
12816
  */
12570
12817
  var ScatterSeries = extendClass(Series, {
12571
12818
  type: 'scatter',
12572
-
12819
+ sorted: false,
12573
12820
  /**
12574
12821
  * Extend the base Series' translate method by adding shape type and
12575
12822
  * arguments for the point trackers
@@ -12605,16 +12852,21 @@ var ScatterSeries = extendClass(Series, {
12605
12852
  while (i--) {
12606
12853
  graphic = points[i].graphic;
12607
12854
  if (graphic) { // doesn't exist for null points
12608
- graphic.element._index = i;
12855
+ graphic.element._i = i;
12609
12856
  }
12610
12857
  }
12611
12858
 
12612
12859
  // Add the event listeners, we need to do this only once
12613
12860
  if (!series._hasTracking) {
12614
12861
  series.group
12862
+ .attr({
12863
+ isTracker: true
12864
+ })
12615
12865
  .on(hasTouch ? 'touchstart' : 'mouseover', function (e) {
12616
12866
  series.onMouseOver();
12617
- points[e.target._index].onMouseOver();
12867
+ if (e.target._i !== UNDEFINED) { // undefined on graph in scatterchart
12868
+ points[e.target._i].onMouseOver();
12869
+ }
12618
12870
  })
12619
12871
  .on('mouseout', function () {
12620
12872
  if (!series.options.stickyTracking) {
@@ -13061,7 +13313,7 @@ var PieSeries = extendClass(Series, {
13061
13313
  };
13062
13314
 
13063
13315
  // assume equal label heights
13064
- labelHeight = halves[0][0] && halves[0][0].dataLabel && pInt(halves[0][0].dataLabel.styles.lineHeight);
13316
+ labelHeight = halves[0][0] && halves[0][0].dataLabel && halves[0][0].dataLabel.getBBox().height;
13065
13317
 
13066
13318
  /* Loop over the points in each quartile, starting from the top and bottom
13067
13319
  * of the pie to detect overlapping labels.
@@ -13286,6 +13538,6 @@ extend(Highcharts, {
13286
13538
  extendClass: extendClass,
13287
13539
  placeBox: placeBox,
13288
13540
  product: 'Highcharts',
13289
- version: '2.2.0'
13541
+ version: '2.2.1'
13290
13542
  });
13291
13543
  }());