highcharts-rails 2.2.0 → 2.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
  }());