Subversion-Projekte lars-tiefland.ci

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
875 lars 1
// ==ClosureCompiler==
2
// @compilation_level SIMPLE_OPTIMIZATIONS
3
 
4
/**
5
 * @license Highstock JS v2.1.8 (2015-08-20)
6
 *
7
 * (c) 2009-2014 Torstein Honsi
8
 *
9
 * License: www.highcharts.com/license
10
 */
11
 
12
// JSLint options:
13
/*global Highcharts, HighchartsAdapter, document, window, navigator, setInterval, clearInterval, clearTimeout, setTimeout, location, jQuery, $, console, each, grep */
14
/*jslint ass: true, sloppy: true, forin: true, plusplus: true, nomen: true, vars: true, regexp: true, newcap: true, browser: true, continue: true, white: true */
15
(function () {
16
// encapsulated variables
17
var UNDEFINED,
18
	doc = document,
19
	win = window,
20
	math = Math,
21
	mathRound = math.round,
22
	mathFloor = math.floor,
23
	mathCeil = math.ceil,
24
	mathMax = math.max,
25
	mathMin = math.min,
26
	mathAbs = math.abs,
27
	mathCos = math.cos,
28
	mathSin = math.sin,
29
	mathPI = math.PI,
30
	deg2rad = mathPI * 2 / 360,
31
 
32
 
33
	// some variables
34
	userAgent = navigator.userAgent,
35
	isOpera = win.opera,
36
	isIE = /(msie|trident)/i.test(userAgent) && !isOpera,
37
	docMode8 = doc.documentMode === 8,
38
	isWebKit = /AppleWebKit/.test(userAgent),
39
	isFirefox = /Firefox/.test(userAgent),
40
	isTouchDevice = /(Mobile|Android|Windows Phone)/.test(userAgent),
41
	SVG_NS = 'http://www.w3.org/2000/svg',
42
	hasSVG = !!doc.createElementNS && !!doc.createElementNS(SVG_NS, 'svg').createSVGRect,
43
	hasBidiBug = isFirefox && parseInt(userAgent.split('Firefox/')[1], 10) < 4, // issue #38
44
	useCanVG = !hasSVG && !isIE && !!doc.createElement('canvas').getContext,
45
	Renderer,
46
	hasTouch,
47
	symbolSizes = {},
48
	idCounter = 0,
49
	garbageBin,
50
	defaultOptions,
51
	dateFormat, // function
52
	pathAnim,
53
	timeUnits,
54
	noop = function () { return UNDEFINED; },
55
	charts = [],
56
	chartCount = 0,
57
	PRODUCT = 'Highstock',
58
	VERSION = '2.1.8',
59
 
60
	// some constants for frequently used strings
61
	DIV = 'div',
62
	ABSOLUTE = 'absolute',
63
	RELATIVE = 'relative',
64
	HIDDEN = 'hidden',
65
	PREFIX = 'highcharts-',
66
	VISIBLE = 'visible',
67
	PX = 'px',
68
	NONE = 'none',
69
	M = 'M',
70
	L = 'L',
71
	numRegex = /^[0-9]+$/,
72
	NORMAL_STATE = '',
73
	HOVER_STATE = 'hover',
74
	SELECT_STATE = 'select',
75
	marginNames = ['plotTop', 'marginRight', 'marginBottom', 'plotLeft'],
76
 
77
	// Object for extending Axis
78
	AxisPlotLineOrBandExtension,
79
 
80
	// constants for attributes
81
	STROKE_WIDTH = 'stroke-width',
82
 
83
	// time methods, changed based on whether or not UTC is used
84
	Date,  // Allow using a different Date class
85
	makeTime,
86
	timezoneOffset,
87
	getTimezoneOffset,
88
	getMinutes,
89
	getHours,
90
	getDay,
91
	getDate,
92
	getMonth,
93
	getFullYear,
94
	setMilliseconds,
95
	setSeconds,
96
	setMinutes,
97
	setHours,
98
	setDate,
99
	setMonth,
100
	setFullYear,
101
 
102
 
103
	// lookup over the types and the associated classes
104
	seriesTypes = {},
105
	Highcharts;
106
 
107
// The Highcharts namespace
108
Highcharts = win.Highcharts = win.Highcharts ? error(16, true) : {};
109
 
110
Highcharts.seriesTypes = seriesTypes;
111
 
112
/**
113
 * Extend an object with the members of another
114
 * @param {Object} a The object to be extended
115
 * @param {Object} b The object to add to the first one
116
 */
117
var extend = Highcharts.extend = function (a, b) {
118
	var n;
119
	if (!a) {
120
		a = {};
121
	}
122
	for (n in b) {
123
		a[n] = b[n];
124
	}
125
	return a;
126
};
127
 
128
/**
129
 * Deep merge two or more objects and return a third object. If the first argument is
130
 * true, the contents of the second object is copied into the first object.
131
 * Previously this function redirected to jQuery.extend(true), but this had two limitations.
132
 * First, it deep merged arrays, which lead to workarounds in Highcharts. Second,
133
 * it copied properties from extended prototypes.
134
 */
135
function merge() {
136
	var i,
137
		args = arguments,
138
		len,
139
		ret = {},
140
		doCopy = function (copy, original) {
141
			var value, key;
142
 
143
			// An object is replacing a primitive
144
			if (typeof copy !== 'object') {
145
				copy = {};
146
			}
147
 
148
			for (key in original) {
149
				if (original.hasOwnProperty(key)) {
150
					value = original[key];
151
 
152
					// Copy the contents of objects, but not arrays or DOM nodes
153
					if (value && typeof value === 'object' && Object.prototype.toString.call(value) !== '[object Array]' &&
154
							key !== 'renderTo' && typeof value.nodeType !== 'number') {
155
						copy[key] = doCopy(copy[key] || {}, value);
156
 
157
					// Primitives and arrays are copied over directly
158
					} else {
159
						copy[key] = original[key];
160
					}
161
				}
162
			}
163
			return copy;
164
		};
165
 
166
	// If first argument is true, copy into the existing object. Used in setOptions.
167
	if (args[0] === true) {
168
		ret = args[1];
169
		args = Array.prototype.slice.call(args, 2);
170
	}
171
 
172
	// For each argument, extend the return
173
	len = args.length;
174
	for (i = 0; i < len; i++) {
175
		ret = doCopy(ret, args[i]);
176
	}
177
 
178
	return ret;
179
}
180
 
181
/**
182
 * Shortcut for parseInt
183
 * @param {Object} s
184
 * @param {Number} mag Magnitude
185
 */
186
function pInt(s, mag) {
187
	return parseInt(s, mag || 10);
188
}
189
 
190
/**
191
 * Check for string
192
 * @param {Object} s
193
 */
194
function isString(s) {
195
	return typeof s === 'string';
196
}
197
 
198
/**
199
 * Check for object
200
 * @param {Object} obj
201
 */
202
function isObject(obj) {
203
	return obj && typeof obj === 'object';
204
}
205
 
206
/**
207
 * Check for array
208
 * @param {Object} obj
209
 */
210
function isArray(obj) {
211
	return Object.prototype.toString.call(obj) === '[object Array]';
212
}
213
 
214
/**
215
 * Check for number
216
 * @param {Object} n
217
 */
218
function isNumber(n) {
219
	return typeof n === 'number';
220
}
221
 
222
function log2lin(num) {
223
	return math.log(num) / math.LN10;
224
}
225
function lin2log(num) {
226
	return math.pow(10, num);
227
}
228
 
229
/**
230
 * Remove last occurence of an item from an array
231
 * @param {Array} arr
232
 * @param {Mixed} item
233
 */
234
function erase(arr, item) {
235
	var i = arr.length;
236
	while (i--) {
237
		if (arr[i] === item) {
238
			arr.splice(i, 1);
239
			break;
240
		}
241
	}
242
	//return arr;
243
}
244
 
245
/**
246
 * Returns true if the object is not null or undefined. Like MooTools' $.defined.
247
 * @param {Object} obj
248
 */
249
function defined(obj) {
250
	return obj !== UNDEFINED && obj !== null;
251
}
252
 
253
/**
254
 * Set or get an attribute or an object of attributes. Can't use jQuery attr because
255
 * it attempts to set expando properties on the SVG element, which is not allowed.
256
 *
257
 * @param {Object} elem The DOM element to receive the attribute(s)
258
 * @param {String|Object} prop The property or an abject of key-value pairs
259
 * @param {String} value The value if a single property is set
260
 */
261
function attr(elem, prop, value) {
262
	var key,
263
		ret;
264
 
265
	// if the prop is a string
266
	if (isString(prop)) {
267
		// set the value
268
		if (defined(value)) {
269
			elem.setAttribute(prop, value);
270
 
271
		// get the value
272
		} else if (elem && elem.getAttribute) { // elem not defined when printing pie demo...
273
			ret = elem.getAttribute(prop);
274
		}
275
 
276
	// else if prop is defined, it is a hash of key/value pairs
277
	} else if (defined(prop) && isObject(prop)) {
278
		for (key in prop) {
279
			elem.setAttribute(key, prop[key]);
280
		}
281
	}
282
	return ret;
283
}
284
/**
285
 * Check if an element is an array, and if not, make it into an array. Like
286
 * MooTools' $.splat.
287
 */
288
function splat(obj) {
289
	return isArray(obj) ? obj : [obj];
290
}
291
 
292
 
293
/**
294
 * Return the first value that is defined. Like MooTools' $.pick.
295
 */
296
var pick = Highcharts.pick = function () {
297
	var args = arguments,
298
		i,
299
		arg,
300
		length = args.length;
301
	for (i = 0; i < length; i++) {
302
		arg = args[i];
303
		if (arg !== UNDEFINED && arg !== null) {
304
			return arg;
305
		}
306
	}
307
};
308
 
309
/**
310
 * Set CSS on a given element
311
 * @param {Object} el
312
 * @param {Object} styles Style object with camel case property names
313
 */
314
function css(el, styles) {
315
	if (isIE && !hasSVG) { // #2686
316
		if (styles && styles.opacity !== UNDEFINED) {
317
			styles.filter = 'alpha(opacity=' + (styles.opacity * 100) + ')';
318
		}
319
	}
320
	extend(el.style, styles);
321
}
322
 
323
/**
324
 * Utility function to create element with attributes and styles
325
 * @param {Object} tag
326
 * @param {Object} attribs
327
 * @param {Object} styles
328
 * @param {Object} parent
329
 * @param {Object} nopad
330
 */
331
function createElement(tag, attribs, styles, parent, nopad) {
332
	var el = doc.createElement(tag);
333
	if (attribs) {
334
		extend(el, attribs);
335
	}
336
	if (nopad) {
337
		css(el, {padding: 0, border: NONE, margin: 0});
338
	}
339
	if (styles) {
340
		css(el, styles);
341
	}
342
	if (parent) {
343
		parent.appendChild(el);
344
	}
345
	return el;
346
}
347
 
348
/**
349
 * Extend a prototyped class by new members
350
 * @param {Object} parent
351
 * @param {Object} members
352
 */
353
function extendClass(parent, members) {
354
	var object = function () { return UNDEFINED; };
355
	object.prototype = new parent();
356
	extend(object.prototype, members);
357
	return object;
358
}
359
 
360
/**
361
 * Pad a string to a given length by adding 0 to the beginning
362
 * @param {Number} number
363
 * @param {Number} length
364
 */
365
function pad(number, length) {
366
	// Create an array of the remaining length +1 and join it with 0's
367
	return new Array((length || 2) + 1 - String(number).length).join(0) + number;
368
}
369
 
370
/**
371
 * Return a length based on either the integer value, or a percentage of a base.
372
 */
373
function relativeLength (value, base) {
374
	return (/%$/).test(value) ? base * parseFloat(value) / 100 : parseFloat(value);
375
}
376
 
377
/**
378
 * Wrap a method with extended functionality, preserving the original function
379
 * @param {Object} obj The context object that the method belongs to
380
 * @param {String} method The name of the method to extend
381
 * @param {Function} func A wrapper function callback. This function is called with the same arguments
382
 * as the original function, except that the original function is unshifted and passed as the first
383
 * argument.
384
 */
385
var wrap = Highcharts.wrap = function (obj, method, func) {
386
	var proceed = obj[method];
387
	obj[method] = function () {
388
		var args = Array.prototype.slice.call(arguments);
389
		args.unshift(proceed);
390
		return func.apply(this, args);
391
	};
392
};
393
 
394
 
395
function getTZOffset(timestamp) {
396
	return ((getTimezoneOffset && getTimezoneOffset(timestamp)) || timezoneOffset || 0) * 60000;
397
}
398
 
399
/**
400
 * Based on http://www.php.net/manual/en/function.strftime.php
401
 * @param {String} format
402
 * @param {Number} timestamp
403
 * @param {Boolean} capitalize
404
 */
405
dateFormat = function (format, timestamp, capitalize) {
406
	if (!defined(timestamp) || isNaN(timestamp)) {
407
		return defaultOptions.lang.invalidDate || '';
408
	}
409
	format = pick(format, '%Y-%m-%d %H:%M:%S');
410
 
411
	var date = new Date(timestamp - getTZOffset(timestamp)),
412
		key, // used in for constuct below
413
		// get the basic time values
414
		hours = date[getHours](),
415
		day = date[getDay](),
416
		dayOfMonth = date[getDate](),
417
		month = date[getMonth](),
418
		fullYear = date[getFullYear](),
419
		lang = defaultOptions.lang,
420
		langWeekdays = lang.weekdays,
421
 
422
		// List all format keys. Custom formats can be added from the outside.
423
		replacements = extend({
424
 
425
			// Day
426
			'a': langWeekdays[day].substr(0, 3), // Short weekday, like 'Mon'
427
			'A': langWeekdays[day], // Long weekday, like 'Monday'
428
			'd': pad(dayOfMonth), // Two digit day of the month, 01 to 31
429
			'e': dayOfMonth, // Day of the month, 1 through 31
430
			'w': day,
431
 
432
			// Week (none implemented)
433
			//'W': weekNumber(),
434
 
435
			// Month
436
			'b': lang.shortMonths[month], // Short month, like 'Jan'
437
			'B': lang.months[month], // Long month, like 'January'
438
			'm': pad(month + 1), // Two digit month number, 01 through 12
439
 
440
			// Year
441
			'y': fullYear.toString().substr(2, 2), // Two digits year, like 09 for 2009
442
			'Y': fullYear, // Four digits year, like 2009
443
 
444
			// Time
445
			'H': pad(hours), // Two digits hours in 24h format, 00 through 23
446
			'k': hours, // Hours in 24h format, 0 through 23
447
			'I': pad((hours % 12) || 12), // Two digits hours in 12h format, 00 through 11
448
			'l': (hours % 12) || 12, // Hours in 12h format, 1 through 12
449
			'M': pad(date[getMinutes]()), // Two digits minutes, 00 through 59
450
			'p': hours < 12 ? 'AM' : 'PM', // Upper case AM or PM
451
			'P': hours < 12 ? 'am' : 'pm', // Lower case AM or PM
452
			'S': pad(date.getSeconds()), // Two digits seconds, 00 through  59
453
			'L': pad(mathRound(timestamp % 1000), 3) // Milliseconds (naming from Ruby)
454
		}, Highcharts.dateFormats);
455
 
456
 
457
	// do the replaces
458
	for (key in replacements) {
459
		while (format.indexOf('%' + key) !== -1) { // regex would do it in one line, but this is faster
460
			format = format.replace('%' + key, typeof replacements[key] === 'function' ? replacements[key](timestamp) : replacements[key]);
461
		}
462
	}
463
 
464
	// Optionally capitalize the string and return
465
	return capitalize ? format.substr(0, 1).toUpperCase() + format.substr(1) : format;
466
};
467
 
468
/**
469
 * Format a single variable. Similar to sprintf, without the % prefix.
470
 */
471
function formatSingle(format, val) {
472
	var floatRegex = /f$/,
473
		decRegex = /\.([0-9])/,
474
		lang = defaultOptions.lang,
475
		decimals;
476
 
477
	if (floatRegex.test(format)) { // float
478
		decimals = format.match(decRegex);
479
		decimals = decimals ? decimals[1] : -1;
480
		if (val !== null) {
481
			val = Highcharts.numberFormat(
482
				val,
483
				decimals,
484
				lang.decimalPoint,
485
				format.indexOf(',') > -1 ? lang.thousandsSep : ''
486
			);
487
		}
488
	} else {
489
		val = dateFormat(format, val);
490
	}
491
	return val;
492
}
493
 
494
/**
495
 * Format a string according to a subset of the rules of Python's String.format method.
496
 */
497
function format(str, ctx) {
498
	var splitter = '{',
499
		isInside = false,
500
		segment,
501
		valueAndFormat,
502
		path,
503
		i,
504
		len,
505
		ret = [],
506
		val,
507
		index;
508
 
509
	while ((index = str.indexOf(splitter)) !== -1) {
510
 
511
		segment = str.slice(0, index);
512
		if (isInside) { // we're on the closing bracket looking back
513
 
514
			valueAndFormat = segment.split(':');
515
			path = valueAndFormat.shift().split('.'); // get first and leave format
516
			len = path.length;
517
			val = ctx;
518
 
519
			// Assign deeper paths
520
			for (i = 0; i < len; i++) {
521
				val = val[path[i]];
522
			}
523
 
524
			// Format the replacement
525
			if (valueAndFormat.length) {
526
				val = formatSingle(valueAndFormat.join(':'), val);
527
			}
528
 
529
			// Push the result and advance the cursor
530
			ret.push(val);
531
 
532
		} else {
533
			ret.push(segment);
534
 
535
		}
536
		str = str.slice(index + 1); // the rest
537
		isInside = !isInside; // toggle
538
		splitter = isInside ? '}' : '{'; // now look for next matching bracket
539
	}
540
	ret.push(str);
541
	return ret.join('');
542
}
543
 
544
/**
545
 * Get the magnitude of a number
546
 */
547
function getMagnitude(num) {
548
	return math.pow(10, mathFloor(math.log(num) / math.LN10));
549
}
550
 
551
/**
552
 * Take an interval and normalize it to multiples of 1, 2, 2.5 and 5
553
 * @param {Number} interval
554
 * @param {Array} multiples
555
 * @param {Number} magnitude
556
 * @param {Object} options
557
 */
558
function normalizeTickInterval(interval, multiples, magnitude, allowDecimals, preventExceed) {
559
	var normalized,
560
		i,
561
		retInterval = interval;
562
 
563
	// round to a tenfold of 1, 2, 2.5 or 5
564
	magnitude = pick(magnitude, 1);
565
	normalized = interval / magnitude;
566
 
567
	// multiples for a linear scale
568
	if (!multiples) {
569
		multiples = [1, 2, 2.5, 5, 10];
570
 
571
		// the allowDecimals option
572
		if (allowDecimals === false) {
573
			if (magnitude === 1) {
574
				multiples = [1, 2, 5, 10];
575
			} else if (magnitude <= 0.1) {
576
				multiples = [1 / magnitude];
577
			}
578
		}
579
	}
580
 
581
	// normalize the interval to the nearest multiple
582
	for (i = 0; i < multiples.length; i++) {
583
		retInterval = multiples[i];
584
		if ((preventExceed && retInterval * magnitude >= interval) || // only allow tick amounts smaller than natural
585
			(!preventExceed && (normalized <= (multiples[i] + (multiples[i + 1] || multiples[i])) / 2))) {
586
			break;
587
		}
588
	}
589
 
590
	// multiply back to the correct magnitude
591
	retInterval *= magnitude;
592
 
593
	return retInterval;
594
}
595
 
596
 
597
/**
598
 * Utility method that sorts an object array and keeping the order of equal items.
599
 * ECMA script standard does not specify the behaviour when items are equal.
600
 */
601
function stableSort(arr, sortFunction) {
602
	var length = arr.length,
603
		sortValue,
604
		i;
605
 
606
	// Add index to each item
607
	for (i = 0; i < length; i++) {
608
		arr[i].ss_i = i; // stable sort index
609
	}
610
 
611
	arr.sort(function (a, b) {
612
		sortValue = sortFunction(a, b);
613
		return sortValue === 0 ? a.ss_i - b.ss_i : sortValue;
614
	});
615
 
616
	// Remove index from items
617
	for (i = 0; i < length; i++) {
618
		delete arr[i].ss_i; // stable sort index
619
	}
620
}
621
 
622
/**
623
 * Non-recursive method to find the lowest member of an array. Math.min raises a maximum
624
 * call stack size exceeded error in Chrome when trying to apply more than 150.000 points. This
625
 * method is slightly slower, but safe.
626
 */
627
function arrayMin(data) {
628
	var i = data.length,
629
		min = data[0];
630
 
631
	while (i--) {
632
		if (data[i] < min) {
633
			min = data[i];
634
		}
635
	}
636
	return min;
637
}
638
 
639
/**
640
 * Non-recursive method to find the lowest member of an array. Math.min raises a maximum
641
 * call stack size exceeded error in Chrome when trying to apply more than 150.000 points. This
642
 * method is slightly slower, but safe.
643
 */
644
function arrayMax(data) {
645
	var i = data.length,
646
		max = data[0];
647
 
648
	while (i--) {
649
		if (data[i] > max) {
650
			max = data[i];
651
		}
652
	}
653
	return max;
654
}
655
 
656
/**
657
 * Utility method that destroys any SVGElement or VMLElement that are properties on the given object.
658
 * It loops all properties and invokes destroy if there is a destroy method. The property is
659
 * then delete'ed.
660
 * @param {Object} The object to destroy properties on
661
 * @param {Object} Exception, do not destroy this property, only delete it.
662
 */
663
function destroyObjectProperties(obj, except) {
664
	var n;
665
	for (n in obj) {
666
		// If the object is non-null and destroy is defined
667
		if (obj[n] && obj[n] !== except && obj[n].destroy) {
668
			// Invoke the destroy
669
			obj[n].destroy();
670
		}
671
 
672
		// Delete the property from the object.
673
		delete obj[n];
674
	}
675
}
676
 
677
 
678
/**
679
 * Discard an element by moving it to the bin and delete
680
 * @param {Object} The HTML node to discard
681
 */
682
function discardElement(element) {
683
	// create a garbage bin element, not part of the DOM
684
	if (!garbageBin) {
685
		garbageBin = createElement(DIV);
686
	}
687
 
688
	// move the node and empty bin
689
	if (element) {
690
		garbageBin.appendChild(element);
691
	}
692
	garbageBin.innerHTML = '';
693
}
694
 
695
/**
696
 * Provide error messages for debugging, with links to online explanation
697
 */
698
function error (code, stop) {
699
	var msg = 'Highcharts error #' + code + ': www.highcharts.com/errors/' + code;
700
	if (stop) {
701
		throw msg;
702
	}
703
	// else ...
704
	if (win.console) {
705
		console.log(msg);
706
	}
707
}
708
 
709
/**
710
 * Fix JS round off float errors
711
 * @param {Number} num
712
 */
713
function correctFloat(num, prec) {
714
	return parseFloat(
715
		num.toPrecision(prec || 14)
716
	);
717
}
718
 
719
/**
720
 * Set the global animation to either a given value, or fall back to the
721
 * given chart's animation option
722
 * @param {Object} animation
723
 * @param {Object} chart
724
 */
725
function setAnimation(animation, chart) {
726
	chart.renderer.globalAnimation = pick(animation, chart.animation);
727
}
728
 
729
/**
730
 * The time unit lookup
731
 */
732
timeUnits = {
733
	millisecond: 1,
734
	second: 1000,
735
	minute: 60000,
736
	hour: 3600000,
737
	day: 24 * 3600000,
738
	week: 7 * 24 * 3600000,
739
	month: 28 * 24 * 3600000,
740
	year: 364 * 24 * 3600000
741
};
742
 
743
 
744
/**
745
 * Format a number and return a string based on input settings
746
 * @param {Number} number The input number to format
747
 * @param {Number} decimals The amount of decimals
748
 * @param {String} decPoint The decimal point, defaults to the one given in the lang options
749
 * @param {String} thousandsSep The thousands separator, defaults to the one given in the lang options
750
 */
751
Highcharts.numberFormat = function (number, decimals, decPoint, thousandsSep) {
752
	var lang = defaultOptions.lang,
753
		// http://kevin.vanzonneveld.net/techblog/article/javascript_equivalent_for_phps_number_format/
754
		n = +number || 0,
755
		c = decimals === -1 ?
756
			mathMin((n.toString().split('.')[1] || '').length, 20) : // Preserve decimals. Not huge numbers (#3793).
757
			(isNaN(decimals = mathAbs(decimals)) ? 2 : decimals),
758
		d = decPoint === undefined ? lang.decimalPoint : decPoint,
759
		t = thousandsSep === undefined ? lang.thousandsSep : thousandsSep,
760
		s = n < 0 ? "-" : "",
761
		i = String(pInt(n = mathAbs(n).toFixed(c))),
762
		j = i.length > 3 ? i.length % 3 : 0;
763
 
764
	return (s + (j ? i.substr(0, j) + t : "") + i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + t) +
765
			(c ? d + mathAbs(n - i).toFixed(c).slice(2) : ""));
766
};
767
/**
768
 * Path interpolation algorithm used across adapters
769
 */
770
pathAnim = {
771
	/**
772
	 * Prepare start and end values so that the path can be animated one to one
773
	 */
774
	init: function (elem, fromD, toD) {
775
		fromD = fromD || '';
776
		var shift = elem.shift,
777
			bezier = fromD.indexOf('C') > -1,
778
			numParams = bezier ? 7 : 3,
779
			endLength,
780
			slice,
781
			i,
782
			start = fromD.split(' '),
783
			end = [].concat(toD), // copy
784
			startBaseLine,
785
			endBaseLine,
786
			sixify = function (arr) { // in splines make move points have six parameters like bezier curves
787
				i = arr.length;
788
				while (i--) {
789
					if (arr[i] === M) {
790
						arr.splice(i + 1, 0, arr[i + 1], arr[i + 2], arr[i + 1], arr[i + 2]);
791
					}
792
				}
793
			};
794
 
795
		if (bezier) {
796
			sixify(start);
797
			sixify(end);
798
		}
799
 
800
		// pull out the base lines before padding
801
		if (elem.isArea) {
802
			startBaseLine = start.splice(start.length - 6, 6);
803
			endBaseLine = end.splice(end.length - 6, 6);
804
		}
805
 
806
		// if shifting points, prepend a dummy point to the end path
807
		if (shift <= end.length / numParams && start.length === end.length) {
808
			while (shift--) {
809
				end = [].concat(end).splice(0, numParams).concat(end);
810
			}
811
		}
812
		elem.shift = 0; // reset for following animations
813
 
814
		// copy and append last point until the length matches the end length
815
		if (start.length) {
816
			endLength = end.length;
817
			while (start.length < endLength) {
818
 
819
				//bezier && sixify(start);
820
				slice = [].concat(start).splice(start.length - numParams, numParams);
821
				if (bezier) { // disable first control point
822
					slice[numParams - 6] = slice[numParams - 2];
823
					slice[numParams - 5] = slice[numParams - 1];
824
				}
825
				start = start.concat(slice);
826
			}
827
		}
828
 
829
		if (startBaseLine) { // append the base lines for areas
830
			start = start.concat(startBaseLine);
831
			end = end.concat(endBaseLine);
832
		}
833
		return [start, end];
834
	},
835
 
836
	/**
837
	 * Interpolate each value of the path and return the array
838
	 */
839
	step: function (start, end, pos, complete) {
840
		var ret = [],
841
			i = start.length,
842
			startVal;
843
 
844
		if (pos === 1) { // land on the final path without adjustment points appended in the ends
845
			ret = complete;
846
 
847
		} else if (i === end.length && pos < 1) {
848
			while (i--) {
849
				startVal = parseFloat(start[i]);
850
				ret[i] =
851
					isNaN(startVal) ? // a letter instruction like M or L
852
						start[i] :
853
						pos * (parseFloat(end[i] - startVal)) + startVal;
854
 
855
			}
856
		} else { // if animation is finished or length not matching, land on right value
857
			ret = end;
858
		}
859
		return ret;
860
	}
861
};
862
 
863
(function ($) {
864
	/**
865
	 * The default HighchartsAdapter for jQuery
866
	 */
867
	win.HighchartsAdapter = win.HighchartsAdapter || ($ && {
868
 
869
		/**
870
		 * Initialize the adapter by applying some extensions to jQuery
871
		 */
872
		init: function (pathAnim) {
873
 
874
			// extend the animate function to allow SVG animations
875
			var Fx = $.fx;
876
 
877
			/*jslint unparam: true*//* allow unused param x in this function */
878
			$.extend($.easing, {
879
				easeOutQuad: function (x, t, b, c, d) {
880
					return -c * (t /= d) * (t - 2) + b;
881
				}
882
			});
883
			/*jslint unparam: false*/
884
 
885
			// extend some methods to check for elem.attr, which means it is a Highcharts SVG object
886
			$.each(['cur', '_default', 'width', 'height', 'opacity'], function (i, fn) {
887
				var obj = Fx.step,
888
					base;
889
 
890
				// Handle different parent objects
891
				if (fn === 'cur') {
892
					obj = Fx.prototype; // 'cur', the getter, relates to Fx.prototype
893
 
894
				} else if (fn === '_default' && $.Tween) { // jQuery 1.8 model
895
					obj = $.Tween.propHooks[fn];
896
					fn = 'set';
897
				}
898
 
899
				// Overwrite the method
900
				base = obj[fn];
901
				if (base) { // step.width and step.height don't exist in jQuery < 1.7
902
 
903
					// create the extended function replacement
904
					obj[fn] = function (fx) {
905
 
906
						var elem;
907
 
908
						// Fx.prototype.cur does not use fx argument
909
						fx = i ? fx : this;
910
 
911
						// Don't run animations on textual properties like align (#1821)
912
						if (fx.prop === 'align') {
913
							return;
914
						}
915
 
916
						// shortcut
917
						elem = fx.elem;
918
 
919
						// Fx.prototype.cur returns the current value. The other ones are setters
920
						// and returning a value has no effect.
921
						return elem.attr ? // is SVG element wrapper
922
							elem.attr(fx.prop, fn === 'cur' ? UNDEFINED : fx.now) : // apply the SVG wrapper's method
923
							base.apply(this, arguments); // use jQuery's built-in method
924
					};
925
				}
926
			});
927
 
928
			// Extend the opacity getter, needed for fading opacity with IE9 and jQuery 1.10+
929
			wrap($.cssHooks.opacity, 'get', function (proceed, elem, computed) {
930
				return elem.attr ? (elem.opacity || 0) : proceed.call(this, elem, computed);
931
			});
932
 
933
			// Define the setter function for d (path definitions)
934
			this.addAnimSetter('d', function (fx) {
935
				var elem = fx.elem,
936
					ends;
937
 
938
				// Normally start and end should be set in state == 0, but sometimes,
939
				// for reasons unknown, this doesn't happen. Perhaps state == 0 is skipped
940
				// in these cases
941
				if (!fx.started) {
942
					ends = pathAnim.init(elem, elem.d, elem.toD);
943
					fx.start = ends[0];
944
					fx.end = ends[1];
945
					fx.started = true;
946
				}
947
 
948
				// Interpolate each value of the path
949
				elem.attr('d', pathAnim.step(fx.start, fx.end, fx.pos, elem.toD));
950
			});
951
 
952
			/**
953
			 * Utility for iterating over an array. Parameters are reversed compared to jQuery.
954
			 * @param {Array} arr
955
			 * @param {Function} fn
956
			 */
957
			this.each = Array.prototype.forEach ?
958
				function (arr, fn) { // modern browsers
959
					return Array.prototype.forEach.call(arr, fn);
960
 
961
				} :
962
				function (arr, fn) { // legacy
963
					var i,
964
						len = arr.length;
965
					for (i = 0; i < len; i++) {
966
						if (fn.call(arr[i], arr[i], i, arr) === false) {
967
							return i;
968
						}
969
					}
970
				};
971
 
972
			/**
973
			 * Register Highcharts as a plugin in the respective framework
974
			 */
975
			$.fn.highcharts = function () {
976
				var constr = 'Chart', // default constructor
977
					args = arguments,
978
					options,
979
					ret,
980
					chart;
981
 
982
				if (this[0]) {
983
 
984
					if (isString(args[0])) {
985
						constr = args[0];
986
						args = Array.prototype.slice.call(args, 1);
987
					}
988
					options = args[0];
989
 
990
					// Create the chart
991
					if (options !== UNDEFINED) {
992
						/*jslint unused:false*/
993
						options.chart = options.chart || {};
994
						options.chart.renderTo = this[0];
995
						chart = new Highcharts[constr](options, args[1]);
996
						ret = this;
997
						/*jslint unused:true*/
998
					}
999
 
1000
					// When called without parameters or with the return argument, get a predefined chart
1001
					if (options === UNDEFINED) {
1002
						ret = charts[attr(this[0], 'data-highcharts-chart')];
1003
					}
1004
				}
1005
 
1006
				return ret;
1007
			};
1008
 
1009
		},
1010
 
1011
		/**
1012
		 * Add an animation setter for a specific property
1013
		 */
1014
		addAnimSetter: function (prop, setter) {
1015
			// jQuery 1.8 style
1016
			if ($.Tween) {
1017
				$.Tween.propHooks[prop] = {
1018
					set: setter
1019
				};
1020
			// pre 1.8
1021
			} else {
1022
				$.fx.step[prop] = setter;
1023
			}
1024
		},
1025
 
1026
		/**
1027
		 * Downloads a script and executes a callback when done.
1028
		 * @param {String} scriptLocation
1029
		 * @param {Function} callback
1030
		 */
1031
		getScript: $.getScript,
1032
 
1033
		/**
1034
		 * Return the index of an item in an array, or -1 if not found
1035
		 */
1036
		inArray: $.inArray,
1037
 
1038
		/**
1039
		 * A direct link to jQuery methods. MooTools and Prototype adapters must be implemented for each case of method.
1040
		 * @param {Object} elem The HTML element
1041
		 * @param {String} method Which method to run on the wrapped element
1042
		 */
1043
		adapterRun: function (elem, method) {
1044
			return $(elem)[method]();
1045
		},
1046
 
1047
		/**
1048
		 * Filter an array
1049
		 */
1050
		grep: $.grep,
1051
 
1052
		/**
1053
		 * Map an array
1054
		 * @param {Array} arr
1055
		 * @param {Function} fn
1056
		 */
1057
		map: function (arr, fn) {
1058
			//return jQuery.map(arr, fn);
1059
			var results = [],
1060
				i = 0,
1061
				len = arr.length;
1062
			for (; i < len; i++) {
1063
				results[i] = fn.call(arr[i], arr[i], i, arr);
1064
			}
1065
			return results;
1066
 
1067
		},
1068
 
1069
		/**
1070
		 * Get the position of an element relative to the top left of the page
1071
		 */
1072
		offset: function (el) {
1073
			return $(el).offset();
1074
		},
1075
 
1076
		/**
1077
		 * Add an event listener
1078
		 * @param {Object} el A HTML element or custom object
1079
		 * @param {String} event The event type
1080
		 * @param {Function} fn The event handler
1081
		 */
1082
		addEvent: function (el, event, fn) {
1083
			$(el).bind(event, fn);
1084
		},
1085
 
1086
		/**
1087
		 * Remove event added with addEvent
1088
		 * @param {Object} el The object
1089
		 * @param {String} eventType The event type. Leave blank to remove all events.
1090
		 * @param {Function} handler The function to remove
1091
		 */
1092
		removeEvent: function (el, eventType, handler) {
1093
			// workaround for jQuery issue with unbinding custom events:
1094
			// http://forum.jQuery.com/topic/javascript-error-when-unbinding-a-custom-event-using-jQuery-1-4-2
1095
			var func = doc.removeEventListener ? 'removeEventListener' : 'detachEvent';
1096
			if (doc[func] && el && !el[func]) {
1097
				el[func] = function () {};
1098
			}
1099
 
1100
			$(el).unbind(eventType, handler);
1101
		},
1102
 
1103
		/**
1104
		 * Fire an event on a custom object
1105
		 * @param {Object} el
1106
		 * @param {String} type
1107
		 * @param {Object} eventArguments
1108
		 * @param {Function} defaultFunction
1109
		 */
1110
		fireEvent: function (el, type, eventArguments, defaultFunction) {
1111
			var event = $.Event(type),
1112
				detachedType = 'detached' + type,
1113
				defaultPrevented;
1114
 
1115
			// Remove warnings in Chrome when accessing returnValue (#2790), layerX and layerY. Although Highcharts
1116
			// never uses these properties, Chrome includes them in the default click event and
1117
			// raises the warning when they are copied over in the extend statement below.
1118
			//
1119
			// To avoid problems in IE (see #1010) where we cannot delete the properties and avoid
1120
			// testing if they are there (warning in chrome) the only option is to test if running IE.
1121
			if (!isIE && eventArguments) {
1122
				delete eventArguments.layerX;
1123
				delete eventArguments.layerY;
1124
				delete eventArguments.returnValue;
1125
			}
1126
 
1127
			extend(event, eventArguments);
1128
 
1129
			// Prevent jQuery from triggering the object method that is named the
1130
			// same as the event. For example, if the event is 'select', jQuery
1131
			// attempts calling el.select and it goes into a loop.
1132
			if (el[type]) {
1133
				el[detachedType] = el[type];
1134
				el[type] = null;
1135
			}
1136
 
1137
			// Wrap preventDefault and stopPropagation in try/catch blocks in
1138
			// order to prevent JS errors when cancelling events on non-DOM
1139
			// objects. #615.
1140
			/*jslint unparam: true*/
1141
			$.each(['preventDefault', 'stopPropagation'], function (i, fn) {
1142
				var base = event[fn];
1143
				event[fn] = function () {
1144
					try {
1145
						base.call(event);
1146
					} catch (e) {
1147
						if (fn === 'preventDefault') {
1148
							defaultPrevented = true;
1149
						}
1150
					}
1151
				};
1152
			});
1153
			/*jslint unparam: false*/
1154
 
1155
			// trigger it
1156
			$(el).trigger(event);
1157
 
1158
			// attach the method
1159
			if (el[detachedType]) {
1160
				el[type] = el[detachedType];
1161
				el[detachedType] = null;
1162
			}
1163
 
1164
			if (defaultFunction && !event.isDefaultPrevented() && !defaultPrevented) {
1165
				defaultFunction(event);
1166
			}
1167
		},
1168
 
1169
		/**
1170
		 * Extension method needed for MooTools
1171
		 */
1172
		washMouseEvent: function (e) {
1173
			var ret = e.originalEvent || e;
1174
 
1175
			// computed by jQuery, needed by IE8
1176
			if (ret.pageX === UNDEFINED) { // #1236
1177
				ret.pageX = e.pageX;
1178
				ret.pageY = e.pageY;
1179
			}
1180
 
1181
			return ret;
1182
		},
1183
 
1184
		/**
1185
		 * Animate a HTML element or SVG element wrapper
1186
		 * @param {Object} el
1187
		 * @param {Object} params
1188
		 * @param {Object} options jQuery-like animation options: duration, easing, callback
1189
		 */
1190
		animate: function (el, params, options) {
1191
			var $el = $(el);
1192
			if (!el.style) {
1193
				el.style = {}; // #1881
1194
			}
1195
			if (params.d) {
1196
				el.toD = params.d; // keep the array form for paths, used in $.fx.step.d
1197
				params.d = 1; // because in jQuery, animating to an array has a different meaning
1198
			}
1199
 
1200
			$el.stop();
1201
			if (params.opacity !== UNDEFINED && el.attr) {
1202
				params.opacity += 'px'; // force jQuery to use same logic as width and height (#2161)
1203
			}
1204
			el.hasAnim = 1; // #3342
1205
			$el.animate(params, options);
1206
 
1207
		},
1208
		/**
1209
		 * Stop running animation
1210
		 */
1211
		stop: function (el) {
1212
			if (el.hasAnim) { // #3342, memory leak on calling $(el) from destroy
1213
				$(el).stop();
1214
			}
1215
		}
1216
	});
1217
}(win.jQuery));
1218
 
1219
 
1220
// check for a custom HighchartsAdapter defined prior to this file
1221
var globalAdapter = win.HighchartsAdapter,
1222
	adapter = globalAdapter || {};
1223
 
1224
// Initialize the adapter
1225
if (globalAdapter) {
1226
	globalAdapter.init.call(globalAdapter, pathAnim);
1227
}
1228
 
1229
 
1230
// Utility functions. If the HighchartsAdapter is not defined, adapter is an empty object
1231
// and all the utility functions will be null. In that case they are populated by the
1232
// default adapters below.
1233
var adapterRun = adapter.adapterRun,
1234
	getScript = adapter.getScript,
1235
	inArray = adapter.inArray,
1236
	each = Highcharts.each = adapter.each,
1237
	grep = adapter.grep,
1238
	offset = adapter.offset,
1239
	map = adapter.map,
1240
	addEvent = adapter.addEvent,
1241
	removeEvent = adapter.removeEvent,
1242
	fireEvent = adapter.fireEvent,
1243
	washMouseEvent = adapter.washMouseEvent,
1244
	animate = adapter.animate,
1245
	stop = adapter.stop;
1246
 
1247
 
1248
 
1249
/* ****************************************************************************
1250
 * Handle the options                                                         *
1251
 *****************************************************************************/
1252
defaultOptions = {
1253
	colors: ['#7cb5ec', '#434348', '#90ed7d', '#f7a35c',
1254
		    '#8085e9', '#f15c80', '#e4d354', '#2b908f', '#f45b5b', '#91e8e1'],
1255
	symbols: ['circle', 'diamond', 'square', 'triangle', 'triangle-down'],
1256
	lang: {
1257
		loading: 'Loading...',
1258
		months: ['January', 'February', 'March', 'April', 'May', 'June', 'July',
1259
				'August', 'September', 'October', 'November', 'December'],
1260
		shortMonths: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
1261
		weekdays: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
1262
		// invalidDate: '',
1263
		decimalPoint: '.',
1264
		numericSymbols: ['k', 'M', 'G', 'T', 'P', 'E'], // SI prefixes used in axis labels
1265
		resetZoom: 'Reset zoom',
1266
		resetZoomTitle: 'Reset zoom level 1:1',
1267
		thousandsSep: ' '
1268
	},
1269
	global: {
1270
		useUTC: true,
1271
		//timezoneOffset: 0,
1272
		canvasToolsURL: 'http://code.highcharts.com/stock/2.1.8/modules/canvas-tools.js',
1273
		VMLRadialGradientURL: 'http://code.highcharts.com/stock/2.1.8/gfx/vml-radial-gradient.png'
1274
	},
1275
	chart: {
1276
		//animation: true,
1277
		//alignTicks: false,
1278
		//reflow: true,
1279
		//className: null,
1280
		//events: { load, selection },
1281
		//margin: [null],
1282
		//marginTop: null,
1283
		//marginRight: null,
1284
		//marginBottom: null,
1285
		//marginLeft: null,
1286
		borderColor: '#4572A7',
1287
		//borderWidth: 0,
1288
		borderRadius: 0,
1289
		defaultSeriesType: 'line',
1290
		ignoreHiddenSeries: true,
1291
		//inverted: false,
1292
		//shadow: false,
1293
		spacing: [10, 10, 15, 10],
1294
		//spacingTop: 10,
1295
		//spacingRight: 10,
1296
		//spacingBottom: 15,
1297
		//spacingLeft: 10,
1298
		//style: {
1299
		//	fontFamily: '"Lucida Grande", "Lucida Sans Unicode", Verdana, Arial, Helvetica, sans-serif', // default font
1300
		//	fontSize: '12px'
1301
		//},
1302
		backgroundColor: '#FFFFFF',
1303
		//plotBackgroundColor: null,
1304
		plotBorderColor: '#C0C0C0',
1305
		//plotBorderWidth: 0,
1306
		//plotShadow: false,
1307
		//zoomType: ''
1308
		resetZoomButton: {
1309
			theme: {
1310
				zIndex: 20
1311
			},
1312
			position: {
1313
				align: 'right',
1314
				x: -10,
1315
				//verticalAlign: 'top',
1316
				y: 10
1317
			}
1318
			// relativeTo: 'plot'
1319
		}
1320
	},
1321
	title: {
1322
		text: 'Chart title',
1323
		align: 'center',
1324
		// floating: false,
1325
		margin: 15,
1326
		// x: 0,
1327
		// verticalAlign: 'top',
1328
		// y: null,
1329
		style: {
1330
			color: '#333333',
1331
			fontSize: '18px'
1332
		}
1333
 
1334
	},
1335
	subtitle: {
1336
		text: '',
1337
		align: 'center',
1338
		// floating: false
1339
		// x: 0,
1340
		// verticalAlign: 'top',
1341
		// y: null,
1342
		style: {
1343
			color: '#555555'
1344
		}
1345
	},
1346
 
1347
	plotOptions: {
1348
		line: { // base series options
1349
			allowPointSelect: false,
1350
			showCheckbox: false,
1351
			animation: {
1352
				duration: 1000
1353
			},
1354
			//connectNulls: false,
1355
			//cursor: 'default',
1356
			//clip: true,
1357
			//dashStyle: null,
1358
			//enableMouseTracking: true,
1359
			events: {},
1360
			//legendIndex: 0,
1361
			//linecap: 'round',
1362
			lineWidth: 2,
1363
			//shadow: false,
1364
			// stacking: null,
1365
			marker: {
1366
				//enabled: true,
1367
				//symbol: null,
1368
				lineWidth: 0,
1369
				radius: 4,
1370
				lineColor: '#FFFFFF',
1371
				//fillColor: null,
1372
				states: { // states for a single point
1373
					hover: {
1374
						enabled: true,
1375
						lineWidthPlus: 1,
1376
						radiusPlus: 2
1377
					},
1378
					select: {
1379
						fillColor: '#FFFFFF',
1380
						lineColor: '#000000',
1381
						lineWidth: 2
1382
					}
1383
				}
1384
			},
1385
			point: {
1386
				events: {}
1387
			},
1388
			dataLabels: {
1389
				align: 'center',
1390
				// defer: true,
1391
				// enabled: false,
1392
				formatter: function () {
1393
					return this.y === null ? '' : Highcharts.numberFormat(this.y, -1);
1394
				},
1395
				style: {
1396
					color: 'contrast',
1397
					fontSize: '11px',
1398
					fontWeight: 'bold',
1399
					textShadow: '0 0 6px contrast, 0 0 3px contrast'
1400
				},
1401
				verticalAlign: 'bottom', // above singular point
1402
				x: 0,
1403
				y: 0,
1404
				// backgroundColor: undefined,
1405
				// borderColor: undefined,
1406
				// borderRadius: undefined,
1407
				// borderWidth: undefined,
1408
				padding: 5
1409
				// shadow: false
1410
			},
1411
			cropThreshold: 300, // draw points outside the plot area when the number of points is less than this
1412
			pointRange: 0,
1413
			//pointStart: 0,
1414
			//pointInterval: 1,
1415
			//showInLegend: null, // auto: true for standalone series, false for linked series
1416
			states: { // states for the entire series
1417
				hover: {
1418
					//enabled: false,
1419
					lineWidthPlus: 1,
1420
					marker: {
1421
						// lineWidth: base + 1,
1422
						// radius: base + 1
1423
					},
1424
					halo: {
1425
						size: 10,
1426
						opacity: 0.25
1427
					}
1428
				},
1429
				select: {
1430
					marker: {}
1431
				}
1432
			},
1433
			stickyTracking: true,
1434
			//tooltip: {
1435
				//pointFormat: '<span style="color:{point.color}">\u25CF</span> {series.name}: <b>{point.y}</b>'
1436
				//valueDecimals: null,
1437
				//xDateFormat: '%A, %b %e, %Y',
1438
				//valuePrefix: '',
1439
				//ySuffix: ''
1440
			//}
1441
			turboThreshold: 1000
1442
			// zIndex: null
1443
		}
1444
	},
1445
	labels: {
1446
		//items: [],
1447
		style: {
1448
			//font: defaultFont,
1449
			position: ABSOLUTE,
1450
			color: '#3E576F'
1451
		}
1452
	},
1453
	legend: {
1454
		enabled: true,
1455
		align: 'center',
1456
		//floating: false,
1457
		layout: 'horizontal',
1458
		labelFormatter: function () {
1459
			return this.name;
1460
		},
1461
		//borderWidth: 0,
1462
		borderColor: '#909090',
1463
		borderRadius: 0,
1464
		navigation: {
1465
			// animation: true,
1466
			activeColor: '#274b6d',
1467
			// arrowSize: 12
1468
			inactiveColor: '#CCC'
1469
			// style: {} // text styles
1470
		},
1471
		// margin: 20,
1472
		// reversed: false,
1473
		shadow: false,
1474
		// backgroundColor: null,
1475
		/*style: {
1476
			padding: '5px'
1477
		},*/
1478
		itemStyle: {
1479
			color: '#333333',
1480
			fontSize: '12px',
1481
			fontWeight: 'bold'
1482
		},
1483
		itemHoverStyle: {
1484
			//cursor: 'pointer', removed as of #601
1485
			color: '#000'
1486
		},
1487
		itemHiddenStyle: {
1488
			color: '#CCC'
1489
		},
1490
		itemCheckboxStyle: {
1491
			position: ABSOLUTE,
1492
			width: '13px', // for IE precision
1493
			height: '13px'
1494
		},
1495
		// itemWidth: undefined,
1496
		// symbolRadius: 0,
1497
		// symbolWidth: 16,
1498
		symbolPadding: 5,
1499
		verticalAlign: 'bottom',
1500
		// width: undefined,
1501
		x: 0,
1502
		y: 0,
1503
		title: {
1504
			//text: null,
1505
			style: {
1506
				fontWeight: 'bold'
1507
			}
1508
		}
1509
	},
1510
 
1511
	loading: {
1512
		// hideDuration: 100,
1513
		labelStyle: {
1514
			fontWeight: 'bold',
1515
			position: RELATIVE,
1516
			top: '45%'
1517
		},
1518
		// showDuration: 0,
1519
		style: {
1520
			position: ABSOLUTE,
1521
			backgroundColor: 'white',
1522
			opacity: 0.5,
1523
			textAlign: 'center'
1524
		}
1525
	},
1526
 
1527
	tooltip: {
1528
		enabled: true,
1529
		animation: hasSVG,
1530
		//crosshairs: null,
1531
		backgroundColor: 'rgba(249, 249, 249, .85)',
1532
		borderWidth: 1,
1533
		borderRadius: 3,
1534
		dateTimeLabelFormats: {
1535
			millisecond: '%A, %b %e, %H:%M:%S.%L',
1536
			second: '%A, %b %e, %H:%M:%S',
1537
			minute: '%A, %b %e, %H:%M',
1538
			hour: '%A, %b %e, %H:%M',
1539
			day: '%A, %b %e, %Y',
1540
			week: 'Week from %A, %b %e, %Y',
1541
			month: '%B %Y',
1542
			year: '%Y'
1543
		},
1544
		footerFormat: '',
1545
		//formatter: defaultFormatter,
1546
		headerFormat: '<span style="font-size: 10px">{point.key}</span><br/>',
1547
		pointFormat: '<span style="color:{point.color}">\u25CF</span> {series.name}: <b>{point.y}</b><br/>',
1548
		shadow: true,
1549
		//shape: 'callout',
1550
		//shared: false,
1551
		snap: isTouchDevice ? 25 : 10,
1552
		style: {
1553
			color: '#333333',
1554
			cursor: 'default',
1555
			fontSize: '12px',
1556
			padding: '8px',
1557
			whiteSpace: 'nowrap'
1558
		}
1559
		//xDateFormat: '%A, %b %e, %Y',
1560
		//valueDecimals: null,
1561
		//valuePrefix: '',
1562
		//valueSuffix: ''
1563
	},
1564
 
1565
	credits: {
1566
		enabled: true,
1567
		text: 'Highcharts.com',
1568
		href: 'http://www.highcharts.com',
1569
		position: {
1570
			align: 'right',
1571
			x: -10,
1572
			verticalAlign: 'bottom',
1573
			y: -5
1574
		},
1575
		style: {
1576
			cursor: 'pointer',
1577
			color: '#909090',
1578
			fontSize: '9px'
1579
		}
1580
	}
1581
};
1582
 
1583
 
1584
 
1585
 
1586
// Series defaults
1587
var defaultPlotOptions = defaultOptions.plotOptions,
1588
	defaultSeriesOptions = defaultPlotOptions.line;
1589
 
1590
// set the default time methods
1591
setTimeMethods();
1592
 
1593
 
1594
 
1595
/**
1596
 * Set the time methods globally based on the useUTC option. Time method can be either
1597
 * local time or UTC (default).
1598
 */
1599
function setTimeMethods() {
1600
	var globalOptions = defaultOptions.global,
1601
		useUTC = globalOptions.useUTC,
1602
		GET = useUTC ? 'getUTC' : 'get',
1603
		SET = useUTC ? 'setUTC' : 'set';
1604
 
1605
 
1606
	Date = globalOptions.Date || window.Date;
1607
	timezoneOffset = useUTC && globalOptions.timezoneOffset;
1608
	getTimezoneOffset = useUTC && globalOptions.getTimezoneOffset;
1609
	makeTime = function (year, month, date, hours, minutes, seconds) {
1610
		var d;
1611
		if (useUTC) {
1612
			d = Date.UTC.apply(0, arguments);
1613
			d += getTZOffset(d);
1614
		} else {
1615
			d = new Date(
1616
				year,
1617
				month,
1618
				pick(date, 1),
1619
				pick(hours, 0),
1620
				pick(minutes, 0),
1621
				pick(seconds, 0)
1622
			).getTime();
1623
		}
1624
		return d;
1625
	};
1626
	getMinutes =      GET + 'Minutes';
1627
	getHours =        GET + 'Hours';
1628
	getDay =          GET + 'Day';
1629
	getDate =         GET + 'Date';
1630
	getMonth =        GET + 'Month';
1631
	getFullYear =     GET + 'FullYear';
1632
	setMilliseconds = SET + 'Milliseconds';
1633
	setSeconds =      SET + 'Seconds';
1634
	setMinutes =      SET + 'Minutes';
1635
	setHours =        SET + 'Hours';
1636
	setDate =         SET + 'Date';
1637
	setMonth =        SET + 'Month';
1638
	setFullYear =     SET + 'FullYear';
1639
 
1640
}
1641
 
1642
/**
1643
 * Merge the default options with custom options and return the new options structure
1644
 * @param {Object} options The new custom options
1645
 */
1646
function setOptions(options) {
1647
 
1648
	// Copy in the default options
1649
	defaultOptions = merge(true, defaultOptions, options);
1650
 
1651
	// Apply UTC
1652
	setTimeMethods();
1653
 
1654
	return defaultOptions;
1655
}
1656
 
1657
/**
1658
 * Get the updated default options. Until 3.0.7, merely exposing defaultOptions for outside modules
1659
 * wasn't enough because the setOptions method created a new object.
1660
 */
1661
function getOptions() {
1662
	return defaultOptions;
1663
}
1664
 
1665
 
1666
/**
1667
 * Handle color operations. The object methods are chainable.
1668
 * @param {String} input The input color in either rbga or hex format
1669
 */
1670
var rgbaRegEx = /rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]?(?:\.[0-9]+)?)\s*\)/,
1671
	hexRegEx = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/,
1672
	rgbRegEx = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/;
1673
 
1674
var Color = function (input) {
1675
	// declare variables
1676
	var rgba = [], result, stops;
1677
 
1678
	/**
1679
	 * Parse the input color to rgba array
1680
	 * @param {String} input
1681
	 */
1682
	function init(input) {
1683
 
1684
		// Gradients
1685
		if (input && input.stops) {
1686
			stops = map(input.stops, function (stop) {
1687
				return Color(stop[1]);
1688
			});
1689
 
1690
		// Solid colors
1691
		} else {
1692
			// rgba
1693
			result = rgbaRegEx.exec(input);
1694
			if (result) {
1695
				rgba = [pInt(result[1]), pInt(result[2]), pInt(result[3]), parseFloat(result[4], 10)];
1696
			} else {
1697
				// hex
1698
				result = hexRegEx.exec(input);
1699
				if (result) {
1700
					rgba = [pInt(result[1], 16), pInt(result[2], 16), pInt(result[3], 16), 1];
1701
				} else {
1702
					// rgb
1703
					result = rgbRegEx.exec(input);
1704
					if (result) {
1705
						rgba = [pInt(result[1]), pInt(result[2]), pInt(result[3]), 1];
1706
					}
1707
				}
1708
			}
1709
		}
1710
 
1711
	}
1712
	/**
1713
	 * Return the color a specified format
1714
	 * @param {String} format
1715
	 */
1716
	function get(format) {
1717
		var ret;
1718
 
1719
		if (stops) {
1720
			ret = merge(input);
1721
			ret.stops = [].concat(ret.stops);
1722
			each(stops, function (stop, i) {
1723
				ret.stops[i] = [ret.stops[i][0], stop.get(format)];
1724
			});
1725
 
1726
		// it's NaN if gradient colors on a column chart
1727
		} else if (rgba && !isNaN(rgba[0])) {
1728
			if (format === 'rgb') {
1729
				ret = 'rgb(' + rgba[0] + ',' + rgba[1] + ',' + rgba[2] + ')';
1730
			} else if (format === 'a') {
1731
				ret = rgba[3];
1732
			} else {
1733
				ret = 'rgba(' + rgba.join(',') + ')';
1734
			}
1735
		} else {
1736
			ret = input;
1737
		}
1738
		return ret;
1739
	}
1740
 
1741
	/**
1742
	 * Brighten the color
1743
	 * @param {Number} alpha
1744
	 */
1745
	function brighten(alpha) {
1746
		if (stops) {
1747
			each(stops, function (stop) {
1748
				stop.brighten(alpha);
1749
			});
1750
 
1751
		} else if (isNumber(alpha) && alpha !== 0) {
1752
			var i;
1753
			for (i = 0; i < 3; i++) {
1754
				rgba[i] += pInt(alpha * 255);
1755
 
1756
				if (rgba[i] < 0) {
1757
					rgba[i] = 0;
1758
				}
1759
				if (rgba[i] > 255) {
1760
					rgba[i] = 255;
1761
				}
1762
			}
1763
		}
1764
		return this;
1765
	}
1766
	/**
1767
	 * Set the color's opacity to a given alpha value
1768
	 * @param {Number} alpha
1769
	 */
1770
	function setOpacity(alpha) {
1771
		rgba[3] = alpha;
1772
		return this;
1773
	}
1774
 
1775
	// initialize: parse the input
1776
	init(input);
1777
 
1778
	// public methods
1779
	return {
1780
		get: get,
1781
		brighten: brighten,
1782
		rgba: rgba,
1783
		setOpacity: setOpacity,
1784
		raw: input
1785
	};
1786
};
1787
 
1788
 
1789
/**
1790
 * A wrapper object for SVG elements
1791
 */
1792
function SVGElement() {}
1793
 
1794
SVGElement.prototype = {
1795
 
1796
	// Default base for animation
1797
	opacity: 1,
1798
	// For labels, these CSS properties are applied to the <text> node directly
1799
	textProps: ['fontSize', 'fontWeight', 'fontFamily', 'fontStyle', 'color',
1800
		'lineHeight', 'width', 'textDecoration', 'textOverflow', 'textShadow'],
1801
 
1802
	/**
1803
	 * Initialize the SVG renderer
1804
	 * @param {Object} renderer
1805
	 * @param {String} nodeName
1806
	 */
1807
	init: function (renderer, nodeName) {
1808
		var wrapper = this;
1809
		wrapper.element = nodeName === 'span' ?
1810
			createElement(nodeName) :
1811
			doc.createElementNS(SVG_NS, nodeName);
1812
		wrapper.renderer = renderer;
1813
	},
1814
 
1815
	/**
1816
	 * Animate a given attribute
1817
	 * @param {Object} params
1818
	 * @param {Number} options The same options as in jQuery animation
1819
	 * @param {Function} complete Function to perform at the end of animation
1820
	 */
1821
	animate: function (params, options, complete) {
1822
		var animOptions = pick(options, this.renderer.globalAnimation, true);
1823
		stop(this); // stop regardless of animation actually running, or reverting to .attr (#607)
1824
		if (animOptions) {
1825
			animOptions = merge(animOptions, {}); //#2625
1826
			if (complete) { // allows using a callback with the global animation without overwriting it
1827
				animOptions.complete = complete;
1828
			}
1829
			animate(this, params, animOptions);
1830
		} else {
1831
			this.attr(params, null, complete);
1832
		}
1833
		return this;
1834
	},
1835
 
1836
	/**
1837
	 * Build an SVG gradient out of a common JavaScript configuration object
1838
	 */
1839
	colorGradient: function (color, prop, elem) {
1840
		var renderer = this.renderer,
1841
			colorObject,
1842
			gradName,
1843
			gradAttr,
1844
			gradients,
1845
			gradientObject,
1846
			stops,
1847
			stopColor,
1848
			stopOpacity,
1849
			radialReference,
1850
			n,
1851
			id,
1852
			key = [];
1853
 
1854
		// Apply linear or radial gradients
1855
		if (color.linearGradient) {
1856
			gradName = 'linearGradient';
1857
		} else if (color.radialGradient) {
1858
			gradName = 'radialGradient';
1859
		}
1860
 
1861
		if (gradName) {
1862
			gradAttr = color[gradName];
1863
			gradients = renderer.gradients;
1864
			stops = color.stops;
1865
			radialReference = elem.radialReference;
1866
 
1867
			// Keep < 2.2 kompatibility
1868
			if (isArray(gradAttr)) {
1869
				color[gradName] = gradAttr = {
1870
					x1: gradAttr[0],
1871
					y1: gradAttr[1],
1872
					x2: gradAttr[2],
1873
					y2: gradAttr[3],
1874
					gradientUnits: 'userSpaceOnUse'
1875
				};
1876
			}
1877
 
1878
			// Correct the radial gradient for the radial reference system
1879
			if (gradName === 'radialGradient' && radialReference && !defined(gradAttr.gradientUnits)) {
1880
				gradAttr = merge(gradAttr, {
1881
					cx: (radialReference[0] - radialReference[2] / 2) + gradAttr.cx * radialReference[2],
1882
					cy: (radialReference[1] - radialReference[2] / 2) + gradAttr.cy * radialReference[2],
1883
					r: gradAttr.r * radialReference[2],
1884
					gradientUnits: 'userSpaceOnUse'
1885
				});
1886
			}
1887
 
1888
			// Build the unique key to detect whether we need to create a new element (#1282)
1889
			for (n in gradAttr) {
1890
				if (n !== 'id') {
1891
					key.push(n, gradAttr[n]);
1892
				}
1893
			}
1894
			for (n in stops) {
1895
				key.push(stops[n]);
1896
			}
1897
			key = key.join(',');
1898
 
1899
			// Check if a gradient object with the same config object is created within this renderer
1900
			if (gradients[key]) {
1901
				id = gradients[key].attr('id');
1902
 
1903
			} else {
1904
 
1905
				// Set the id and create the element
1906
				gradAttr.id = id = PREFIX + idCounter++;
1907
				gradients[key] = gradientObject = renderer.createElement(gradName)
1908
					.attr(gradAttr)
1909
					.add(renderer.defs);
1910
 
1911
 
1912
				// The gradient needs to keep a list of stops to be able to destroy them
1913
				gradientObject.stops = [];
1914
				each(stops, function (stop) {
1915
					var stopObject;
1916
					if (stop[1].indexOf('rgba') === 0) {
1917
						colorObject = Color(stop[1]);
1918
						stopColor = colorObject.get('rgb');
1919
						stopOpacity = colorObject.get('a');
1920
					} else {
1921
						stopColor = stop[1];
1922
						stopOpacity = 1;
1923
					}
1924
					stopObject = renderer.createElement('stop').attr({
1925
						offset: stop[0],
1926
						'stop-color': stopColor,
1927
						'stop-opacity': stopOpacity
1928
					}).add(gradientObject);
1929
 
1930
					// Add the stop element to the gradient
1931
					gradientObject.stops.push(stopObject);
1932
				});
1933
			}
1934
 
1935
			// Set the reference to the gradient object
1936
			elem.setAttribute(prop, 'url(' + renderer.url + '#' + id + ')');
1937
		}
1938
	},
1939
 
1940
	/**
1941
	 * Apply a polyfill to the text-stroke CSS property, by copying the text element
1942
	 * and apply strokes to the copy.
1943
	 *
1944
	 * Contrast checks at http://jsfiddle.net/highcharts/43soe9m1/2/
1945
	 *
1946
	 * docs: update default, document the polyfill and the limitations on hex colors and pixel values, document contrast pseudo-color
1947
	 */
1948
	applyTextShadow: function (textShadow) {
1949
		var elem = this.element,
1950
			tspans,
1951
			hasContrast = textShadow.indexOf('contrast') !== -1,
1952
			styles = {},
1953
			// IE10 and IE11 report textShadow in elem.style even though it doesn't work. Check
1954
			// this again with new IE release. In exports, the rendering is passed to PhantomJS.
1955
			supports = this.renderer.forExport || (elem.style.textShadow !== UNDEFINED && !isIE);
1956
 
1957
		// When the text shadow is set to contrast, use dark stroke for light text and vice versa
1958
		if (hasContrast) {
1959
			styles.textShadow = textShadow = textShadow.replace(/contrast/g, this.renderer.getContrast(elem.style.fill));
1960
		}
1961
 
1962
		// Safari with retina displays as well as PhantomJS bug (#3974). Firefox does not tolerate this,
1963
		// it removes the text shadows.
1964
		if (isWebKit) {
1965
			styles.textRendering = 'geometricPrecision';
1966
		}
1967
 
1968
		/* Selective side-by-side testing in supported browser (http://jsfiddle.net/highcharts/73L1ptrh/)
1969
		if (elem.textContent.indexOf('2.') === 0) {
1970
			elem.style['text-shadow'] = 'none';
1971
			supports = false;
1972
		}
1973
		// */
1974
 
1975
		// No reason to polyfill, we've got native support
1976
		if (supports) {
1977
			css(elem, styles); // Apply altered textShadow or textRendering workaround
1978
		} else {
1979
 
1980
			this.fakeTS = true; // Fake text shadow
1981
 
1982
			// In order to get the right y position of the clones,
1983
			// copy over the y setter
1984
			this.ySetter = this.xSetter;
1985
 
1986
			tspans = [].slice.call(elem.getElementsByTagName('tspan'));
1987
			each(textShadow.split(/\s?,\s?/g), function (textShadow) {
1988
				var firstChild = elem.firstChild,
1989
					color,
1990
					strokeWidth;
1991
 
1992
				textShadow = textShadow.split(' ');
1993
				color = textShadow[textShadow.length - 1];
1994
 
1995
				// Approximately tune the settings to the text-shadow behaviour
1996
				strokeWidth = textShadow[textShadow.length - 2];
1997
 
1998
				if (strokeWidth) {
1999
					each(tspans, function (tspan, y) {
2000
						var clone;
2001
 
2002
						// Let the first line start at the correct X position
2003
						if (y === 0) {
2004
							tspan.setAttribute('x', elem.getAttribute('x'));
2005
							y = elem.getAttribute('y');
2006
							tspan.setAttribute('y', y || 0);
2007
							if (y === null) {
2008
								elem.setAttribute('y', 0);
2009
							}
2010
						}
2011
 
2012
						// Create the clone and apply shadow properties
2013
						clone = tspan.cloneNode(1);
2014
						attr(clone, {
2015
							'class': PREFIX + 'text-shadow',
2016
							'fill': color,
2017
							'stroke': color,
2018
							'stroke-opacity': 1 / mathMax(pInt(strokeWidth), 3),
2019
							'stroke-width': strokeWidth,
2020
							'stroke-linejoin': 'round'
2021
						});
2022
						elem.insertBefore(clone, firstChild);
2023
					});
2024
				}
2025
			});
2026
		}
2027
	},
2028
 
2029
	/**
2030
	 * Set or get a given attribute
2031
	 * @param {Object|String} hash
2032
	 * @param {Mixed|Undefined} val
2033
	 */
2034
	attr: function (hash, val, complete) {
2035
		var key,
2036
			value,
2037
			element = this.element,
2038
			hasSetSymbolSize,
2039
			ret = this,
2040
			skipAttr;
2041
 
2042
		// single key-value pair
2043
		if (typeof hash === 'string' && val !== UNDEFINED) {
2044
			key = hash;
2045
			hash = {};
2046
			hash[key] = val;
2047
		}
2048
 
2049
		// used as a getter: first argument is a string, second is undefined
2050
		if (typeof hash === 'string') {
2051
			ret = (this[hash + 'Getter'] || this._defaultGetter).call(this, hash, element);
2052
 
2053
		// setter
2054
		} else {
2055
 
2056
			for (key in hash) {
2057
				value = hash[key];
2058
				skipAttr = false;
2059
 
2060
 
2061
 
2062
				if (this.symbolName && /^(x|y|width|height|r|start|end|innerR|anchorX|anchorY)/.test(key)) {
2063
					if (!hasSetSymbolSize) {
2064
						this.symbolAttr(hash);
2065
						hasSetSymbolSize = true;
2066
					}
2067
					skipAttr = true;
2068
				}
2069
 
2070
				if (this.rotation && (key === 'x' || key === 'y')) {
2071
					this.doTransform = true;
2072
				}
2073
 
2074
				if (!skipAttr) {
2075
					(this[key + 'Setter'] || this._defaultSetter).call(this, value, key, element);
2076
				}
2077
 
2078
				// Let the shadow follow the main element
2079
				if (this.shadows && /^(width|height|visibility|x|y|d|transform|cx|cy|r)$/.test(key)) {
2080
					this.updateShadows(key, value);
2081
				}
2082
			}
2083
 
2084
			// Update transform. Do this outside the loop to prevent redundant updating for batch setting
2085
			// of attributes.
2086
			if (this.doTransform) {
2087
				this.updateTransform();
2088
				this.doTransform = false;
2089
			}
2090
 
2091
		}
2092
 
2093
		// In accordance with animate, run a complete callback
2094
		if (complete) {
2095
			complete();
2096
		}
2097
 
2098
		return ret;
2099
	},
2100
 
2101
	updateShadows: function (key, value) {
2102
		var shadows = this.shadows,
2103
			i = shadows.length;
2104
		while (i--) {
2105
			shadows[i].setAttribute(
2106
				key,
2107
				key === 'height' ?
2108
					mathMax(value - (shadows[i].cutHeight || 0), 0) :
2109
					key === 'd' ? this.d : value
2110
			);
2111
		}
2112
	},
2113
 
2114
	/**
2115
	 * Add a class name to an element
2116
	 */
2117
	addClass: function (className) {
2118
		var element = this.element,
2119
			currentClassName = attr(element, 'class') || '';
2120
 
2121
		if (currentClassName.indexOf(className) === -1) {
2122
			attr(element, 'class', currentClassName + ' ' + className);
2123
		}
2124
		return this;
2125
	},
2126
	/* hasClass and removeClass are not (yet) needed
2127
	hasClass: function (className) {
2128
		return attr(this.element, 'class').indexOf(className) !== -1;
2129
	},
2130
	removeClass: function (className) {
2131
		attr(this.element, 'class', attr(this.element, 'class').replace(className, ''));
2132
		return this;
2133
	},
2134
	*/
2135
 
2136
	/**
2137
	 * If one of the symbol size affecting parameters are changed,
2138
	 * check all the others only once for each call to an element's
2139
	 * .attr() method
2140
	 * @param {Object} hash
2141
	 */
2142
	symbolAttr: function (hash) {
2143
		var wrapper = this;
2144
 
2145
		each(['x', 'y', 'r', 'start', 'end', 'width', 'height', 'innerR', 'anchorX', 'anchorY'], function (key) {
2146
			wrapper[key] = pick(hash[key], wrapper[key]);
2147
		});
2148
 
2149
		wrapper.attr({
2150
			d: wrapper.renderer.symbols[wrapper.symbolName](
2151
				wrapper.x,
2152
				wrapper.y,
2153
				wrapper.width,
2154
				wrapper.height,
2155
				wrapper
2156
			)
2157
		});
2158
	},
2159
 
2160
	/**
2161
	 * Apply a clipping path to this object
2162
	 * @param {String} id
2163
	 */
2164
	clip: function (clipRect) {
2165
		return this.attr('clip-path', clipRect ? 'url(' + this.renderer.url + '#' + clipRect.id + ')' : NONE);
2166
	},
2167
 
2168
	/**
2169
	 * Calculate the coordinates needed for drawing a rectangle crisply and return the
2170
	 * calculated attributes
2171
	 * @param {Number} strokeWidth
2172
	 * @param {Number} x
2173
	 * @param {Number} y
2174
	 * @param {Number} width
2175
	 * @param {Number} height
2176
	 */
2177
	crisp: function (rect) {
2178
 
2179
		var wrapper = this,
2180
			key,
2181
			attribs = {},
2182
			normalizer,
2183
			strokeWidth = rect.strokeWidth || wrapper.strokeWidth || 0;
2184
 
2185
		normalizer = mathRound(strokeWidth) % 2 / 2; // mathRound because strokeWidth can sometimes have roundoff errors
2186
 
2187
		// normalize for crisp edges
2188
		rect.x = mathFloor(rect.x || wrapper.x || 0) + normalizer;
2189
		rect.y = mathFloor(rect.y || wrapper.y || 0) + normalizer;
2190
		rect.width = mathFloor((rect.width || wrapper.width || 0) - 2 * normalizer);
2191
		rect.height = mathFloor((rect.height || wrapper.height || 0) - 2 * normalizer);
2192
		rect.strokeWidth = strokeWidth;
2193
 
2194
		for (key in rect) {
2195
			if (wrapper[key] !== rect[key]) { // only set attribute if changed
2196
				wrapper[key] = attribs[key] = rect[key];
2197
			}
2198
		}
2199
 
2200
		return attribs;
2201
	},
2202
 
2203
	/**
2204
	 * Set styles for the element
2205
	 * @param {Object} styles
2206
	 */
2207
	css: function (styles) {
2208
		var elemWrapper = this,
2209
			oldStyles = elemWrapper.styles,
2210
			newStyles = {},
2211
			elem = elemWrapper.element,
2212
			textWidth,
2213
			n,
2214
			serializedCss = '',
2215
			hyphenate,
2216
			hasNew = !oldStyles;
2217
 
2218
		// convert legacy
2219
		if (styles && styles.color) {
2220
			styles.fill = styles.color;
2221
		}
2222
 
2223
		// Filter out existing styles to increase performance (#2640)
2224
		if (oldStyles) {
2225
			for (n in styles) {
2226
				if (styles[n] !== oldStyles[n]) {
2227
					newStyles[n] = styles[n];
2228
					hasNew = true;
2229
				}
2230
			}
2231
		}
2232
		if (hasNew) {
2233
			textWidth = elemWrapper.textWidth =
2234
				(styles && styles.width && elem.nodeName.toLowerCase() === 'text' && pInt(styles.width)) ||
2235
				elemWrapper.textWidth; // #3501
2236
 
2237
			// Merge the new styles with the old ones
2238
			if (oldStyles) {
2239
				styles = extend(
2240
					oldStyles,
2241
					newStyles
2242
				);
2243
			}
2244
 
2245
			// store object
2246
			elemWrapper.styles = styles;
2247
 
2248
			if (textWidth && (useCanVG || (!hasSVG && elemWrapper.renderer.forExport))) {
2249
				delete styles.width;
2250
			}
2251
 
2252
			// serialize and set style attribute
2253
			if (isIE && !hasSVG) {
2254
				css(elemWrapper.element, styles);
2255
			} else {
2256
				/*jslint unparam: true*/
2257
				hyphenate = function (a, b) { return '-' + b.toLowerCase(); };
2258
				/*jslint unparam: false*/
2259
				for (n in styles) {
2260
					serializedCss += n.replace(/([A-Z])/g, hyphenate) + ':' + styles[n] + ';';
2261
				}
2262
				attr(elem, 'style', serializedCss); // #1881
2263
			}
2264
 
2265
 
2266
			// re-build text
2267
			if (textWidth && elemWrapper.added) {
2268
				elemWrapper.renderer.buildText(elemWrapper);
2269
			}
2270
		}
2271
 
2272
		return elemWrapper;
2273
	},
2274
 
2275
	/**
2276
	 * Add an event listener
2277
	 * @param {String} eventType
2278
	 * @param {Function} handler
2279
	 */
2280
	on: function (eventType, handler) {
2281
		var svgElement = this,
2282
			element = svgElement.element;
2283
 
2284
		// touch
2285
		if (hasTouch && eventType === 'click') {
2286
			element.ontouchstart = function (e) {
2287
				svgElement.touchEventFired = Date.now();
2288
				e.preventDefault();
2289
				handler.call(element, e);
2290
			};
2291
			element.onclick = function (e) {
2292
				if (userAgent.indexOf('Android') === -1 || Date.now() - (svgElement.touchEventFired || 0) > 1100) { // #2269
2293
					handler.call(element, e);
2294
				}
2295
			};
2296
		} else {
2297
			// simplest possible event model for internal use
2298
			element['on' + eventType] = handler;
2299
		}
2300
		return this;
2301
	},
2302
 
2303
	/**
2304
	 * Set the coordinates needed to draw a consistent radial gradient across
2305
	 * pie slices regardless of positioning inside the chart. The format is
2306
	 * [centerX, centerY, diameter] in pixels.
2307
	 */
2308
	setRadialReference: function (coordinates) {
2309
		this.element.radialReference = coordinates;
2310
		return this;
2311
	},
2312
 
2313
	/**
2314
	 * Move an object and its children by x and y values
2315
	 * @param {Number} x
2316
	 * @param {Number} y
2317
	 */
2318
	translate: function (x, y) {
2319
		return this.attr({
2320
			translateX: x,
2321
			translateY: y
2322
		});
2323
	},
2324
 
2325
	/**
2326
	 * Invert a group, rotate and flip
2327
	 */
2328
	invert: function () {
2329
		var wrapper = this;
2330
		wrapper.inverted = true;
2331
		wrapper.updateTransform();
2332
		return wrapper;
2333
	},
2334
 
2335
	/**
2336
	 * Private method to update the transform attribute based on internal
2337
	 * properties
2338
	 */
2339
	updateTransform: function () {
2340
		var wrapper = this,
2341
			translateX = wrapper.translateX || 0,
2342
			translateY = wrapper.translateY || 0,
2343
			scaleX = wrapper.scaleX,
2344
			scaleY = wrapper.scaleY,
2345
			inverted = wrapper.inverted,
2346
			rotation = wrapper.rotation,
2347
			element = wrapper.element,
2348
			transform;
2349
 
2350
		// flipping affects translate as adjustment for flipping around the group's axis
2351
		if (inverted) {
2352
			translateX += wrapper.attr('width');
2353
			translateY += wrapper.attr('height');
2354
		}
2355
 
2356
		// Apply translate. Nearly all transformed elements have translation, so instead
2357
		// of checking for translate = 0, do it always (#1767, #1846).
2358
		transform = ['translate(' + translateX + ',' + translateY + ')'];
2359
 
2360
		// apply rotation
2361
		if (inverted) {
2362
			transform.push('rotate(90) scale(-1,1)');
2363
		} else if (rotation) { // text rotation
2364
			transform.push('rotate(' + rotation + ' ' + (element.getAttribute('x') || 0) + ' ' + (element.getAttribute('y') || 0) + ')');
2365
 
2366
			// Delete bBox memo when the rotation changes
2367
			//delete wrapper.bBox;
2368
		}
2369
 
2370
		// apply scale
2371
		if (defined(scaleX) || defined(scaleY)) {
2372
			transform.push('scale(' + pick(scaleX, 1) + ' ' + pick(scaleY, 1) + ')');
2373
		}
2374
 
2375
		if (transform.length) {
2376
			element.setAttribute('transform', transform.join(' '));
2377
		}
2378
	},
2379
	/**
2380
	 * Bring the element to the front
2381
	 */
2382
	toFront: function () {
2383
		var element = this.element;
2384
		element.parentNode.appendChild(element);
2385
		return this;
2386
	},
2387
 
2388
 
2389
	/**
2390
	 * Break down alignment options like align, verticalAlign, x and y
2391
	 * to x and y relative to the chart.
2392
	 *
2393
	 * @param {Object} alignOptions
2394
	 * @param {Boolean} alignByTranslate
2395
	 * @param {String[Object} box The box to align to, needs a width and height. When the
2396
	 *		box is a string, it refers to an object in the Renderer. For example, when
2397
	 *		box is 'spacingBox', it refers to Renderer.spacingBox which holds width, height
2398
	 *		x and y properties.
2399
	 *
2400
	 */
2401
	align: function (alignOptions, alignByTranslate, box) {
2402
		var align,
2403
			vAlign,
2404
			x,
2405
			y,
2406
			attribs = {},
2407
			alignTo,
2408
			renderer = this.renderer,
2409
			alignedObjects = renderer.alignedObjects;
2410
 
2411
		// First call on instanciate
2412
		if (alignOptions) {
2413
			this.alignOptions = alignOptions;
2414
			this.alignByTranslate = alignByTranslate;
2415
			if (!box || isString(box)) { // boxes other than renderer handle this internally
2416
				this.alignTo = alignTo = box || 'renderer';
2417
				erase(alignedObjects, this); // prevent duplicates, like legendGroup after resize
2418
				alignedObjects.push(this);
2419
				box = null; // reassign it below
2420
			}
2421
 
2422
		// When called on resize, no arguments are supplied
2423
		} else {
2424
			alignOptions = this.alignOptions;
2425
			alignByTranslate = this.alignByTranslate;
2426
			alignTo = this.alignTo;
2427
		}
2428
 
2429
		box = pick(box, renderer[alignTo], renderer);
2430
 
2431
		// Assign variables
2432
		align = alignOptions.align;
2433
		vAlign = alignOptions.verticalAlign;
2434
		x = (box.x || 0) + (alignOptions.x || 0); // default: left align
2435
		y = (box.y || 0) + (alignOptions.y || 0); // default: top align
2436
 
2437
		// Align
2438
		if (align === 'right' || align === 'center') {
2439
			x += (box.width - (alignOptions.width || 0)) /
2440
					{ right: 1, center: 2 }[align];
2441
		}
2442
		attribs[alignByTranslate ? 'translateX' : 'x'] = mathRound(x);
2443
 
2444
 
2445
		// Vertical align
2446
		if (vAlign === 'bottom' || vAlign === 'middle') {
2447
			y += (box.height - (alignOptions.height || 0)) /
2448
					({ bottom: 1, middle: 2 }[vAlign] || 1);
2449
 
2450
		}
2451
		attribs[alignByTranslate ? 'translateY' : 'y'] = mathRound(y);
2452
 
2453
		// Animate only if already placed
2454
		this[this.placed ? 'animate' : 'attr'](attribs);
2455
		this.placed = true;
2456
		this.alignAttr = attribs;
2457
 
2458
		return this;
2459
	},
2460
 
2461
	/**
2462
	 * Get the bounding box (width, height, x and y) for the element
2463
	 */
2464
	getBBox: function (reload) {
2465
		var wrapper = this,
2466
			bBox,// = wrapper.bBox,
2467
			renderer = wrapper.renderer,
2468
			width,
2469
			height,
2470
			rotation = wrapper.rotation,
2471
			element = wrapper.element,
2472
			styles = wrapper.styles,
2473
			rad = rotation * deg2rad,
2474
			textStr = wrapper.textStr,
2475
			textShadow,
2476
			elemStyle = element.style,
2477
			toggleTextShadowShim,
2478
			cacheKey;
2479
 
2480
		if (textStr !== UNDEFINED) {
2481
 
2482
			// Properties that affect bounding box
2483
			cacheKey = ['', rotation || 0, styles && styles.fontSize, element.style.width].join(',');
2484
 
2485
			// Since numbers are monospaced, and numerical labels appear a lot in a chart,
2486
			// we assume that a label of n characters has the same bounding box as others
2487
			// of the same length.
2488
			if (textStr === '' || numRegex.test(textStr)) {
2489
				cacheKey = 'num:' + textStr.toString().length + cacheKey;
2490
 
2491
			// Caching all strings reduces rendering time by 4-5%.
2492
			} else {
2493
				cacheKey = textStr + cacheKey;
2494
			}
2495
		}
2496
 
2497
		if (cacheKey && !reload) {
2498
			bBox = renderer.cache[cacheKey];
2499
		}
2500
 
2501
		// No cache found
2502
		if (!bBox) {
2503
 
2504
			// SVG elements
2505
			if (element.namespaceURI === SVG_NS || renderer.forExport) {
2506
				try { // Fails in Firefox if the container has display: none.
2507
 
2508
					// When the text shadow shim is used, we need to hide the fake shadows
2509
					// to get the correct bounding box (#3872)
2510
					toggleTextShadowShim = this.fakeTS && function (display) {
2511
						each(element.querySelectorAll('.' + PREFIX + 'text-shadow'), function (tspan) {
2512
							tspan.style.display = display;
2513
						});
2514
					};
2515
 
2516
					// Workaround for #3842, Firefox reporting wrong bounding box for shadows
2517
					if (isFirefox && elemStyle.textShadow) {
2518
						textShadow = elemStyle.textShadow;
2519
						elemStyle.textShadow = '';
2520
					} else if (toggleTextShadowShim) {
2521
						toggleTextShadowShim(NONE);
2522
					}
2523
 
2524
					bBox = element.getBBox ?
2525
						// SVG: use extend because IE9 is not allowed to change width and height in case
2526
						// of rotation (below)
2527
						extend({}, element.getBBox()) :
2528
						// Canvas renderer and legacy IE in export mode
2529
						{
2530
							width: element.offsetWidth,
2531
							height: element.offsetHeight
2532
						};
2533
 
2534
					// #3842
2535
					if (textShadow) {
2536
						elemStyle.textShadow = textShadow;
2537
					} else if (toggleTextShadowShim) {
2538
						toggleTextShadowShim('');
2539
					}
2540
				} catch (e) {}
2541
 
2542
				// If the bBox is not set, the try-catch block above failed. The other condition
2543
				// is for Opera that returns a width of -Infinity on hidden elements.
2544
				if (!bBox || bBox.width < 0) {
2545
					bBox = { width: 0, height: 0 };
2546
				}
2547
 
2548
 
2549
			// VML Renderer or useHTML within SVG
2550
			} else {
2551
 
2552
				bBox = wrapper.htmlGetBBox();
2553
 
2554
			}
2555
 
2556
			// True SVG elements as well as HTML elements in modern browsers using the .useHTML option
2557
			// need to compensated for rotation
2558
			if (renderer.isSVG) {
2559
				width = bBox.width;
2560
				height = bBox.height;
2561
 
2562
				// Workaround for wrong bounding box in IE9 and IE10 (#1101, #1505, #1669, #2568)
2563
				if (isIE && styles && styles.fontSize === '11px' && height.toPrecision(3) === '16.9') {
2564
					bBox.height = height = 14;
2565
				}
2566
 
2567
				// Adjust for rotated text
2568
				if (rotation) {
2569
					bBox.width = mathAbs(height * mathSin(rad)) + mathAbs(width * mathCos(rad));
2570
					bBox.height = mathAbs(height * mathCos(rad)) + mathAbs(width * mathSin(rad));
2571
				}
2572
			}
2573
 
2574
			// Cache it
2575
			if (cacheKey) {
2576
				renderer.cache[cacheKey] = bBox;
2577
			}
2578
		}
2579
		return bBox;
2580
	},
2581
 
2582
	/**
2583
	 * Show the element
2584
	 */
2585
	show: function (inherit) {
2586
		return this.attr({ visibility: inherit ? 'inherit' : VISIBLE });
2587
	},
2588
 
2589
	/**
2590
	 * Hide the element
2591
	 */
2592
	hide: function () {
2593
		return this.attr({ visibility: HIDDEN });
2594
	},
2595
 
2596
	fadeOut: function (duration) {
2597
		var elemWrapper = this;
2598
		elemWrapper.animate({
2599
			opacity: 0
2600
		}, {
2601
			duration: duration || 150,
2602
			complete: function () {
2603
				elemWrapper.attr({ y: -9999 }); // #3088, assuming we're only using this for tooltips
2604
			}
2605
		});
2606
	},
2607
 
2608
	/**
2609
	 * Add the element
2610
	 * @param {Object|Undefined} parent Can be an element, an element wrapper or undefined
2611
	 *	to append the element to the renderer.box.
2612
	 */
2613
	add: function (parent) {
2614
 
2615
		var renderer = this.renderer,
2616
			element = this.element,
2617
			inserted;
2618
 
2619
		if (parent) {
2620
			this.parentGroup = parent;
2621
		}
2622
 
2623
		// mark as inverted
2624
		this.parentInverted = parent && parent.inverted;
2625
 
2626
		// build formatted text
2627
		if (this.textStr !== undefined) {
2628
			renderer.buildText(this);
2629
		}
2630
 
2631
		// Mark as added
2632
		this.added = true;
2633
 
2634
		// If we're adding to renderer root, or other elements in the group
2635
		// have a z index, we need to handle it
2636
		if (!parent || parent.handleZ || this.zIndex) {
2637
			inserted = this.zIndexSetter();
2638
		}
2639
 
2640
		// If zIndex is not handled, append at the end
2641
		if (!inserted) {
2642
			(parent ? parent.element : renderer.box).appendChild(element);
2643
		}
2644
 
2645
		// fire an event for internal hooks
2646
		if (this.onAdd) {
2647
			this.onAdd();
2648
		}
2649
 
2650
		return this;
2651
	},
2652
 
2653
	/**
2654
	 * Removes a child either by removeChild or move to garbageBin.
2655
	 * Issue 490; in VML removeChild results in Orphaned nodes according to sIEve, discardElement does not.
2656
	 */
2657
	safeRemoveChild: function (element) {
2658
		var parentNode = element.parentNode;
2659
		if (parentNode) {
2660
			parentNode.removeChild(element);
2661
		}
2662
	},
2663
 
2664
	/**
2665
	 * Destroy the element and element wrapper
2666
	 */
2667
	destroy: function () {
2668
		var wrapper = this,
2669
			element = wrapper.element || {},
2670
			shadows = wrapper.shadows,
2671
			parentToClean = wrapper.renderer.isSVG && element.nodeName === 'SPAN' && wrapper.parentGroup,
2672
			grandParent,
2673
			key,
2674
			i;
2675
 
2676
		// remove events
2677
		element.onclick = element.onmouseout = element.onmouseover = element.onmousemove = element.point = null;
2678
		stop(wrapper); // stop running animations
2679
 
2680
		if (wrapper.clipPath) {
2681
			wrapper.clipPath = wrapper.clipPath.destroy();
2682
		}
2683
 
2684
		// Destroy stops in case this is a gradient object
2685
		if (wrapper.stops) {
2686
			for (i = 0; i < wrapper.stops.length; i++) {
2687
				wrapper.stops[i] = wrapper.stops[i].destroy();
2688
			}
2689
			wrapper.stops = null;
2690
		}
2691
 
2692
		// remove element
2693
		wrapper.safeRemoveChild(element);
2694
 
2695
		// destroy shadows
2696
		if (shadows) {
2697
			each(shadows, function (shadow) {
2698
				wrapper.safeRemoveChild(shadow);
2699
			});
2700
		}
2701
 
2702
		// In case of useHTML, clean up empty containers emulating SVG groups (#1960, #2393, #2697).
2703
		while (parentToClean && parentToClean.div && parentToClean.div.childNodes.length === 0) {
2704
			grandParent = parentToClean.parentGroup;
2705
			wrapper.safeRemoveChild(parentToClean.div);
2706
			delete parentToClean.div;
2707
			parentToClean = grandParent;
2708
		}
2709
 
2710
		// remove from alignObjects
2711
		if (wrapper.alignTo) {
2712
			erase(wrapper.renderer.alignedObjects, wrapper);
2713
		}
2714
 
2715
		for (key in wrapper) {
2716
			delete wrapper[key];
2717
		}
2718
 
2719
		return null;
2720
	},
2721
 
2722
	/**
2723
	 * Add a shadow to the element. Must be done after the element is added to the DOM
2724
	 * @param {Boolean|Object} shadowOptions
2725
	 */
2726
	shadow: function (shadowOptions, group, cutOff) {
2727
		var shadows = [],
2728
			i,
2729
			shadow,
2730
			element = this.element,
2731
			strokeWidth,
2732
			shadowWidth,
2733
			shadowElementOpacity,
2734
 
2735
			// compensate for inverted plot area
2736
			transform;
2737
 
2738
 
2739
		if (shadowOptions) {
2740
			shadowWidth = pick(shadowOptions.width, 3);
2741
			shadowElementOpacity = (shadowOptions.opacity || 0.15) / shadowWidth;
2742
			transform = this.parentInverted ?
2743
				'(-1,-1)' :
2744
				'(' + pick(shadowOptions.offsetX, 1) + ', ' + pick(shadowOptions.offsetY, 1) + ')';
2745
			for (i = 1; i <= shadowWidth; i++) {
2746
				shadow = element.cloneNode(0);
2747
				strokeWidth = (shadowWidth * 2) + 1 - (2 * i);
2748
				attr(shadow, {
2749
					'isShadow': 'true',
2750
					'stroke': shadowOptions.color || 'black',
2751
					'stroke-opacity': shadowElementOpacity * i,
2752
					'stroke-width': strokeWidth,
2753
					'transform': 'translate' + transform,
2754
					'fill': NONE
2755
				});
2756
				if (cutOff) {
2757
					attr(shadow, 'height', mathMax(attr(shadow, 'height') - strokeWidth, 0));
2758
					shadow.cutHeight = strokeWidth;
2759
				}
2760
 
2761
				if (group) {
2762
					group.element.appendChild(shadow);
2763
				} else {
2764
					element.parentNode.insertBefore(shadow, element);
2765
				}
2766
 
2767
				shadows.push(shadow);
2768
			}
2769
 
2770
			this.shadows = shadows;
2771
		}
2772
		return this;
2773
 
2774
	},
2775
 
2776
	xGetter: function (key) {
2777
		if (this.element.nodeName === 'circle') {
2778
			key = { x: 'cx', y: 'cy' }[key] || key;
2779
		}
2780
		return this._defaultGetter(key);
2781
	},
2782
 
2783
	/**
2784
	 * Get the current value of an attribute or pseudo attribute, used mainly
2785
	 * for animation.
2786
	 */
2787
	_defaultGetter: function (key) {
2788
		var ret = pick(this[key], this.element ? this.element.getAttribute(key) : null, 0);
2789
 
2790
		if (/^[\-0-9\.]+$/.test(ret)) { // is numerical
2791
			ret = parseFloat(ret);
2792
		}
2793
		return ret;
2794
	},
2795
 
2796
 
2797
	dSetter: function (value, key, element) {
2798
		if (value && value.join) { // join path
2799
			value = value.join(' ');
2800
		}
2801
		if (/(NaN| {2}|^$)/.test(value)) {
2802
			value = 'M 0 0';
2803
		}
2804
		element.setAttribute(key, value);
2805
 
2806
		this[key] = value;
2807
	},
2808
	dashstyleSetter: function (value) {
2809
		var i;
2810
		value = value && value.toLowerCase();
2811
		if (value) {
2812
			value = value
2813
				.replace('shortdashdotdot', '3,1,1,1,1,1,')
2814
				.replace('shortdashdot', '3,1,1,1')
2815
				.replace('shortdot', '1,1,')
2816
				.replace('shortdash', '3,1,')
2817
				.replace('longdash', '8,3,')
2818
				.replace(/dot/g, '1,3,')
2819
				.replace('dash', '4,3,')
2820
				.replace(/,$/, '')
2821
				.split(','); // ending comma
2822
 
2823
			i = value.length;
2824
			while (i--) {
2825
				value[i] = pInt(value[i]) * this['stroke-width'];
2826
			}
2827
			value = value.join(',')
2828
				.replace('NaN', 'none'); // #3226
2829
			this.element.setAttribute('stroke-dasharray', value);
2830
		}
2831
	},
2832
	alignSetter: function (value) {
2833
		this.element.setAttribute('text-anchor', { left: 'start', center: 'middle', right: 'end' }[value]);
2834
	},
2835
	opacitySetter: function (value, key, element) {
2836
		this[key] = value;
2837
		element.setAttribute(key, value);
2838
	},
2839
	titleSetter: function (value) {
2840
		var titleNode = this.element.getElementsByTagName('title')[0];
2841
		if (!titleNode) {
2842
			titleNode = doc.createElementNS(SVG_NS, 'title');
2843
			this.element.appendChild(titleNode);
2844
		}
2845
		titleNode.appendChild(
2846
			doc.createTextNode(
2847
				(String(pick(value), '')).replace(/<[^>]*>/g, '') // #3276, #3895
2848
			)
2849
		);
2850
	},
2851
	textSetter: function (value) {
2852
		if (value !== this.textStr) {
2853
			// Delete bBox memo when the text changes
2854
			delete this.bBox;
2855
 
2856
			this.textStr = value;
2857
			if (this.added) {
2858
				this.renderer.buildText(this);
2859
			}
2860
		}
2861
	},
2862
	fillSetter: function (value, key, element) {
2863
		if (typeof value === 'string') {
2864
			element.setAttribute(key, value);
2865
		} else if (value) {
2866
			this.colorGradient(value, key, element);
2867
		}
2868
	},
2869
	visibilitySetter: function (value, key, element) {
2870
		// IE9-11 doesn't handle visibilty:inherit well, so we remove the attribute instead (#2881, #3909)
2871
		if (value === 'inherit') {
2872
			element.removeAttribute(key);
2873
		} else {
2874
			element.setAttribute(key, value);
2875
		}
2876
	},
2877
	zIndexSetter: function (value, key) {
2878
		var renderer = this.renderer,
2879
			parentGroup = this.parentGroup,
2880
			parentWrapper = parentGroup || renderer,
2881
			parentNode = parentWrapper.element || renderer.box,
2882
			childNodes,
2883
			otherElement,
2884
			otherZIndex,
2885
			element = this.element,
2886
			inserted,
2887
			run = this.added,
2888
			i;
2889
 
2890
		if (defined(value)) {
2891
			element.setAttribute(key, value); // So we can read it for other elements in the group
2892
			value = +value;
2893
			if (this[key] === value) { // Only update when needed (#3865)
2894
				run = false;
2895
			}
2896
			this[key] = value;
2897
		}
2898
 
2899
		// Insert according to this and other elements' zIndex. Before .add() is called,
2900
		// nothing is done. Then on add, or by later calls to zIndexSetter, the node
2901
		// is placed on the right place in the DOM.
2902
		if (run) {
2903
			value = this.zIndex;
2904
 
2905
			if (value && parentGroup) {
2906
				parentGroup.handleZ = true;
2907
			}
2908
 
2909
			childNodes = parentNode.childNodes;
2910
			for (i = 0; i < childNodes.length && !inserted; i++) {
2911
				otherElement = childNodes[i];
2912
				otherZIndex = attr(otherElement, 'zIndex');
2913
				if (otherElement !== element && (
2914
						// Insert before the first element with a higher zIndex
2915
						pInt(otherZIndex) > value ||
2916
						// If no zIndex given, insert before the first element with a zIndex
2917
						(!defined(value) && defined(otherZIndex))
2918
 
2919
						)) {
2920
					parentNode.insertBefore(element, otherElement);
2921
					inserted = true;
2922
				}
2923
			}
2924
			if (!inserted) {
2925
				parentNode.appendChild(element);
2926
			}
2927
		}
2928
		return inserted;
2929
	},
2930
	_defaultSetter: function (value, key, element) {
2931
		element.setAttribute(key, value);
2932
	}
2933
};
2934
 
2935
// Some shared setters and getters
2936
SVGElement.prototype.yGetter = SVGElement.prototype.xGetter;
2937
SVGElement.prototype.translateXSetter = SVGElement.prototype.translateYSetter =
2938
		SVGElement.prototype.rotationSetter = SVGElement.prototype.verticalAlignSetter =
2939
		SVGElement.prototype.scaleXSetter = SVGElement.prototype.scaleYSetter = function (value, key) {
2940
	this[key] = value;
2941
	this.doTransform = true;
2942
};
2943
 
2944
// WebKit and Batik have problems with a stroke-width of zero, so in this case we remove the
2945
// stroke attribute altogether. #1270, #1369, #3065, #3072.
2946
SVGElement.prototype['stroke-widthSetter'] = SVGElement.prototype.strokeSetter = function (value, key, element) {
2947
	this[key] = value;
2948
	// Only apply the stroke attribute if the stroke width is defined and larger than 0
2949
	if (this.stroke && this['stroke-width']) {
2950
		this.strokeWidth = this['stroke-width'];
2951
		SVGElement.prototype.fillSetter.call(this, this.stroke, 'stroke', element); // use prototype as instance may be overridden
2952
		element.setAttribute('stroke-width', this['stroke-width']);
2953
		this.hasStroke = true;
2954
	} else if (key === 'stroke-width' && value === 0 && this.hasStroke) {
2955
		element.removeAttribute('stroke');
2956
		this.hasStroke = false;
2957
	}
2958
};
2959
 
2960
 
2961
/**
2962
 * The default SVG renderer
2963
 */
2964
var SVGRenderer = function () {
2965
	this.init.apply(this, arguments);
2966
};
2967
SVGRenderer.prototype = {
2968
	Element: SVGElement,
2969
 
2970
	/**
2971
	 * Initialize the SVGRenderer
2972
	 * @param {Object} container
2973
	 * @param {Number} width
2974
	 * @param {Number} height
2975
	 * @param {Boolean} forExport
2976
	 */
2977
	init: function (container, width, height, style, forExport) {
2978
		var renderer = this,
2979
			loc = location,
2980
			boxWrapper,
2981
			element,
2982
			desc;
2983
 
2984
		boxWrapper = renderer.createElement('svg')
2985
			.attr({
2986
				version: '1.1'
2987
			})
2988
			.css(this.getStyle(style));
2989
		element = boxWrapper.element;
2990
		container.appendChild(element);
2991
 
2992
		// For browsers other than IE, add the namespace attribute (#1978)
2993
		if (container.innerHTML.indexOf('xmlns') === -1) {
2994
			attr(element, 'xmlns', SVG_NS);
2995
		}
2996
 
2997
		// object properties
2998
		renderer.isSVG = true;
2999
		renderer.box = element;
3000
		renderer.boxWrapper = boxWrapper;
3001
		renderer.alignedObjects = [];
3002
 
3003
		// Page url used for internal references. #24, #672, #1070
3004
		renderer.url = (isFirefox || isWebKit) && doc.getElementsByTagName('base').length ?
3005
			loc.href
3006
				.replace(/#.*?$/, '') // remove the hash
3007
				.replace(/([\('\)])/g, '\\$1') // escape parantheses and quotes
3008
				.replace(/ /g, '%20') : // replace spaces (needed for Safari only)
3009
			'';
3010
 
3011
		// Add description
3012
		desc = this.createElement('desc').add();
3013
		desc.element.appendChild(doc.createTextNode('Created with ' + PRODUCT + ' ' + VERSION));
3014
 
3015
 
3016
		renderer.defs = this.createElement('defs').add();
3017
		renderer.forExport = forExport;
3018
		renderer.gradients = {}; // Object where gradient SvgElements are stored
3019
		renderer.cache = {}; // Cache for numerical bounding boxes
3020
 
3021
		renderer.setSize(width, height, false);
3022
 
3023
 
3024
 
3025
		// Issue 110 workaround:
3026
		// In Firefox, if a div is positioned by percentage, its pixel position may land
3027
		// between pixels. The container itself doesn't display this, but an SVG element
3028
		// inside this container will be drawn at subpixel precision. In order to draw
3029
		// sharp lines, this must be compensated for. This doesn't seem to work inside
3030
		// iframes though (like in jsFiddle).
3031
		var subPixelFix, rect;
3032
		if (isFirefox && container.getBoundingClientRect) {
3033
			renderer.subPixelFix = subPixelFix = function () {
3034
				css(container, { left: 0, top: 0 });
3035
				rect = container.getBoundingClientRect();
3036
				css(container, {
3037
					left: (mathCeil(rect.left) - rect.left) + PX,
3038
					top: (mathCeil(rect.top) - rect.top) + PX
3039
				});
3040
			};
3041
 
3042
			// run the fix now
3043
			subPixelFix();
3044
 
3045
			// run it on resize
3046
			addEvent(win, 'resize', subPixelFix);
3047
		}
3048
	},
3049
 
3050
	getStyle: function (style) {
3051
		return (this.style = extend({
3052
			fontFamily: '"Lucida Grande", "Lucida Sans Unicode", Arial, Helvetica, sans-serif', // default font
3053
			fontSize: '12px'
3054
		}, style));
3055
	},
3056
 
3057
	/**
3058
	 * Detect whether the renderer is hidden. This happens when one of the parent elements
3059
	 * has display: none. #608.
3060
	 */
3061
	isHidden: function () {
3062
		return !this.boxWrapper.getBBox().width;
3063
	},
3064
 
3065
	/**
3066
	 * Destroys the renderer and its allocated members.
3067
	 */
3068
	destroy: function () {
3069
		var renderer = this,
3070
			rendererDefs = renderer.defs;
3071
		renderer.box = null;
3072
		renderer.boxWrapper = renderer.boxWrapper.destroy();
3073
 
3074
		// Call destroy on all gradient elements
3075
		destroyObjectProperties(renderer.gradients || {});
3076
		renderer.gradients = null;
3077
 
3078
		// Defs are null in VMLRenderer
3079
		// Otherwise, destroy them here.
3080
		if (rendererDefs) {
3081
			renderer.defs = rendererDefs.destroy();
3082
		}
3083
 
3084
		// Remove sub pixel fix handler
3085
		// We need to check that there is a handler, otherwise all functions that are registered for event 'resize' are removed
3086
		// See issue #982
3087
		if (renderer.subPixelFix) {
3088
			removeEvent(win, 'resize', renderer.subPixelFix);
3089
		}
3090
 
3091
		renderer.alignedObjects = null;
3092
 
3093
		return null;
3094
	},
3095
 
3096
	/**
3097
	 * Create a wrapper for an SVG element
3098
	 * @param {Object} nodeName
3099
	 */
3100
	createElement: function (nodeName) {
3101
		var wrapper = new this.Element();
3102
		wrapper.init(this, nodeName);
3103
		return wrapper;
3104
	},
3105
 
3106
	/**
3107
	 * Dummy function for use in canvas renderer
3108
	 */
3109
	draw: function () {},
3110
 
3111
	/**
3112
	 * Parse a simple HTML string into SVG tspans
3113
	 *
3114
	 * @param {Object} textNode The parent text SVG node
3115
	 */
3116
	buildText: function (wrapper) {
3117
		var textNode = wrapper.element,
3118
			renderer = this,
3119
			forExport = renderer.forExport,
3120
			textStr = pick(wrapper.textStr, '').toString(),
3121
			hasMarkup = textStr.indexOf('<') !== -1,
3122
			lines,
3123
			childNodes = textNode.childNodes,
3124
			styleRegex,
3125
			hrefRegex,
3126
			parentX = attr(textNode, 'x'),
3127
			textStyles = wrapper.styles,
3128
			width = wrapper.textWidth,
3129
			textLineHeight = textStyles && textStyles.lineHeight,
3130
			textShadow = textStyles && textStyles.textShadow,
3131
			ellipsis = textStyles && textStyles.textOverflow === 'ellipsis',
3132
			i = childNodes.length,
3133
			tempParent = width && !wrapper.added && this.box,
3134
			getLineHeight = function (tspan) {
3135
				return textLineHeight ?
3136
					pInt(textLineHeight) :
3137
					renderer.fontMetrics(
3138
						/(px|em)$/.test(tspan && tspan.style.fontSize) ?
3139
							tspan.style.fontSize :
3140
							((textStyles && textStyles.fontSize) || renderer.style.fontSize || 12),
3141
						tspan
3142
					).h;
3143
			},
3144
			unescapeAngleBrackets = function (inputStr) {
3145
				return inputStr.replace(/&lt;/g, '<').replace(/&gt;/g, '>');
3146
			};
3147
 
3148
		/// remove old text
3149
		while (i--) {
3150
			textNode.removeChild(childNodes[i]);
3151
		}
3152
 
3153
		// Skip tspans, add text directly to text node. The forceTSpan is a hook
3154
		// used in text outline hack.
3155
		if (!hasMarkup && !textShadow && !ellipsis && textStr.indexOf(' ') === -1) {
3156
			textNode.appendChild(doc.createTextNode(unescapeAngleBrackets(textStr)));
3157
			return;
3158
 
3159
		// Complex strings, add more logic
3160
		} else {
3161
 
3162
			styleRegex = /<.*style="([^"]+)".*>/;
3163
			hrefRegex = /<.*href="(http[^"]+)".*>/;
3164
 
3165
			if (tempParent) {
3166
				tempParent.appendChild(textNode); // attach it to the DOM to read offset width
3167
			}
3168
 
3169
			if (hasMarkup) {
3170
				lines = textStr
3171
					.replace(/<(b|strong)>/g, '<span style="font-weight:bold">')
3172
					.replace(/<(i|em)>/g, '<span style="font-style:italic">')
3173
					.replace(/<a/g, '<span')
3174
					.replace(/<\/(b|strong|i|em|a)>/g, '</span>')
3175
					.split(/<br.*?>/g);
3176
 
3177
			} else {
3178
				lines = [textStr];
3179
			}
3180
 
3181
 
3182
			// remove empty line at end
3183
			if (lines[lines.length - 1] === '') {
3184
				lines.pop();
3185
			}
3186
 
3187
 
3188
			// build the lines
3189
			each(lines, function (line, lineNo) {
3190
				var spans, spanNo = 0;
3191
 
3192
				line = line.replace(/<span/g, '|||<span').replace(/<\/span>/g, '</span>|||');
3193
				spans = line.split('|||');
3194
 
3195
				each(spans, function (span) {
3196
					if (span !== '' || spans.length === 1) {
3197
						var attributes = {},
3198
							tspan = doc.createElementNS(SVG_NS, 'tspan'),
3199
							spanStyle; // #390
3200
						if (styleRegex.test(span)) {
3201
							spanStyle = span.match(styleRegex)[1].replace(/(;| |^)color([ :])/, '$1fill$2');
3202
							attr(tspan, 'style', spanStyle);
3203
						}
3204
						if (hrefRegex.test(span) && !forExport) { // Not for export - #1529
3205
							attr(tspan, 'onclick', 'location.href=\"' + span.match(hrefRegex)[1] + '\"');
3206
							css(tspan, { cursor: 'pointer' });
3207
						}
3208
 
3209
						span = unescapeAngleBrackets(span.replace(/<(.|\n)*?>/g, '') || ' ');
3210
 
3211
						// Nested tags aren't supported, and cause crash in Safari (#1596)
3212
						if (span !== ' ') {
3213
 
3214
							// add the text node
3215
							tspan.appendChild(doc.createTextNode(span));
3216
 
3217
							if (!spanNo) { // first span in a line, align it to the left
3218
								if (lineNo && parentX !== null) {
3219
									attributes.x = parentX;
3220
								}
3221
							} else {
3222
								attributes.dx = 0; // #16
3223
							}
3224
 
3225
							// add attributes
3226
							attr(tspan, attributes);
3227
 
3228
							// Append it
3229
							textNode.appendChild(tspan);
3230
 
3231
							// first span on subsequent line, add the line height
3232
							if (!spanNo && lineNo) {
3233
 
3234
								// allow getting the right offset height in exporting in IE
3235
								if (!hasSVG && forExport) {
3236
									css(tspan, { display: 'block' });
3237
								}
3238
 
3239
								// Set the line height based on the font size of either
3240
								// the text element or the tspan element
3241
								attr(
3242
									tspan,
3243
									'dy',
3244
									getLineHeight(tspan)
3245
								);
3246
							}
3247
 
3248
							/*if (width) {
3249
								renderer.breakText(wrapper, width);
3250
							}*/
3251
 
3252
							// Check width and apply soft breaks or ellipsis
3253
							if (width) {
3254
								var words = span.replace(/([^\^])-/g, '$1- ').split(' '), // #1273
3255
									hasWhiteSpace = spans.length > 1 || lineNo || (words.length > 1 && textStyles.whiteSpace !== 'nowrap'),
3256
									tooLong,
3257
									wasTooLong,
3258
									actualWidth,
3259
									rest = [],
3260
									dy = getLineHeight(tspan),
3261
									softLineNo = 1,
3262
									rotation = wrapper.rotation,
3263
									wordStr = span, // for ellipsis
3264
									cursor = wordStr.length, // binary search cursor
3265
									bBox;
3266
 
3267
								while ((hasWhiteSpace || ellipsis) && (words.length || rest.length)) {
3268
									wrapper.rotation = 0; // discard rotation when computing box
3269
									bBox = wrapper.getBBox(true);
3270
									actualWidth = bBox.width;
3271
 
3272
									// Old IE cannot measure the actualWidth for SVG elements (#2314)
3273
									if (!hasSVG && renderer.forExport) {
3274
										actualWidth = renderer.measureSpanWidth(tspan.firstChild.data, wrapper.styles);
3275
									}
3276
 
3277
									tooLong = actualWidth > width;
3278
 
3279
									// For ellipsis, do a binary search for the correct string length
3280
									if (wasTooLong === undefined) {
3281
										wasTooLong = tooLong; // First time
3282
									}
3283
									if (ellipsis && wasTooLong) {
3284
										cursor /= 2;
3285
 
3286
										if (wordStr === '' || (!tooLong && cursor < 0.5)) {
3287
											words = []; // All ok, break out
3288
										} else {
3289
											if (tooLong) {
3290
												wasTooLong = true;
3291
											}
3292
											wordStr = span.substring(0, wordStr.length + (tooLong ? -1 : 1) * mathCeil(cursor));
3293
											words = [wordStr + (width > 3 ? '\u2026' : '')];
3294
											tspan.removeChild(tspan.firstChild);
3295
										}
3296
 
3297
									// Looping down, this is the first word sequence that is not too long,
3298
									// so we can move on to build the next line.
3299
									} else if (!tooLong || words.length === 1) {
3300
										words = rest;
3301
										rest = [];
3302
 
3303
										if (words.length) {
3304
											softLineNo++;
3305
 
3306
											tspan = doc.createElementNS(SVG_NS, 'tspan');
3307
											attr(tspan, {
3308
												dy: dy,
3309
												x: parentX
3310
											});
3311
											if (spanStyle) { // #390
3312
												attr(tspan, 'style', spanStyle);
3313
											}
3314
											textNode.appendChild(tspan);
3315
										}
3316
										if (actualWidth > width) { // a single word is pressing it out
3317
											width = actualWidth;
3318
										}
3319
									} else { // append to existing line tspan
3320
										tspan.removeChild(tspan.firstChild);
3321
										rest.unshift(words.pop());
3322
									}
3323
									if (words.length) {
3324
										tspan.appendChild(doc.createTextNode(words.join(' ').replace(/- /g, '-')));
3325
									}
3326
								}
3327
								if (wasTooLong) {
3328
									wrapper.attr('title', wrapper.textStr);
3329
								}
3330
								wrapper.rotation = rotation;
3331
							}
3332
 
3333
							spanNo++;
3334
						}
3335
					}
3336
				});
3337
			});
3338
			if (tempParent) {
3339
				tempParent.removeChild(textNode); // attach it to the DOM to read offset width
3340
			}
3341
 
3342
			// Apply the text shadow
3343
			if (textShadow && wrapper.applyTextShadow) {
3344
				wrapper.applyTextShadow(textShadow);
3345
			}
3346
		}
3347
	},
3348
 
3349
 
3350
 
3351
	/*
3352
	breakText: function (wrapper, width) {
3353
		var bBox = wrapper.getBBox(),
3354
			node = wrapper.element,
3355
			textLength = node.textContent.length,
3356
			pos = mathRound(width * textLength / bBox.width), // try this position first, based on average character width
3357
			increment = 0,
3358
			finalPos;
3359
 
3360
		if (bBox.width > width) {
3361
			while (finalPos === undefined) {
3362
				textLength = node.getSubStringLength(0, pos);
3363
 
3364
				if (textLength <= width) {
3365
					if (increment === -1) {
3366
						finalPos = pos;
3367
					} else {
3368
						increment = 1;
3369
					}
3370
				} else {
3371
					if (increment === 1) {
3372
						finalPos = pos - 1;
3373
					} else {
3374
						increment = -1;
3375
					}
3376
				}
3377
				pos += increment;
3378
			}
3379
		}
3380
		console.log(finalPos, node.getSubStringLength(0, finalPos))
3381
	},
3382
	*/
3383
 
3384
	/**
3385
	 * Returns white for dark colors and black for bright colors
3386
	 */
3387
	getContrast: function (color) {
3388
		color = Color(color).rgba;
3389
		return color[0] + color[1] + color[2] > 384 ? '#000000' : '#FFFFFF';
3390
	},
3391
 
3392
	/**
3393
	 * Create a button with preset states
3394
	 * @param {String} text
3395
	 * @param {Number} x
3396
	 * @param {Number} y
3397
	 * @param {Function} callback
3398
	 * @param {Object} normalState
3399
	 * @param {Object} hoverState
3400
	 * @param {Object} pressedState
3401
	 */
3402
	button: function (text, x, y, callback, normalState, hoverState, pressedState, disabledState, shape) {
3403
		var label = this.label(text, x, y, shape, null, null, null, null, 'button'),
3404
			curState = 0,
3405
			stateOptions,
3406
			stateStyle,
3407
			normalStyle,
3408
			hoverStyle,
3409
			pressedStyle,
3410
			disabledStyle,
3411
			verticalGradient = { x1: 0, y1: 0, x2: 0, y2: 1 };
3412
 
3413
		// Normal state - prepare the attributes
3414
		normalState = merge({
3415
			'stroke-width': 1,
3416
			stroke: '#CCCCCC',
3417
			fill: {
3418
				linearGradient: verticalGradient,
3419
				stops: [
3420
					[0, '#FEFEFE'],
3421
					[1, '#F6F6F6']
3422
				]
3423
			},
3424
			r: 2,
3425
			padding: 5,
3426
			style: {
3427
				color: 'black'
3428
			}
3429
		}, normalState);
3430
		normalStyle = normalState.style;
3431
		delete normalState.style;
3432
 
3433
		// Hover state
3434
		hoverState = merge(normalState, {
3435
			stroke: '#68A',
3436
			fill: {
3437
				linearGradient: verticalGradient,
3438
				stops: [
3439
					[0, '#FFF'],
3440
					[1, '#ACF']
3441
				]
3442
			}
3443
		}, hoverState);
3444
		hoverStyle = hoverState.style;
3445
		delete hoverState.style;
3446
 
3447
		// Pressed state
3448
		pressedState = merge(normalState, {
3449
			stroke: '#68A',
3450
			fill: {
3451
				linearGradient: verticalGradient,
3452
				stops: [
3453
					[0, '#9BD'],
3454
					[1, '#CDF']
3455
				]
3456
			}
3457
		}, pressedState);
3458
		pressedStyle = pressedState.style;
3459
		delete pressedState.style;
3460
 
3461
		// Disabled state
3462
		disabledState = merge(normalState, {
3463
			style: {
3464
				color: '#CCC'
3465
			}
3466
		}, disabledState);
3467
		disabledStyle = disabledState.style;
3468
		delete disabledState.style;
3469
 
3470
		// Add the events. IE9 and IE10 need mouseover and mouseout to funciton (#667).
3471
		addEvent(label.element, isIE ? 'mouseover' : 'mouseenter', function () {
3472
			if (curState !== 3) {
3473
				label.attr(hoverState)
3474
					.css(hoverStyle);
3475
			}
3476
		});
3477
		addEvent(label.element, isIE ? 'mouseout' : 'mouseleave', function () {
3478
			if (curState !== 3) {
3479
				stateOptions = [normalState, hoverState, pressedState][curState];
3480
				stateStyle = [normalStyle, hoverStyle, pressedStyle][curState];
3481
				label.attr(stateOptions)
3482
					.css(stateStyle);
3483
			}
3484
		});
3485
 
3486
		label.setState = function (state) {
3487
			label.state = curState = state;
3488
			if (!state) {
3489
				label.attr(normalState)
3490
					.css(normalStyle);
3491
			} else if (state === 2) {
3492
				label.attr(pressedState)
3493
					.css(pressedStyle);
3494
			} else if (state === 3) {
3495
				label.attr(disabledState)
3496
					.css(disabledStyle);
3497
			}
3498
		};
3499
 
3500
		return label
3501
			.on('click', function (e) {
3502
				if (curState !== 3) {
3503
					callback.call(label, e);
3504
				}
3505
			})
3506
			.attr(normalState)
3507
			.css(extend({ cursor: 'default' }, normalStyle));
3508
	},
3509
 
3510
	/**
3511
	 * Make a straight line crisper by not spilling out to neighbour pixels
3512
	 * @param {Array} points
3513
	 * @param {Number} width
3514
	 */
3515
	crispLine: function (points, width) {
3516
		// points format: [M, 0, 0, L, 100, 0]
3517
		// normalize to a crisp line
3518
		if (points[1] === points[4]) {
3519
			// Substract due to #1129. Now bottom and left axis gridlines behave the same.
3520
			points[1] = points[4] = mathRound(points[1]) - (width % 2 / 2);
3521
		}
3522
		if (points[2] === points[5]) {
3523
			points[2] = points[5] = mathRound(points[2]) + (width % 2 / 2);
3524
		}
3525
		return points;
3526
	},
3527
 
3528
 
3529
	/**
3530
	 * Draw a path
3531
	 * @param {Array} path An SVG path in array form
3532
	 */
3533
	path: function (path) {
3534
		var attr = {
3535
			fill: NONE
3536
		};
3537
		if (isArray(path)) {
3538
			attr.d = path;
3539
		} else if (isObject(path)) { // attributes
3540
			extend(attr, path);
3541
		}
3542
		return this.createElement('path').attr(attr);
3543
	},
3544
 
3545
	/**
3546
	 * Draw and return an SVG circle
3547
	 * @param {Number} x The x position
3548
	 * @param {Number} y The y position
3549
	 * @param {Number} r The radius
3550
	 */
3551
	circle: function (x, y, r) {
3552
		var attr = isObject(x) ?
3553
			x :
3554
			{
3555
				x: x,
3556
				y: y,
3557
				r: r
3558
			},
3559
			wrapper = this.createElement('circle');
3560
 
3561
		wrapper.xSetter = function (value) {
3562
			this.element.setAttribute('cx', value);
3563
		};
3564
		wrapper.ySetter = function (value) {
3565
			this.element.setAttribute('cy', value);
3566
		};
3567
		return wrapper.attr(attr);
3568
	},
3569
 
3570
	/**
3571
	 * Draw and return an arc
3572
	 * @param {Number} x X position
3573
	 * @param {Number} y Y position
3574
	 * @param {Number} r Radius
3575
	 * @param {Number} innerR Inner radius like used in donut charts
3576
	 * @param {Number} start Starting angle
3577
	 * @param {Number} end Ending angle
3578
	 */
3579
	arc: function (x, y, r, innerR, start, end) {
3580
		var arc;
3581
 
3582
		if (isObject(x)) {
3583
			y = x.y;
3584
			r = x.r;
3585
			innerR = x.innerR;
3586
			start = x.start;
3587
			end = x.end;
3588
			x = x.x;
3589
		}
3590
 
3591
		// Arcs are defined as symbols for the ability to set
3592
		// attributes in attr and animate
3593
		arc = this.symbol('arc', x || 0, y || 0, r || 0, r || 0, {
3594
			innerR: innerR || 0,
3595
			start: start || 0,
3596
			end: end || 0
3597
		});
3598
		arc.r = r; // #959
3599
		return arc;
3600
	},
3601
 
3602
	/**
3603
	 * Draw and return a rectangle
3604
	 * @param {Number} x Left position
3605
	 * @param {Number} y Top position
3606
	 * @param {Number} width
3607
	 * @param {Number} height
3608
	 * @param {Number} r Border corner radius
3609
	 * @param {Number} strokeWidth A stroke width can be supplied to allow crisp drawing
3610
	 */
3611
	rect: function (x, y, width, height, r, strokeWidth) {
3612
 
3613
		r = isObject(x) ? x.r : r;
3614
 
3615
		var wrapper = this.createElement('rect'),
3616
			attribs = isObject(x) ? x : x === UNDEFINED ? {} : {
3617
				x: x,
3618
				y: y,
3619
				width: mathMax(width, 0),
3620
				height: mathMax(height, 0)
3621
			};
3622
 
3623
		if (strokeWidth !== UNDEFINED) {
3624
			attribs.strokeWidth = strokeWidth;
3625
			attribs = wrapper.crisp(attribs);
3626
		}
3627
 
3628
		if (r) {
3629
			attribs.r = r;
3630
		}
3631
 
3632
		wrapper.rSetter = function (value) {
3633
			attr(this.element, {
3634
				rx: value,
3635
				ry: value
3636
			});
3637
		};
3638
 
3639
		return wrapper.attr(attribs);
3640
	},
3641
 
3642
	/**
3643
	 * Resize the box and re-align all aligned elements
3644
	 * @param {Object} width
3645
	 * @param {Object} height
3646
	 * @param {Boolean} animate
3647
	 *
3648
	 */
3649
	setSize: function (width, height, animate) {
3650
		var renderer = this,
3651
			alignedObjects = renderer.alignedObjects,
3652
			i = alignedObjects.length;
3653
 
3654
		renderer.width = width;
3655
		renderer.height = height;
3656
 
3657
		renderer.boxWrapper[pick(animate, true) ? 'animate' : 'attr']({
3658
			width: width,
3659
			height: height
3660
		});
3661
 
3662
		while (i--) {
3663
			alignedObjects[i].align();
3664
		}
3665
	},
3666
 
3667
	/**
3668
	 * Create a group
3669
	 * @param {String} name The group will be given a class name of 'highcharts-{name}'.
3670
	 *	 This can be used for styling and scripting.
3671
	 */
3672
	g: function (name) {
3673
		var elem = this.createElement('g');
3674
		return defined(name) ? elem.attr({ 'class': PREFIX + name }) : elem;
3675
	},
3676
 
3677
	/**
3678
	 * Display an image
3679
	 * @param {String} src
3680
	 * @param {Number} x
3681
	 * @param {Number} y
3682
	 * @param {Number} width
3683
	 * @param {Number} height
3684
	 */
3685
	image: function (src, x, y, width, height) {
3686
		var attribs = {
3687
				preserveAspectRatio: NONE
3688
			},
3689
			elemWrapper;
3690
 
3691
		// optional properties
3692
		if (arguments.length > 1) {
3693
			extend(attribs, {
3694
				x: x,
3695
				y: y,
3696
				width: width,
3697
				height: height
3698
			});
3699
		}
3700
 
3701
		elemWrapper = this.createElement('image').attr(attribs);
3702
 
3703
		// set the href in the xlink namespace
3704
		if (elemWrapper.element.setAttributeNS) {
3705
			elemWrapper.element.setAttributeNS('http://www.w3.org/1999/xlink',
3706
				'href', src);
3707
		} else {
3708
			// could be exporting in IE
3709
			// using href throws "not supported" in ie7 and under, requries regex shim to fix later
3710
			elemWrapper.element.setAttribute('hc-svg-href', src);
3711
		}
3712
		return elemWrapper;
3713
	},
3714
 
3715
	/**
3716
	 * Draw a symbol out of pre-defined shape paths from the namespace 'symbol' object.
3717
	 *
3718
	 * @param {Object} symbol
3719
	 * @param {Object} x
3720
	 * @param {Object} y
3721
	 * @param {Object} radius
3722
	 * @param {Object} options
3723
	 */
3724
	symbol: function (symbol, x, y, width, height, options) {
3725
 
3726
		var obj,
3727
 
3728
			// get the symbol definition function
3729
			symbolFn = this.symbols[symbol],
3730
 
3731
			// check if there's a path defined for this symbol
3732
			path = symbolFn && symbolFn(
3733
				mathRound(x),
3734
				mathRound(y),
3735
				width,
3736
				height,
3737
				options
3738
			),
3739
 
3740
			imageElement,
3741
			imageRegex = /^url\((.*?)\)$/,
3742
			imageSrc,
3743
			imageSize,
3744
			centerImage;
3745
 
3746
		if (path) {
3747
 
3748
			obj = this.path(path);
3749
			// expando properties for use in animate and attr
3750
			extend(obj, {
3751
				symbolName: symbol,
3752
				x: x,
3753
				y: y,
3754
				width: width,
3755
				height: height
3756
			});
3757
			if (options) {
3758
				extend(obj, options);
3759
			}
3760
 
3761
 
3762
		// image symbols
3763
		} else if (imageRegex.test(symbol)) {
3764
 
3765
			// On image load, set the size and position
3766
			centerImage = function (img, size) {
3767
				if (img.element) { // it may be destroyed in the meantime (#1390)
3768
					img.attr({
3769
						width: size[0],
3770
						height: size[1]
3771
					});
3772
 
3773
					if (!img.alignByTranslate) { // #185
3774
						img.translate(
3775
							mathRound((width - size[0]) / 2), // #1378
3776
							mathRound((height - size[1]) / 2)
3777
						);
3778
					}
3779
				}
3780
			};
3781
 
3782
			imageSrc = symbol.match(imageRegex)[1];
3783
			imageSize = symbolSizes[imageSrc] || (options && options.width && options.height && [options.width, options.height]);
3784
 
3785
			// Ireate the image synchronously, add attribs async
3786
			obj = this.image(imageSrc)
3787
				.attr({
3788
					x: x,
3789
					y: y
3790
				});
3791
			obj.isImg = true;
3792
 
3793
			if (imageSize) {
3794
				centerImage(obj, imageSize);
3795
			} else {
3796
				// Initialize image to be 0 size so export will still function if there's no cached sizes.
3797
				obj.attr({ width: 0, height: 0 });
3798
 
3799
				// Create a dummy JavaScript image to get the width and height. Due to a bug in IE < 8,
3800
				// the created element must be assigned to a variable in order to load (#292).
3801
				imageElement = createElement('img', {
3802
					onload: function () {
3803
						centerImage(obj, symbolSizes[imageSrc] = [this.width, this.height]);
3804
					},
3805
					src: imageSrc
3806
				});
3807
			}
3808
		}
3809
 
3810
		return obj;
3811
	},
3812
 
3813
	/**
3814
	 * An extendable collection of functions for defining symbol paths.
3815
	 */
3816
	symbols: {
3817
		'circle': function (x, y, w, h) {
3818
			var cpw = 0.166 * w;
3819
			return [
3820
				M, x + w / 2, y,
3821
				'C', x + w + cpw, y, x + w + cpw, y + h, x + w / 2, y + h,
3822
				'C', x - cpw, y + h, x - cpw, y, x + w / 2, y,
3823
				'Z'
3824
			];
3825
		},
3826
 
3827
		'square': function (x, y, w, h) {
3828
			return [
3829
				M, x, y,
3830
				L, x + w, y,
3831
				x + w, y + h,
3832
				x, y + h,
3833
				'Z'
3834
			];
3835
		},
3836
 
3837
		'triangle': function (x, y, w, h) {
3838
			return [
3839
				M, x + w / 2, y,
3840
				L, x + w, y + h,
3841
				x, y + h,
3842
				'Z'
3843
			];
3844
		},
3845
 
3846
		'triangle-down': function (x, y, w, h) {
3847
			return [
3848
				M, x, y,
3849
				L, x + w, y,
3850
				x + w / 2, y + h,
3851
				'Z'
3852
			];
3853
		},
3854
		'diamond': function (x, y, w, h) {
3855
			return [
3856
				M, x + w / 2, y,
3857
				L, x + w, y + h / 2,
3858
				x + w / 2, y + h,
3859
				x, y + h / 2,
3860
				'Z'
3861
			];
3862
		},
3863
		'arc': function (x, y, w, h, options) {
3864
			var start = options.start,
3865
				radius = options.r || w || h,
3866
				end = options.end - 0.001, // to prevent cos and sin of start and end from becoming equal on 360 arcs (related: #1561)
3867
				innerRadius = options.innerR,
3868
				open = options.open,
3869
				cosStart = mathCos(start),
3870
				sinStart = mathSin(start),
3871
				cosEnd = mathCos(end),
3872
				sinEnd = mathSin(end),
3873
				longArc = options.end - start < mathPI ? 0 : 1;
3874
 
3875
			return [
3876
				M,
3877
				x + radius * cosStart,
3878
				y + radius * sinStart,
3879
				'A', // arcTo
3880
				radius, // x radius
3881
				radius, // y radius
3882
				0, // slanting
3883
				longArc, // long or short arc
3884
				1, // clockwise
3885
				x + radius * cosEnd,
3886
				y + radius * sinEnd,
3887
				open ? M : L,
3888
				x + innerRadius * cosEnd,
3889
				y + innerRadius * sinEnd,
3890
				'A', // arcTo
3891
				innerRadius, // x radius
3892
				innerRadius, // y radius
3893
				0, // slanting
3894
				longArc, // long or short arc
3895
				0, // clockwise
3896
				x + innerRadius * cosStart,
3897
				y + innerRadius * sinStart,
3898
 
3899
				open ? '' : 'Z' // close
3900
			];
3901
		},
3902
 
3903
		/**
3904
		 * Callout shape used for default tooltips, also used for rounded rectangles in VML
3905
		 */
3906
		callout: function (x, y, w, h, options) {
3907
			var arrowLength = 6,
3908
				halfDistance = 6,
3909
				r = mathMin((options && options.r) || 0, w, h),
3910
				safeDistance = r + halfDistance,
3911
				anchorX = options && options.anchorX,
3912
				anchorY = options && options.anchorY,
3913
				path;
3914
 
3915
			path = [
3916
				'M', x + r, y,
3917
				'L', x + w - r, y, // top side
3918
				'C', x + w, y, x + w, y, x + w, y + r, // top-right corner
3919
				'L', x + w, y + h - r, // right side
3920
				'C', x + w, y + h, x + w, y + h, x + w - r, y + h, // bottom-right corner
3921
				'L', x + r, y + h, // bottom side
3922
				'C', x, y + h, x, y + h, x, y + h - r, // bottom-left corner
3923
				'L', x, y + r, // left side
3924
				'C', x, y, x, y, x + r, y // top-right corner
3925
			];
3926
 
3927
			if (anchorX && anchorX > w && anchorY > y + safeDistance && anchorY < y + h - safeDistance) { // replace right side
3928
				path.splice(13, 3,
3929
					'L', x + w, anchorY - halfDistance,
3930
					x + w + arrowLength, anchorY,
3931
					x + w, anchorY + halfDistance,
3932
					x + w, y + h - r
3933
				);
3934
			} else if (anchorX && anchorX < 0 && anchorY > y + safeDistance && anchorY < y + h - safeDistance) { // replace left side
3935
				path.splice(33, 3,
3936
					'L', x, anchorY + halfDistance,
3937
					x - arrowLength, anchorY,
3938
					x, anchorY - halfDistance,
3939
					x, y + r
3940
				);
3941
			} else if (anchorY && anchorY > h && anchorX > x + safeDistance && anchorX < x + w - safeDistance) { // replace bottom
3942
				path.splice(23, 3,
3943
					'L', anchorX + halfDistance, y + h,
3944
					anchorX, y + h + arrowLength,
3945
					anchorX - halfDistance, y + h,
3946
					x + r, y + h
3947
				);
3948
			} else if (anchorY && anchorY < 0 && anchorX > x + safeDistance && anchorX < x + w - safeDistance) { // replace top
3949
				path.splice(3, 3,
3950
					'L', anchorX - halfDistance, y,
3951
					anchorX, y - arrowLength,
3952
					anchorX + halfDistance, y,
3953
					w - r, y
3954
				);
3955
			}
3956
			return path;
3957
		}
3958
	},
3959
 
3960
	/**
3961
	 * Define a clipping rectangle
3962
	 * @param {String} id
3963
	 * @param {Number} x
3964
	 * @param {Number} y
3965
	 * @param {Number} width
3966
	 * @param {Number} height
3967
	 */
3968
	clipRect: function (x, y, width, height) {
3969
		var wrapper,
3970
			id = PREFIX + idCounter++,
3971
 
3972
			clipPath = this.createElement('clipPath').attr({
3973
				id: id
3974
			}).add(this.defs);
3975
 
3976
		wrapper = this.rect(x, y, width, height, 0).add(clipPath);
3977
		wrapper.id = id;
3978
		wrapper.clipPath = clipPath;
3979
		wrapper.count = 0;
3980
 
3981
		return wrapper;
3982
	},
3983
 
3984
 
3985
 
3986
 
3987
 
3988
	/**
3989
	 * Add text to the SVG object
3990
	 * @param {String} str
3991
	 * @param {Number} x Left position
3992
	 * @param {Number} y Top position
3993
	 * @param {Boolean} useHTML Use HTML to render the text
3994
	 */
3995
	text: function (str, x, y, useHTML) {
3996
 
3997
		// declare variables
3998
		var renderer = this,
3999
			fakeSVG = useCanVG || (!hasSVG && renderer.forExport),
4000
			wrapper,
4001
			attr = {};
4002
 
4003
		if (useHTML && !renderer.forExport) {
4004
			return renderer.html(str, x, y);
4005
		}
4006
 
4007
		attr.x = Math.round(x || 0); // X is always needed for line-wrap logic
4008
		if (y) {
4009
			attr.y = Math.round(y);
4010
		}
4011
		if (str || str === 0) {
4012
			attr.text = str;
4013
		}
4014
 
4015
		wrapper = renderer.createElement('text')
4016
			.attr(attr);
4017
 
4018
		// Prevent wrapping from creating false offsetWidths in export in legacy IE (#1079, #1063)
4019
		if (fakeSVG) {
4020
			wrapper.css({
4021
				position: ABSOLUTE
4022
			});
4023
		}
4024
 
4025
		if (!useHTML) {
4026
			wrapper.xSetter = function (value, key, element) {
4027
				var tspans = element.getElementsByTagName('tspan'),
4028
					tspan,
4029
					parentVal = element.getAttribute(key),
4030
					i;
4031
				for (i = 0; i < tspans.length; i++) {
4032
					tspan = tspans[i];
4033
					// If the x values are equal, the tspan represents a linebreak
4034
					if (tspan.getAttribute(key) === parentVal) {
4035
						tspan.setAttribute(key, value);
4036
					}
4037
				}
4038
				element.setAttribute(key, value);
4039
			};
4040
		}
4041
 
4042
		return wrapper;
4043
	},
4044
 
4045
	/**
4046
	 * Utility to return the baseline offset and total line height from the font size
4047
	 */
4048
	fontMetrics: function (fontSize, elem) {
4049
		var lineHeight,
4050
			baseline,
4051
			style;
4052
 
4053
		fontSize = fontSize || this.style.fontSize;
4054
		if (elem && win.getComputedStyle) {
4055
			elem = elem.element || elem; // SVGElement
4056
			style = win.getComputedStyle(elem, "");
4057
			fontSize = style && style.fontSize; // #4309, the style doesn't exist inside a hidden iframe in Firefox
4058
		}
4059
		fontSize = /px/.test(fontSize) ? pInt(fontSize) : /em/.test(fontSize) ? parseFloat(fontSize) * 12 : 12;
4060
 
4061
		// Empirical values found by comparing font size and bounding box height.
4062
		// Applies to the default font family. http://jsfiddle.net/highcharts/7xvn7/
4063
		lineHeight = fontSize < 24 ? fontSize + 3 : mathRound(fontSize * 1.2);
4064
		baseline = mathRound(lineHeight * 0.8);
4065
 
4066
		return {
4067
			h: lineHeight,
4068
			b: baseline,
4069
			f: fontSize
4070
		};
4071
	},
4072
 
4073
	/**
4074
	 * Correct X and Y positioning of a label for rotation (#1764)
4075
	 */
4076
	rotCorr: function (baseline, rotation, alterY) {
4077
		var y = baseline;
4078
		if (rotation && alterY) {
4079
			y = mathMax(y * mathCos(rotation * deg2rad), 4);
4080
		}
4081
		return {
4082
			x: (-baseline / 3) * mathSin(rotation * deg2rad),
4083
			y: y
4084
		};
4085
	},
4086
 
4087
	/**
4088
	 * Add a label, a text item that can hold a colored or gradient background
4089
	 * as well as a border and shadow.
4090
	 * @param {string} str
4091
	 * @param {Number} x
4092
	 * @param {Number} y
4093
	 * @param {String} shape
4094
	 * @param {Number} anchorX In case the shape has a pointer, like a flag, this is the
4095
	 *	coordinates it should be pinned to
4096
	 * @param {Number} anchorY
4097
	 * @param {Boolean} baseline Whether to position the label relative to the text baseline,
4098
	 *	like renderer.text, or to the upper border of the rectangle.
4099
	 * @param {String} className Class name for the group
4100
	 */
4101
	label: function (str, x, y, shape, anchorX, anchorY, useHTML, baseline, className) {
4102
 
4103
		var renderer = this,
4104
			wrapper = renderer.g(className),
4105
			text = renderer.text('', 0, 0, useHTML)
4106
				.attr({
4107
					zIndex: 1
4108
				}),
4109
				//.add(wrapper),
4110
			box,
4111
			bBox,
4112
			alignFactor = 0,
4113
			padding = 3,
4114
			paddingLeft = 0,
4115
			width,
4116
			height,
4117
			wrapperX,
4118
			wrapperY,
4119
			crispAdjust = 0,
4120
			deferredAttr = {},
4121
			baselineOffset,
4122
			needsBox;
4123
 
4124
		/**
4125
		 * This function runs after the label is added to the DOM (when the bounding box is
4126
		 * available), and after the text of the label is updated to detect the new bounding
4127
		 * box and reflect it in the border box.
4128
		 */
4129
		function updateBoxSize() {
4130
			var boxX,
4131
				boxY,
4132
				style = text.element.style;
4133
 
4134
			bBox = (width === undefined || height === undefined || wrapper.styles.textAlign) && defined(text.textStr) &&
4135
				text.getBBox(); //#3295 && 3514 box failure when string equals 0
4136
			wrapper.width = (width || bBox.width || 0) + 2 * padding + paddingLeft;
4137
			wrapper.height = (height || bBox.height || 0) + 2 * padding;
4138
 
4139
			// update the label-scoped y offset
4140
			baselineOffset = padding + renderer.fontMetrics(style && style.fontSize, text).b;
4141
 
4142
 
4143
			if (needsBox) {
4144
 
4145
				// create the border box if it is not already present
4146
				if (!box) {
4147
					boxX = mathRound(-alignFactor * padding) + crispAdjust;
4148
					boxY = (baseline ? -baselineOffset : 0) + crispAdjust;
4149
 
4150
					wrapper.box = box = shape ?
4151
						renderer.symbol(shape, boxX, boxY, wrapper.width, wrapper.height, deferredAttr) :
4152
						renderer.rect(boxX, boxY, wrapper.width, wrapper.height, 0, deferredAttr[STROKE_WIDTH]);
4153
 
4154
					if (!box.isImg) { // #4324, fill "none" causes it to be ignored by mouse events in IE
4155
						box.attr('fill', NONE);
4156
					}
4157
					box.add(wrapper);
4158
				}
4159
 
4160
				// apply the box attributes
4161
				if (!box.isImg) { // #1630
4162
					box.attr(extend({
4163
						width: mathRound(wrapper.width),
4164
						height: mathRound(wrapper.height)
4165
					}, deferredAttr));
4166
				}
4167
				deferredAttr = null;
4168
			}
4169
		}
4170
 
4171
		/**
4172
		 * This function runs after setting text or padding, but only if padding is changed
4173
		 */
4174
		function updateTextPadding() {
4175
			var styles = wrapper.styles,
4176
				textAlign = styles && styles.textAlign,
4177
				x = paddingLeft + padding * (1 - alignFactor),
4178
				y;
4179
 
4180
			// determin y based on the baseline
4181
			y = baseline ? 0 : baselineOffset;
4182
 
4183
			// compensate for alignment
4184
			if (defined(width) && bBox && (textAlign === 'center' || textAlign === 'right')) {
4185
				x += { center: 0.5, right: 1 }[textAlign] * (width - bBox.width);
4186
			}
4187
 
4188
			// update if anything changed
4189
			if (x !== text.x || y !== text.y) {
4190
				text.attr('x', x);
4191
				if (y !== UNDEFINED) {
4192
					text.attr('y', y);
4193
				}
4194
			}
4195
 
4196
			// record current values
4197
			text.x = x;
4198
			text.y = y;
4199
		}
4200
 
4201
		/**
4202
		 * Set a box attribute, or defer it if the box is not yet created
4203
		 * @param {Object} key
4204
		 * @param {Object} value
4205
		 */
4206
		function boxAttr(key, value) {
4207
			if (box) {
4208
				box.attr(key, value);
4209
			} else {
4210
				deferredAttr[key] = value;
4211
			}
4212
		}
4213
 
4214
		/**
4215
		 * After the text element is added, get the desired size of the border box
4216
		 * and add it before the text in the DOM.
4217
		 */
4218
		wrapper.onAdd = function () {
4219
			text.add(wrapper);
4220
			wrapper.attr({
4221
				text: (str || str === 0) ? str : '', // alignment is available now // #3295: 0 not rendered if given as a value
4222
				x: x,
4223
				y: y
4224
			});
4225
 
4226
			if (box && defined(anchorX)) {
4227
				wrapper.attr({
4228
					anchorX: anchorX,
4229
					anchorY: anchorY
4230
				});
4231
			}
4232
		};
4233
 
4234
		/*
4235
		 * Add specific attribute setters.
4236
		 */
4237
 
4238
		// only change local variables
4239
		wrapper.widthSetter = function (value) {
4240
			width = value;
4241
		};
4242
		wrapper.heightSetter = function (value) {
4243
			height = value;
4244
		};
4245
		wrapper.paddingSetter =  function (value) {
4246
			if (defined(value) && value !== padding) {
4247
				padding = wrapper.padding = value;
4248
				updateTextPadding();
4249
			}
4250
		};
4251
		wrapper.paddingLeftSetter =  function (value) {
4252
			if (defined(value) && value !== paddingLeft) {
4253
				paddingLeft = value;
4254
				updateTextPadding();
4255
			}
4256
		};
4257
 
4258
 
4259
		// change local variable and prevent setting attribute on the group
4260
		wrapper.alignSetter = function (value) {
4261
			alignFactor = { left: 0, center: 0.5, right: 1 }[value];
4262
		};
4263
 
4264
		// apply these to the box and the text alike
4265
		wrapper.textSetter = function (value) {
4266
			if (value !== UNDEFINED) {
4267
				text.textSetter(value);
4268
			}
4269
			updateBoxSize();
4270
			updateTextPadding();
4271
		};
4272
 
4273
		// apply these to the box but not to the text
4274
		wrapper['stroke-widthSetter'] = function (value, key) {
4275
			if (value) {
4276
				needsBox = true;
4277
			}
4278
			crispAdjust = value % 2 / 2;
4279
			boxAttr(key, value);
4280
		};
4281
		wrapper.strokeSetter = wrapper.fillSetter = wrapper.rSetter = function (value, key) {
4282
			if (key === 'fill' && value) {
4283
				needsBox = true;
4284
			}
4285
			boxAttr(key, value);
4286
		};
4287
		wrapper.anchorXSetter = function (value, key) {
4288
			anchorX = value;
4289
			boxAttr(key, mathRound(value) - crispAdjust - wrapperX);
4290
		};
4291
		wrapper.anchorYSetter = function (value, key) {
4292
			anchorY = value;
4293
			boxAttr(key, value - wrapperY);
4294
		};
4295
 
4296
		// rename attributes
4297
		wrapper.xSetter = function (value) {
4298
			wrapper.x = value; // for animation getter
4299
			if (alignFactor) {
4300
				value -= alignFactor * ((width || bBox.width) + padding);
4301
			}
4302
			wrapperX = mathRound(value);
4303
			wrapper.attr('translateX', wrapperX);
4304
		};
4305
		wrapper.ySetter = function (value) {
4306
			wrapperY = wrapper.y = mathRound(value);
4307
			wrapper.attr('translateY', wrapperY);
4308
		};
4309
 
4310
		// Redirect certain methods to either the box or the text
4311
		var baseCss = wrapper.css;
4312
		return extend(wrapper, {
4313
			/**
4314
			 * Pick up some properties and apply them to the text instead of the wrapper
4315
			 */
4316
			css: function (styles) {
4317
				if (styles) {
4318
					var textStyles = {};
4319
					styles = merge(styles); // create a copy to avoid altering the original object (#537)
4320
					each(wrapper.textProps, function (prop) {
4321
						if (styles[prop] !== UNDEFINED) {
4322
							textStyles[prop] = styles[prop];
4323
							delete styles[prop];
4324
						}
4325
					});
4326
					text.css(textStyles);
4327
				}
4328
				return baseCss.call(wrapper, styles);
4329
			},
4330
			/**
4331
			 * Return the bounding box of the box, not the group
4332
			 */
4333
			getBBox: function () {
4334
				return {
4335
					width: bBox.width + 2 * padding,
4336
					height: bBox.height + 2 * padding,
4337
					x: bBox.x - padding,
4338
					y: bBox.y - padding
4339
				};
4340
			},
4341
			/**
4342
			 * Apply the shadow to the box
4343
			 */
4344
			shadow: function (b) {
4345
				if (box) {
4346
					box.shadow(b);
4347
				}
4348
				return wrapper;
4349
			},
4350
			/**
4351
			 * Destroy and release memory.
4352
			 */
4353
			destroy: function () {
4354
 
4355
				// Added by button implementation
4356
				removeEvent(wrapper.element, 'mouseenter');
4357
				removeEvent(wrapper.element, 'mouseleave');
4358
 
4359
				if (text) {
4360
					text = text.destroy();
4361
				}
4362
				if (box) {
4363
					box = box.destroy();
4364
				}
4365
				// Call base implementation to destroy the rest
4366
				SVGElement.prototype.destroy.call(wrapper);
4367
 
4368
				// Release local pointers (#1298)
4369
				wrapper = renderer = updateBoxSize = updateTextPadding = boxAttr = null;
4370
			}
4371
		});
4372
	}
4373
}; // end SVGRenderer
4374
 
4375
 
4376
// general renderer
4377
Renderer = SVGRenderer;
4378
// extend SvgElement for useHTML option
4379
extend(SVGElement.prototype, {
4380
	/**
4381
	 * Apply CSS to HTML elements. This is used in text within SVG rendering and
4382
	 * by the VML renderer
4383
	 */
4384
	htmlCss: function (styles) {
4385
		var wrapper = this,
4386
			element = wrapper.element,
4387
			textWidth = styles && element.tagName === 'SPAN' && styles.width;
4388
 
4389
		if (textWidth) {
4390
			delete styles.width;
4391
			wrapper.textWidth = textWidth;
4392
			wrapper.updateTransform();
4393
		}
4394
		if (styles && styles.textOverflow === 'ellipsis') {
4395
			styles.whiteSpace = 'nowrap';
4396
			styles.overflow = 'hidden';
4397
		}
4398
		wrapper.styles = extend(wrapper.styles, styles);
4399
		css(wrapper.element, styles);
4400
 
4401
		return wrapper;
4402
	},
4403
 
4404
	/**
4405
	 * VML and useHTML method for calculating the bounding box based on offsets
4406
	 * @param {Boolean} refresh Whether to force a fresh value from the DOM or to
4407
	 * use the cached value
4408
	 *
4409
	 * @return {Object} A hash containing values for x, y, width and height
4410
	 */
4411
 
4412
	htmlGetBBox: function () {
4413
		var wrapper = this,
4414
			element = wrapper.element;
4415
 
4416
		// faking getBBox in exported SVG in legacy IE
4417
		// faking getBBox in exported SVG in legacy IE (is this a duplicate of the fix for #1079?)
4418
		if (element.nodeName === 'text') {
4419
			element.style.position = ABSOLUTE;
4420
		}
4421
 
4422
		return {
4423
			x: element.offsetLeft,
4424
			y: element.offsetTop,
4425
			width: element.offsetWidth,
4426
			height: element.offsetHeight
4427
		};
4428
	},
4429
 
4430
	/**
4431
	 * VML override private method to update elements based on internal
4432
	 * properties based on SVG transform
4433
	 */
4434
	htmlUpdateTransform: function () {
4435
		// aligning non added elements is expensive
4436
		if (!this.added) {
4437
			this.alignOnAdd = true;
4438
			return;
4439
		}
4440
 
4441
		var wrapper = this,
4442
			renderer = wrapper.renderer,
4443
			elem = wrapper.element,
4444
			translateX = wrapper.translateX || 0,
4445
			translateY = wrapper.translateY || 0,
4446
			x = wrapper.x || 0,
4447
			y = wrapper.y || 0,
4448
			align = wrapper.textAlign || 'left',
4449
			alignCorrection = { left: 0, center: 0.5, right: 1 }[align],
4450
			shadows = wrapper.shadows,
4451
			styles = wrapper.styles;
4452
 
4453
		// apply translate
4454
		css(elem, {
4455
			marginLeft: translateX,
4456
			marginTop: translateY
4457
		});
4458
		if (shadows) { // used in labels/tooltip
4459
			each(shadows, function (shadow) {
4460
				css(shadow, {
4461
					marginLeft: translateX + 1,
4462
					marginTop: translateY + 1
4463
				});
4464
			});
4465
		}
4466
 
4467
		// apply inversion
4468
		if (wrapper.inverted) { // wrapper is a group
4469
			each(elem.childNodes, function (child) {
4470
				renderer.invertChild(child, elem);
4471
			});
4472
		}
4473
 
4474
		if (elem.tagName === 'SPAN') {
4475
 
4476
			var width,
4477
				rotation = wrapper.rotation,
4478
				baseline,
4479
				textWidth = pInt(wrapper.textWidth),
4480
				currentTextTransform = [rotation, align, elem.innerHTML, wrapper.textWidth, wrapper.textAlign].join(',');
4481
 
4482
			if (currentTextTransform !== wrapper.cTT) { // do the calculations and DOM access only if properties changed
4483
 
4484
 
4485
				baseline = renderer.fontMetrics(elem.style.fontSize).b;
4486
 
4487
				// Renderer specific handling of span rotation
4488
				if (defined(rotation)) {
4489
					wrapper.setSpanRotation(rotation, alignCorrection, baseline);
4490
				}
4491
 
4492
				width = pick(wrapper.elemWidth, elem.offsetWidth);
4493
 
4494
				// Update textWidth
4495
				if (width > textWidth && /[ \-]/.test(elem.textContent || elem.innerText)) { // #983, #1254
4496
					css(elem, {
4497
						width: textWidth + PX,
4498
						display: 'block',
4499
						whiteSpace: (styles && styles.whiteSpace) || 'normal' // #3331
4500
					});
4501
					width = textWidth;
4502
				}
4503
 
4504
				wrapper.getSpanCorrection(width, baseline, alignCorrection, rotation, align);
4505
			}
4506
 
4507
			// apply position with correction
4508
			css(elem, {
4509
				left: (x + (wrapper.xCorr || 0)) + PX,
4510
				top: (y + (wrapper.yCorr || 0)) + PX
4511
			});
4512
 
4513
			// force reflow in webkit to apply the left and top on useHTML element (#1249)
4514
			if (isWebKit) {
4515
				baseline = elem.offsetHeight; // assigned to baseline for JSLint purpose
4516
			}
4517
 
4518
			// record current text transform
4519
			wrapper.cTT = currentTextTransform;
4520
		}
4521
	},
4522
 
4523
	/**
4524
	 * Set the rotation of an individual HTML span
4525
	 */
4526
	setSpanRotation: function (rotation, alignCorrection, baseline) {
4527
		var rotationStyle = {},
4528
			cssTransformKey = isIE ? '-ms-transform' : isWebKit ? '-webkit-transform' : isFirefox ? 'MozTransform' : isOpera ? '-o-transform' : '';
4529
 
4530
		rotationStyle[cssTransformKey] = rotationStyle.transform = 'rotate(' + rotation + 'deg)';
4531
		rotationStyle[cssTransformKey + (isFirefox ? 'Origin' : '-origin')] = rotationStyle.transformOrigin = (alignCorrection * 100) + '% ' + baseline + 'px';
4532
		css(this.element, rotationStyle);
4533
	},
4534
 
4535
	/**
4536
	 * Get the correction in X and Y positioning as the element is rotated.
4537
	 */
4538
	getSpanCorrection: function (width, baseline, alignCorrection) {
4539
		this.xCorr = -width * alignCorrection;
4540
		this.yCorr = -baseline;
4541
	}
4542
});
4543
 
4544
// Extend SvgRenderer for useHTML option.
4545
extend(SVGRenderer.prototype, {
4546
	/**
4547
	 * Create HTML text node. This is used by the VML renderer as well as the SVG
4548
	 * renderer through the useHTML option.
4549
	 *
4550
	 * @param {String} str
4551
	 * @param {Number} x
4552
	 * @param {Number} y
4553
	 */
4554
	html: function (str, x, y) {
4555
		var wrapper = this.createElement('span'),
4556
			element = wrapper.element,
4557
			renderer = wrapper.renderer;
4558
 
4559
		// Text setter
4560
		wrapper.textSetter = function (value) {
4561
			if (value !== element.innerHTML) {
4562
				delete this.bBox;
4563
			}
4564
			element.innerHTML = this.textStr = value;
4565
			wrapper.htmlUpdateTransform();
4566
		};
4567
 
4568
		// Various setters which rely on update transform
4569
		wrapper.xSetter = wrapper.ySetter = wrapper.alignSetter = wrapper.rotationSetter = function (value, key) {
4570
			if (key === 'align') {
4571
				key = 'textAlign'; // Do not overwrite the SVGElement.align method. Same as VML.
4572
			}
4573
			wrapper[key] = value;
4574
			wrapper.htmlUpdateTransform();
4575
		};
4576
 
4577
		// Set the default attributes
4578
		wrapper.attr({
4579
				text: str,
4580
				x: mathRound(x),
4581
				y: mathRound(y)
4582
			})
4583
			.css({
4584
				position: ABSOLUTE,
4585
				fontFamily: this.style.fontFamily,
4586
				fontSize: this.style.fontSize
4587
			});
4588
 
4589
		// Keep the whiteSpace style outside the wrapper.styles collection
4590
		element.style.whiteSpace = 'nowrap';
4591
 
4592
		// Use the HTML specific .css method
4593
		wrapper.css = wrapper.htmlCss;
4594
 
4595
		// This is specific for HTML within SVG
4596
		if (renderer.isSVG) {
4597
			wrapper.add = function (svgGroupWrapper) {
4598
 
4599
				var htmlGroup,
4600
					container = renderer.box.parentNode,
4601
					parentGroup,
4602
					parents = [];
4603
 
4604
				this.parentGroup = svgGroupWrapper;
4605
 
4606
				// Create a mock group to hold the HTML elements
4607
				if (svgGroupWrapper) {
4608
					htmlGroup = svgGroupWrapper.div;
4609
					if (!htmlGroup) {
4610
 
4611
						// Read the parent chain into an array and read from top down
4612
						parentGroup = svgGroupWrapper;
4613
						while (parentGroup) {
4614
 
4615
							parents.push(parentGroup);
4616
 
4617
							// Move up to the next parent group
4618
							parentGroup = parentGroup.parentGroup;
4619
						}
4620
 
4621
						// Ensure dynamically updating position when any parent is translated
4622
						each(parents.reverse(), function (parentGroup) {
4623
							var htmlGroupStyle,
4624
								cls = attr(parentGroup.element, 'class');
4625
 
4626
							if (cls) {
4627
								cls = { className: cls };
4628
							} // else null
4629
 
4630
							// Create a HTML div and append it to the parent div to emulate
4631
							// the SVG group structure
4632
							htmlGroup = parentGroup.div = parentGroup.div || createElement(DIV, cls, {
4633
								position: ABSOLUTE,
4634
								left: (parentGroup.translateX || 0) + PX,
4635
								top: (parentGroup.translateY || 0) + PX
4636
							}, htmlGroup || container); // the top group is appended to container
4637
 
4638
							// Shortcut
4639
							htmlGroupStyle = htmlGroup.style;
4640
 
4641
							// Set listeners to update the HTML div's position whenever the SVG group
4642
							// position is changed
4643
							extend(parentGroup, {
4644
								translateXSetter: function (value, key) {
4645
									htmlGroupStyle.left = value + PX;
4646
									parentGroup[key] = value;
4647
									parentGroup.doTransform = true;
4648
								},
4649
								translateYSetter: function (value, key) {
4650
									htmlGroupStyle.top = value + PX;
4651
									parentGroup[key] = value;
4652
									parentGroup.doTransform = true;
4653
								}
4654
							});
4655
							wrap(parentGroup, 'visibilitySetter', function (proceed, value, key, elem) {
4656
								proceed.call(this, value, key, elem);
4657
								htmlGroupStyle[key] = value;
4658
							});
4659
						});
4660
 
4661
					}
4662
				} else {
4663
					htmlGroup = container;
4664
				}
4665
 
4666
				htmlGroup.appendChild(element);
4667
 
4668
				// Shared with VML:
4669
				wrapper.added = true;
4670
				if (wrapper.alignOnAdd) {
4671
					wrapper.htmlUpdateTransform();
4672
				}
4673
 
4674
				return wrapper;
4675
			};
4676
		}
4677
		return wrapper;
4678
	}
4679
});
4680
 
4681
/* ****************************************************************************
4682
 *                                                                            *
4683
 * START OF INTERNET EXPLORER <= 8 SPECIFIC CODE                              *
4684
 *                                                                            *
4685
 * For applications and websites that don't need IE support, like platform    *
4686
 * targeted mobile apps and web apps, this code can be removed.               *
4687
 *                                                                            *
4688
 *****************************************************************************/
4689
 
4690
/**
4691
 * @constructor
4692
 */
4693
var VMLRenderer, VMLElement;
4694
if (!hasSVG && !useCanVG) {
4695
 
4696
/**
4697
 * The VML element wrapper.
4698
 */
4699
VMLElement = {
4700
 
4701
	/**
4702
	 * Initialize a new VML element wrapper. It builds the markup as a string
4703
	 * to minimize DOM traffic.
4704
	 * @param {Object} renderer
4705
	 * @param {Object} nodeName
4706
	 */
4707
	init: function (renderer, nodeName) {
4708
		var wrapper = this,
4709
			markup =  ['<', nodeName, ' filled="f" stroked="f"'],
4710
			style = ['position: ', ABSOLUTE, ';'],
4711
			isDiv = nodeName === DIV;
4712
 
4713
		// divs and shapes need size
4714
		if (nodeName === 'shape' || isDiv) {
4715
			style.push('left:0;top:0;width:1px;height:1px;');
4716
		}
4717
		style.push('visibility: ', isDiv ? HIDDEN : VISIBLE);
4718
 
4719
		markup.push(' style="', style.join(''), '"/>');
4720
 
4721
		// create element with default attributes and style
4722
		if (nodeName) {
4723
			markup = isDiv || nodeName === 'span' || nodeName === 'img' ?
4724
				markup.join('')
4725
				: renderer.prepVML(markup);
4726
			wrapper.element = createElement(markup);
4727
		}
4728
 
4729
		wrapper.renderer = renderer;
4730
	},
4731
 
4732
	/**
4733
	 * Add the node to the given parent
4734
	 * @param {Object} parent
4735
	 */
4736
	add: function (parent) {
4737
		var wrapper = this,
4738
			renderer = wrapper.renderer,
4739
			element = wrapper.element,
4740
			box = renderer.box,
4741
			inverted = parent && parent.inverted,
4742
 
4743
			// get the parent node
4744
			parentNode = parent ?
4745
				parent.element || parent :
4746
				box;
4747
 
4748
 
4749
		// if the parent group is inverted, apply inversion on all children
4750
		if (inverted) { // only on groups
4751
			renderer.invertChild(element, parentNode);
4752
		}
4753
 
4754
		// append it
4755
		parentNode.appendChild(element);
4756
 
4757
		// align text after adding to be able to read offset
4758
		wrapper.added = true;
4759
		if (wrapper.alignOnAdd && !wrapper.deferUpdateTransform) {
4760
			wrapper.updateTransform();
4761
		}
4762
 
4763
		// fire an event for internal hooks
4764
		if (wrapper.onAdd) {
4765
			wrapper.onAdd();
4766
		}
4767
 
4768
		return wrapper;
4769
	},
4770
 
4771
	/**
4772
	 * VML always uses htmlUpdateTransform
4773
	 */
4774
	updateTransform: SVGElement.prototype.htmlUpdateTransform,
4775
 
4776
	/**
4777
	 * Set the rotation of a span with oldIE's filter
4778
	 */
4779
	setSpanRotation: function () {
4780
		// Adjust for alignment and rotation. Rotation of useHTML content is not yet implemented
4781
		// but it can probably be implemented for Firefox 3.5+ on user request. FF3.5+
4782
		// has support for CSS3 transform. The getBBox method also needs to be updated
4783
		// to compensate for the rotation, like it currently does for SVG.
4784
		// Test case: http://jsfiddle.net/highcharts/Ybt44/
4785
 
4786
		var rotation = this.rotation,
4787
			costheta = mathCos(rotation * deg2rad),
4788
			sintheta = mathSin(rotation * deg2rad);
4789
 
4790
		css(this.element, {
4791
			filter: rotation ? ['progid:DXImageTransform.Microsoft.Matrix(M11=', costheta,
4792
				', M12=', -sintheta, ', M21=', sintheta, ', M22=', costheta,
4793
				', sizingMethod=\'auto expand\')'].join('') : NONE
4794
		});
4795
	},
4796
 
4797
	/**
4798
	 * Get the positioning correction for the span after rotating.
4799
	 */
4800
	getSpanCorrection: function (width, baseline, alignCorrection, rotation, align) {
4801
 
4802
		var costheta = rotation ? mathCos(rotation * deg2rad) : 1,
4803
			sintheta = rotation ? mathSin(rotation * deg2rad) : 0,
4804
			height = pick(this.elemHeight, this.element.offsetHeight),
4805
			quad,
4806
			nonLeft = align && align !== 'left';
4807
 
4808
		// correct x and y
4809
		this.xCorr = costheta < 0 && -width;
4810
		this.yCorr = sintheta < 0 && -height;
4811
 
4812
		// correct for baseline and corners spilling out after rotation
4813
		quad = costheta * sintheta < 0;
4814
		this.xCorr += sintheta * baseline * (quad ? 1 - alignCorrection : alignCorrection);
4815
		this.yCorr -= costheta * baseline * (rotation ? (quad ? alignCorrection : 1 - alignCorrection) : 1);
4816
		// correct for the length/height of the text
4817
		if (nonLeft) {
4818
			this.xCorr -= width * alignCorrection * (costheta < 0 ? -1 : 1);
4819
			if (rotation) {
4820
				this.yCorr -= height * alignCorrection * (sintheta < 0 ? -1 : 1);
4821
			}
4822
			css(this.element, {
4823
				textAlign: align
4824
			});
4825
		}
4826
	},
4827
 
4828
	/**
4829
	 * Converts a subset of an SVG path definition to its VML counterpart. Takes an array
4830
	 * as the parameter and returns a string.
4831
	 */
4832
	pathToVML: function (value) {
4833
		// convert paths
4834
		var i = value.length,
4835
			path = [];
4836
 
4837
		while (i--) {
4838
 
4839
			// Multiply by 10 to allow subpixel precision.
4840
			// Substracting half a pixel seems to make the coordinates
4841
			// align with SVG, but this hasn't been tested thoroughly
4842
			if (isNumber(value[i])) {
4843
				path[i] = mathRound(value[i] * 10) - 5;
4844
			} else if (value[i] === 'Z') { // close the path
4845
				path[i] = 'x';
4846
			} else {
4847
				path[i] = value[i];
4848
 
4849
				// When the start X and end X coordinates of an arc are too close,
4850
				// they are rounded to the same value above. In this case, substract or
4851
				// add 1 from the end X and Y positions. #186, #760, #1371, #1410.
4852
				if (value.isArc && (value[i] === 'wa' || value[i] === 'at')) {
4853
					// Start and end X
4854
					if (path[i + 5] === path[i + 7]) {
4855
						path[i + 7] += value[i + 7] > value[i + 5] ? 1 : -1;
4856
					}
4857
					// Start and end Y
4858
					if (path[i + 6] === path[i + 8]) {
4859
						path[i + 8] += value[i + 8] > value[i + 6] ? 1 : -1;
4860
					}
4861
				}
4862
			}
4863
		}
4864
 
4865
 
4866
		// Loop up again to handle path shortcuts (#2132)
4867
		/*while (i++ < path.length) {
4868
			if (path[i] === 'H') { // horizontal line to
4869
				path[i] = 'L';
4870
				path.splice(i + 2, 0, path[i - 1]);
4871
			} else if (path[i] === 'V') { // vertical line to
4872
				path[i] = 'L';
4873
				path.splice(i + 1, 0, path[i - 2]);
4874
			}
4875
		}*/
4876
		return path.join(' ') || 'x';
4877
	},
4878
 
4879
	/**
4880
	 * Set the element's clipping to a predefined rectangle
4881
	 *
4882
	 * @param {String} id The id of the clip rectangle
4883
	 */
4884
	clip: function (clipRect) {
4885
		var wrapper = this,
4886
			clipMembers,
4887
			cssRet;
4888
 
4889
		if (clipRect) {
4890
			clipMembers = clipRect.members;
4891
			erase(clipMembers, wrapper); // Ensure unique list of elements (#1258)
4892
			clipMembers.push(wrapper);
4893
			wrapper.destroyClip = function () {
4894
				erase(clipMembers, wrapper);
4895
			};
4896
			cssRet = clipRect.getCSS(wrapper);
4897
 
4898
		} else {
4899
			if (wrapper.destroyClip) {
4900
				wrapper.destroyClip();
4901
			}
4902
			cssRet = { clip: docMode8 ? 'inherit' : 'rect(auto)' }; // #1214
4903
		}
4904
 
4905
		return wrapper.css(cssRet);
4906
 
4907
	},
4908
 
4909
	/**
4910
	 * Set styles for the element
4911
	 * @param {Object} styles
4912
	 */
4913
	css: SVGElement.prototype.htmlCss,
4914
 
4915
	/**
4916
	 * Removes a child either by removeChild or move to garbageBin.
4917
	 * Issue 490; in VML removeChild results in Orphaned nodes according to sIEve, discardElement does not.
4918
	 */
4919
	safeRemoveChild: function (element) {
4920
		// discardElement will detach the node from its parent before attaching it
4921
		// to the garbage bin. Therefore it is important that the node is attached and have parent.
4922
		if (element.parentNode) {
4923
			discardElement(element);
4924
		}
4925
	},
4926
 
4927
	/**
4928
	 * Extend element.destroy by removing it from the clip members array
4929
	 */
4930
	destroy: function () {
4931
		if (this.destroyClip) {
4932
			this.destroyClip();
4933
		}
4934
 
4935
		return SVGElement.prototype.destroy.apply(this);
4936
	},
4937
 
4938
	/**
4939
	 * Add an event listener. VML override for normalizing event parameters.
4940
	 * @param {String} eventType
4941
	 * @param {Function} handler
4942
	 */
4943
	on: function (eventType, handler) {
4944
		// simplest possible event model for internal use
4945
		this.element['on' + eventType] = function () {
4946
			var evt = win.event;
4947
			evt.target = evt.srcElement;
4948
			handler(evt);
4949
		};
4950
		return this;
4951
	},
4952
 
4953
	/**
4954
	 * In stacked columns, cut off the shadows so that they don't overlap
4955
	 */
4956
	cutOffPath: function (path, length) {
4957
 
4958
		var len;
4959
 
4960
		path = path.split(/[ ,]/);
4961
		len = path.length;
4962
 
4963
		if (len === 9 || len === 11) {
4964
			path[len - 4] = path[len - 2] = pInt(path[len - 2]) - 10 * length;
4965
		}
4966
		return path.join(' ');
4967
	},
4968
 
4969
	/**
4970
	 * Apply a drop shadow by copying elements and giving them different strokes
4971
	 * @param {Boolean|Object} shadowOptions
4972
	 */
4973
	shadow: function (shadowOptions, group, cutOff) {
4974
		var shadows = [],
4975
			i,
4976
			element = this.element,
4977
			renderer = this.renderer,
4978
			shadow,
4979
			elemStyle = element.style,
4980
			markup,
4981
			path = element.path,
4982
			strokeWidth,
4983
			modifiedPath,
4984
			shadowWidth,
4985
			shadowElementOpacity;
4986
 
4987
		// some times empty paths are not strings
4988
		if (path && typeof path.value !== 'string') {
4989
			path = 'x';
4990
		}
4991
		modifiedPath = path;
4992
 
4993
		if (shadowOptions) {
4994
			shadowWidth = pick(shadowOptions.width, 3);
4995
			shadowElementOpacity = (shadowOptions.opacity || 0.15) / shadowWidth;
4996
			for (i = 1; i <= 3; i++) {
4997
 
4998
				strokeWidth = (shadowWidth * 2) + 1 - (2 * i);
4999
 
5000
				// Cut off shadows for stacked column items
5001
				if (cutOff) {
5002
					modifiedPath = this.cutOffPath(path.value, strokeWidth + 0.5);
5003
				}
5004
 
5005
				markup = ['<shape isShadow="true" strokeweight="', strokeWidth,
5006
					'" filled="false" path="', modifiedPath,
5007
					'" coordsize="10 10" style="', element.style.cssText, '" />'];
5008
 
5009
				shadow = createElement(renderer.prepVML(markup),
5010
					null, {
5011
						left: pInt(elemStyle.left) + pick(shadowOptions.offsetX, 1),
5012
						top: pInt(elemStyle.top) + pick(shadowOptions.offsetY, 1)
5013
					}
5014
				);
5015
				if (cutOff) {
5016
					shadow.cutOff = strokeWidth + 1;
5017
				}
5018
 
5019
				// apply the opacity
5020
				markup = ['<stroke color="', shadowOptions.color || 'black', '" opacity="', shadowElementOpacity * i, '"/>'];
5021
				createElement(renderer.prepVML(markup), null, null, shadow);
5022
 
5023
 
5024
				// insert it
5025
				if (group) {
5026
					group.element.appendChild(shadow);
5027
				} else {
5028
					element.parentNode.insertBefore(shadow, element);
5029
				}
5030
 
5031
				// record it
5032
				shadows.push(shadow);
5033
 
5034
			}
5035
 
5036
			this.shadows = shadows;
5037
		}
5038
		return this;
5039
	},
5040
	updateShadows: noop, // Used in SVG only
5041
 
5042
	setAttr: function (key, value) {
5043
		if (docMode8) { // IE8 setAttribute bug
5044
			this.element[key] = value;
5045
		} else {
5046
			this.element.setAttribute(key, value);
5047
		}
5048
	},
5049
	classSetter: function (value) {
5050
		// IE8 Standards mode has problems retrieving the className unless set like this
5051
		this.element.className = value;
5052
	},
5053
	dashstyleSetter: function (value, key, element) {
5054
		var strokeElem = element.getElementsByTagName('stroke')[0] ||
5055
			createElement(this.renderer.prepVML(['<stroke/>']), null, null, element);
5056
		strokeElem[key] = value || 'solid';
5057
		this[key] = value; /* because changing stroke-width will change the dash length
5058
			and cause an epileptic effect */
5059
	},
5060
	dSetter: function (value, key, element) {
5061
		var i,
5062
			shadows = this.shadows;
5063
		value = value || [];
5064
		this.d = value.join && value.join(' '); // used in getter for animation
5065
 
5066
		element.path = value = this.pathToVML(value);
5067
 
5068
		// update shadows
5069
		if (shadows) {
5070
			i = shadows.length;
5071
			while (i--) {
5072
				shadows[i].path = shadows[i].cutOff ? this.cutOffPath(value, shadows[i].cutOff) : value;
5073
			}
5074
		}
5075
		this.setAttr(key, value);
5076
	},
5077
	fillSetter: function (value, key, element) {
5078
		var nodeName = element.nodeName;
5079
		if (nodeName === 'SPAN') { // text color
5080
			element.style.color = value;
5081
		} else if (nodeName !== 'IMG') { // #1336
5082
			element.filled = value !== NONE;
5083
			this.setAttr('fillcolor', this.renderer.color(value, element, key, this));
5084
		}
5085
	},
5086
	opacitySetter: noop, // Don't bother - animation is too slow and filters introduce artifacts
5087
	rotationSetter: function (value, key, element) {
5088
		var style = element.style;
5089
		this[key] = style[key] = value; // style is for #1873
5090
 
5091
		// Correction for the 1x1 size of the shape container. Used in gauge needles.
5092
		style.left = -mathRound(mathSin(value * deg2rad) + 1) + PX;
5093
		style.top = mathRound(mathCos(value * deg2rad)) + PX;
5094
	},
5095
	strokeSetter: function (value, key, element) {
5096
		this.setAttr('strokecolor', this.renderer.color(value, element, key));
5097
	},
5098
	'stroke-widthSetter': function (value, key, element) {
5099
		element.stroked = !!value; // VML "stroked" attribute
5100
		this[key] = value; // used in getter, issue #113
5101
		if (isNumber(value)) {
5102
			value += PX;
5103
		}
5104
		this.setAttr('strokeweight', value);
5105
	},
5106
	titleSetter: function (value, key) {
5107
		this.setAttr(key, value);
5108
	},
5109
	visibilitySetter: function (value, key, element) {
5110
 
5111
		// Handle inherited visibility
5112
		if (value === 'inherit') {
5113
			value = VISIBLE;
5114
		}
5115
 
5116
		// Let the shadow follow the main element
5117
		if (this.shadows) {
5118
			each(this.shadows, function (shadow) {
5119
				shadow.style[key] = value;
5120
			});
5121
		}
5122
 
5123
		// Instead of toggling the visibility CSS property, move the div out of the viewport.
5124
		// This works around #61 and #586
5125
		if (element.nodeName === 'DIV') {
5126
			value = value === HIDDEN ? '-999em' : 0;
5127
 
5128
			// In order to redraw, IE7 needs the div to be visible when tucked away
5129
			// outside the viewport. So the visibility is actually opposite of
5130
			// the expected value. This applies to the tooltip only.
5131
			if (!docMode8) {
5132
				element.style[key] = value ? VISIBLE : HIDDEN;
5133
			}
5134
			key = 'top';
5135
		}
5136
		element.style[key] = value;
5137
	},
5138
	xSetter: function (value, key, element) {
5139
		this[key] = value; // used in getter
5140
 
5141
		if (key === 'x') {
5142
			key = 'left';
5143
		} else if (key === 'y') {
5144
			key = 'top';
5145
		}/* else {
5146
			value = mathMax(0, value); // don't set width or height below zero (#311)
5147
		}*/
5148
 
5149
		// clipping rectangle special
5150
		if (this.updateClipping) {
5151
			this[key] = value; // the key is now 'left' or 'top' for 'x' and 'y'
5152
			this.updateClipping();
5153
		} else {
5154
			// normal
5155
			element.style[key] = value;
5156
		}
5157
	},
5158
	zIndexSetter: function (value, key, element) {
5159
		element.style[key] = value;
5160
	}
5161
};
5162
Highcharts.VMLElement = VMLElement = extendClass(SVGElement, VMLElement);
5163
 
5164
// Some shared setters
5165
VMLElement.prototype.ySetter =
5166
	VMLElement.prototype.widthSetter =
5167
	VMLElement.prototype.heightSetter =
5168
	VMLElement.prototype.xSetter;
5169
 
5170
 
5171
/**
5172
 * The VML renderer
5173
 */
5174
var VMLRendererExtension = { // inherit SVGRenderer
5175
 
5176
	Element: VMLElement,
5177
	isIE8: userAgent.indexOf('MSIE 8.0') > -1,
5178
 
5179
 
5180
	/**
5181
	 * Initialize the VMLRenderer
5182
	 * @param {Object} container
5183
	 * @param {Number} width
5184
	 * @param {Number} height
5185
	 */
5186
	init: function (container, width, height, style) {
5187
		var renderer = this,
5188
			boxWrapper,
5189
			box,
5190
			css;
5191
 
5192
		renderer.alignedObjects = [];
5193
 
5194
		boxWrapper = renderer.createElement(DIV)
5195
			.css(extend(this.getStyle(style), { position: RELATIVE}));
5196
		box = boxWrapper.element;
5197
		container.appendChild(boxWrapper.element);
5198
 
5199
 
5200
		// generate the containing box
5201
		renderer.isVML = true;
5202
		renderer.box = box;
5203
		renderer.boxWrapper = boxWrapper;
5204
		renderer.cache = {};
5205
 
5206
 
5207
		renderer.setSize(width, height, false);
5208
 
5209
		// The only way to make IE6 and IE7 print is to use a global namespace. However,
5210
		// with IE8 the only way to make the dynamic shapes visible in screen and print mode
5211
		// seems to be to add the xmlns attribute and the behaviour style inline.
5212
		if (!doc.namespaces.hcv) {
5213
 
5214
			doc.namespaces.add('hcv', 'urn:schemas-microsoft-com:vml');
5215
 
5216
			// Setup default CSS (#2153, #2368, #2384)
5217
			css = 'hcv\\:fill, hcv\\:path, hcv\\:shape, hcv\\:stroke' +
5218
				'{ behavior:url(#default#VML); display: inline-block; } ';
5219
			try {
5220
				doc.createStyleSheet().cssText = css;
5221
			} catch (e) {
5222
				doc.styleSheets[0].cssText += css;
5223
			}
5224
 
5225
		}
5226
	},
5227
 
5228
 
5229
	/**
5230
	 * Detect whether the renderer is hidden. This happens when one of the parent elements
5231
	 * has display: none
5232
	 */
5233
	isHidden: function () {
5234
		return !this.box.offsetWidth;
5235
	},
5236
 
5237
	/**
5238
	 * Define a clipping rectangle. In VML it is accomplished by storing the values
5239
	 * for setting the CSS style to all associated members.
5240
	 *
5241
	 * @param {Number} x
5242
	 * @param {Number} y
5243
	 * @param {Number} width
5244
	 * @param {Number} height
5245
	 */
5246
	clipRect: function (x, y, width, height) {
5247
 
5248
		// create a dummy element
5249
		var clipRect = this.createElement(),
5250
			isObj = isObject(x);
5251
 
5252
		// mimic a rectangle with its style object for automatic updating in attr
5253
		return extend(clipRect, {
5254
			members: [],
5255
			count: 0,
5256
			left: (isObj ? x.x : x) + 1,
5257
			top: (isObj ? x.y : y) + 1,
5258
			width: (isObj ? x.width : width) - 1,
5259
			height: (isObj ? x.height : height) - 1,
5260
			getCSS: function (wrapper) {
5261
				var element = wrapper.element,
5262
					nodeName = element.nodeName,
5263
					isShape = nodeName === 'shape',
5264
					inverted = wrapper.inverted,
5265
					rect = this,
5266
					top = rect.top - (isShape ? element.offsetTop : 0),
5267
					left = rect.left,
5268
					right = left + rect.width,
5269
					bottom = top + rect.height,
5270
					ret = {
5271
						clip: 'rect(' +
5272
							mathRound(inverted ? left : top) + 'px,' +
5273
							mathRound(inverted ? bottom : right) + 'px,' +
5274
							mathRound(inverted ? right : bottom) + 'px,' +
5275
							mathRound(inverted ? top : left) + 'px)'
5276
					};
5277
 
5278
				// issue 74 workaround
5279
				if (!inverted && docMode8 && nodeName === 'DIV') {
5280
					extend(ret, {
5281
						width: right + PX,
5282
						height: bottom + PX
5283
					});
5284
				}
5285
				return ret;
5286
			},
5287
 
5288
			// used in attr and animation to update the clipping of all members
5289
			updateClipping: function () {
5290
				each(clipRect.members, function (member) {
5291
					if (member.element) { // Deleted series, like in stock/members/series-remove demo. Should be removed from members, but this will do.
5292
						member.css(clipRect.getCSS(member));
5293
					}
5294
				});
5295
			}
5296
		});
5297
 
5298
	},
5299
 
5300
 
5301
	/**
5302
	 * Take a color and return it if it's a string, make it a gradient if it's a
5303
	 * gradient configuration object, and apply opacity.
5304
	 *
5305
	 * @param {Object} color The color or config object
5306
	 */
5307
	color: function (color, elem, prop, wrapper) {
5308
		var renderer = this,
5309
			colorObject,
5310
			regexRgba = /^rgba/,
5311
			markup,
5312
			fillType,
5313
			ret = NONE;
5314
 
5315
		// Check for linear or radial gradient
5316
		if (color && color.linearGradient) {
5317
			fillType = 'gradient';
5318
		} else if (color && color.radialGradient) {
5319
			fillType = 'pattern';
5320
		}
5321
 
5322
 
5323
		if (fillType) {
5324
 
5325
			var stopColor,
5326
				stopOpacity,
5327
				gradient = color.linearGradient || color.radialGradient,
5328
				x1,
5329
				y1,
5330
				x2,
5331
				y2,
5332
				opacity1,
5333
				opacity2,
5334
				color1,
5335
				color2,
5336
				fillAttr = '',
5337
				stops = color.stops,
5338
				firstStop,
5339
				lastStop,
5340
				colors = [],
5341
				addFillNode = function () {
5342
					// Add the fill subnode. When colors attribute is used, the meanings of opacity and o:opacity2
5343
					// are reversed.
5344
					markup = ['<fill colors="' + colors.join(',') + '" opacity="', opacity2, '" o:opacity2="', opacity1,
5345
						'" type="', fillType, '" ', fillAttr, 'focus="100%" method="any" />'];
5346
					createElement(renderer.prepVML(markup), null, null, elem);
5347
				};
5348
 
5349
			// Extend from 0 to 1
5350
			firstStop = stops[0];
5351
			lastStop = stops[stops.length - 1];
5352
			if (firstStop[0] > 0) {
5353
				stops.unshift([
5354
					0,
5355
					firstStop[1]
5356
				]);
5357
			}
5358
			if (lastStop[0] < 1) {
5359
				stops.push([
5360
					1,
5361
					lastStop[1]
5362
				]);
5363
			}
5364
 
5365
			// Compute the stops
5366
			each(stops, function (stop, i) {
5367
				if (regexRgba.test(stop[1])) {
5368
					colorObject = Color(stop[1]);
5369
					stopColor = colorObject.get('rgb');
5370
					stopOpacity = colorObject.get('a');
5371
				} else {
5372
					stopColor = stop[1];
5373
					stopOpacity = 1;
5374
				}
5375
 
5376
				// Build the color attribute
5377
				colors.push((stop[0] * 100) + '% ' + stopColor);
5378
 
5379
				// Only start and end opacities are allowed, so we use the first and the last
5380
				if (!i) {
5381
					opacity1 = stopOpacity;
5382
					color2 = stopColor;
5383
				} else {
5384
					opacity2 = stopOpacity;
5385
					color1 = stopColor;
5386
				}
5387
			});
5388
 
5389
			// Apply the gradient to fills only.
5390
			if (prop === 'fill') {
5391
 
5392
				// Handle linear gradient angle
5393
				if (fillType === 'gradient') {
5394
					x1 = gradient.x1 || gradient[0] || 0;
5395
					y1 = gradient.y1 || gradient[1] || 0;
5396
					x2 = gradient.x2 || gradient[2] || 0;
5397
					y2 = gradient.y2 || gradient[3] || 0;
5398
					fillAttr = 'angle="' + (90  - math.atan(
5399
						(y2 - y1) / // y vector
5400
						(x2 - x1) // x vector
5401
						) * 180 / mathPI) + '"';
5402
 
5403
					addFillNode();
5404
 
5405
				// Radial (circular) gradient
5406
				} else {
5407
 
5408
					var r = gradient.r,
5409
						sizex = r * 2,
5410
						sizey = r * 2,
5411
						cx = gradient.cx,
5412
						cy = gradient.cy,
5413
						radialReference = elem.radialReference,
5414
						bBox,
5415
						applyRadialGradient = function () {
5416
							if (radialReference) {
5417
								bBox = wrapper.getBBox();
5418
								cx += (radialReference[0] - bBox.x) / bBox.width - 0.5;
5419
								cy += (radialReference[1] - bBox.y) / bBox.height - 0.5;
5420
								sizex *= radialReference[2] / bBox.width;
5421
								sizey *= radialReference[2] / bBox.height;
5422
							}
5423
							fillAttr = 'src="' + defaultOptions.global.VMLRadialGradientURL + '" ' +
5424
								'size="' + sizex + ',' + sizey + '" ' +
5425
								'origin="0.5,0.5" ' +
5426
								'position="' + cx + ',' + cy + '" ' +
5427
								'color2="' + color2 + '" ';
5428
 
5429
							addFillNode();
5430
						};
5431
 
5432
					// Apply radial gradient
5433
					if (wrapper.added) {
5434
						applyRadialGradient();
5435
					} else {
5436
						// We need to know the bounding box to get the size and position right
5437
						wrapper.onAdd = applyRadialGradient;
5438
					}
5439
 
5440
					// The fill element's color attribute is broken in IE8 standards mode, so we
5441
					// need to set the parent shape's fillcolor attribute instead.
5442
					ret = color1;
5443
				}
5444
 
5445
			// Gradients are not supported for VML stroke, return the first color. #722.
5446
			} else {
5447
				ret = stopColor;
5448
			}
5449
 
5450
		// if the color is an rgba color, split it and add a fill node
5451
		// to hold the opacity component
5452
		} else if (regexRgba.test(color) && elem.tagName !== 'IMG') {
5453
 
5454
			colorObject = Color(color);
5455
 
5456
			markup = ['<', prop, ' opacity="', colorObject.get('a'), '"/>'];
5457
			createElement(this.prepVML(markup), null, null, elem);
5458
 
5459
			ret = colorObject.get('rgb');
5460
 
5461
 
5462
		} else {
5463
			var propNodes = elem.getElementsByTagName(prop); // 'stroke' or 'fill' node
5464
			if (propNodes.length) {
5465
				propNodes[0].opacity = 1;
5466
				propNodes[0].type = 'solid';
5467
			}
5468
			ret = color;
5469
		}
5470
 
5471
		return ret;
5472
	},
5473
 
5474
	/**
5475
	 * Take a VML string and prepare it for either IE8 or IE6/IE7.
5476
	 * @param {Array} markup A string array of the VML markup to prepare
5477
	 */
5478
	prepVML: function (markup) {
5479
		var vmlStyle = 'display:inline-block;behavior:url(#default#VML);',
5480
			isIE8 = this.isIE8;
5481
 
5482
		markup = markup.join('');
5483
 
5484
		if (isIE8) { // add xmlns and style inline
5485
			markup = markup.replace('/>', ' xmlns="urn:schemas-microsoft-com:vml" />');
5486
			if (markup.indexOf('style="') === -1) {
5487
				markup = markup.replace('/>', ' style="' + vmlStyle + '" />');
5488
			} else {
5489
				markup = markup.replace('style="', 'style="' + vmlStyle);
5490
			}
5491
 
5492
		} else { // add namespace
5493
			markup = markup.replace('<', '<hcv:');
5494
		}
5495
 
5496
		return markup;
5497
	},
5498
 
5499
	/**
5500
	 * Create rotated and aligned text
5501
	 * @param {String} str
5502
	 * @param {Number} x
5503
	 * @param {Number} y
5504
	 */
5505
	text: SVGRenderer.prototype.html,
5506
 
5507
	/**
5508
	 * Create and return a path element
5509
	 * @param {Array} path
5510
	 */
5511
	path: function (path) {
5512
		var attr = {
5513
			// subpixel precision down to 0.1 (width and height = 1px)
5514
			coordsize: '10 10'
5515
		};
5516
		if (isArray(path)) {
5517
			attr.d = path;
5518
		} else if (isObject(path)) { // attributes
5519
			extend(attr, path);
5520
		}
5521
		// create the shape
5522
		return this.createElement('shape').attr(attr);
5523
	},
5524
 
5525
	/**
5526
	 * Create and return a circle element. In VML circles are implemented as
5527
	 * shapes, which is faster than v:oval
5528
	 * @param {Number} x
5529
	 * @param {Number} y
5530
	 * @param {Number} r
5531
	 */
5532
	circle: function (x, y, r) {
5533
		var circle = this.symbol('circle');
5534
		if (isObject(x)) {
5535
			r = x.r;
5536
			y = x.y;
5537
			x = x.x;
5538
		}
5539
		circle.isCircle = true; // Causes x and y to mean center (#1682)
5540
		circle.r = r;
5541
		return circle.attr({ x: x, y: y });
5542
	},
5543
 
5544
	/**
5545
	 * Create a group using an outer div and an inner v:group to allow rotating
5546
	 * and flipping. A simple v:group would have problems with positioning
5547
	 * child HTML elements and CSS clip.
5548
	 *
5549
	 * @param {String} name The name of the group
5550
	 */
5551
	g: function (name) {
5552
		var wrapper,
5553
			attribs;
5554
 
5555
		// set the class name
5556
		if (name) {
5557
			attribs = { 'className': PREFIX + name, 'class': PREFIX + name };
5558
		}
5559
 
5560
		// the div to hold HTML and clipping
5561
		wrapper = this.createElement(DIV).attr(attribs);
5562
 
5563
		return wrapper;
5564
	},
5565
 
5566
	/**
5567
	 * VML override to create a regular HTML image
5568
	 * @param {String} src
5569
	 * @param {Number} x
5570
	 * @param {Number} y
5571
	 * @param {Number} width
5572
	 * @param {Number} height
5573
	 */
5574
	image: function (src, x, y, width, height) {
5575
		var obj = this.createElement('img')
5576
			.attr({ src: src });
5577
 
5578
		if (arguments.length > 1) {
5579
			obj.attr({
5580
				x: x,
5581
				y: y,
5582
				width: width,
5583
				height: height
5584
			});
5585
		}
5586
		return obj;
5587
	},
5588
 
5589
	/**
5590
	 * For rectangles, VML uses a shape for rect to overcome bugs and rotation problems
5591
	 */
5592
	createElement: function (nodeName) {
5593
		return nodeName === 'rect' ? this.symbol(nodeName) : SVGRenderer.prototype.createElement.call(this, nodeName);
5594
	},
5595
 
5596
	/**
5597
	 * In the VML renderer, each child of an inverted div (group) is inverted
5598
	 * @param {Object} element
5599
	 * @param {Object} parentNode
5600
	 */
5601
	invertChild: function (element, parentNode) {
5602
		var ren = this,
5603
			parentStyle = parentNode.style,
5604
			imgStyle = element.tagName === 'IMG' && element.style; // #1111
5605
 
5606
		css(element, {
5607
			flip: 'x',
5608
			left: pInt(parentStyle.width) - (imgStyle ? pInt(imgStyle.top) : 1),
5609
			top: pInt(parentStyle.height) - (imgStyle ? pInt(imgStyle.left) : 1),
5610
			rotation: -90
5611
		});
5612
 
5613
		// Recursively invert child elements, needed for nested composite shapes like box plots and error bars. #1680, #1806.
5614
		each(element.childNodes, function (child) {
5615
			ren.invertChild(child, element);
5616
		});
5617
	},
5618
 
5619
	/**
5620
	 * Symbol definitions that override the parent SVG renderer's symbols
5621
	 *
5622
	 */
5623
	symbols: {
5624
		// VML specific arc function
5625
		arc: function (x, y, w, h, options) {
5626
			var start = options.start,
5627
				end = options.end,
5628
				radius = options.r || w || h,
5629
				innerRadius = options.innerR,
5630
				cosStart = mathCos(start),
5631
				sinStart = mathSin(start),
5632
				cosEnd = mathCos(end),
5633
				sinEnd = mathSin(end),
5634
				ret;
5635
 
5636
			if (end - start === 0) { // no angle, don't show it.
5637
				return ['x'];
5638
			}
5639
 
5640
			ret = [
5641
				'wa', // clockwise arc to
5642
				x - radius, // left
5643
				y - radius, // top
5644
				x + radius, // right
5645
				y + radius, // bottom
5646
				x + radius * cosStart, // start x
5647
				y + radius * sinStart, // start y
5648
				x + radius * cosEnd, // end x
5649
				y + radius * sinEnd  // end y
5650
			];
5651
 
5652
			if (options.open && !innerRadius) {
5653
				ret.push(
5654
					'e',
5655
					M,
5656
					x,// - innerRadius,
5657
					y// - innerRadius
5658
				);
5659
			}
5660
 
5661
			ret.push(
5662
				'at', // anti clockwise arc to
5663
				x - innerRadius, // left
5664
				y - innerRadius, // top
5665
				x + innerRadius, // right
5666
				y + innerRadius, // bottom
5667
				x + innerRadius * cosEnd, // start x
5668
				y + innerRadius * sinEnd, // start y
5669
				x + innerRadius * cosStart, // end x
5670
				y + innerRadius * sinStart, // end y
5671
				'x', // finish path
5672
				'e' // close
5673
			);
5674
 
5675
			ret.isArc = true;
5676
			return ret;
5677
 
5678
		},
5679
		// Add circle symbol path. This performs significantly faster than v:oval.
5680
		circle: function (x, y, w, h, wrapper) {
5681
 
5682
			if (wrapper) {
5683
				w = h = 2 * wrapper.r;
5684
			}
5685
 
5686
			// Center correction, #1682
5687
			if (wrapper && wrapper.isCircle) {
5688
				x -= w / 2;
5689
				y -= h / 2;
5690
			}
5691
 
5692
			// Return the path
5693
			return [
5694
				'wa', // clockwisearcto
5695
				x, // left
5696
				y, // top
5697
				x + w, // right
5698
				y + h, // bottom
5699
				x + w, // start x
5700
				y + h / 2,     // start y
5701
				x + w, // end x
5702
				y + h / 2,     // end y
5703
				//'x', // finish path
5704
				'e' // close
5705
			];
5706
		},
5707
		/**
5708
		 * Add rectangle symbol path which eases rotation and omits arcsize problems
5709
		 * compared to the built-in VML roundrect shape. When borders are not rounded,
5710
		 * use the simpler square path, else use the callout path without the arrow.
5711
		 */
5712
		rect: function (x, y, w, h, options) {
5713
			return SVGRenderer.prototype.symbols[
5714
				!defined(options) || !options.r ? 'square' : 'callout'
5715
			].call(0, x, y, w, h, options);
5716
		}
5717
	}
5718
};
5719
Highcharts.VMLRenderer = VMLRenderer = function () {
5720
	this.init.apply(this, arguments);
5721
};
5722
VMLRenderer.prototype = merge(SVGRenderer.prototype, VMLRendererExtension);
5723
 
5724
	// general renderer
5725
	Renderer = VMLRenderer;
5726
}
5727
 
5728
// This method is used with exporting in old IE, when emulating SVG (see #2314)
5729
SVGRenderer.prototype.measureSpanWidth = function (text, styles) {
5730
	var measuringSpan = doc.createElement('span'),
5731
		offsetWidth,
5732
	textNode = doc.createTextNode(text);
5733
 
5734
	measuringSpan.appendChild(textNode);
5735
	css(measuringSpan, styles);
5736
	this.box.appendChild(measuringSpan);
5737
	offsetWidth = measuringSpan.offsetWidth;
5738
	discardElement(measuringSpan); // #2463
5739
	return offsetWidth;
5740
};
5741
 
5742
 
5743
/* ****************************************************************************
5744
 *                                                                            *
5745
 * END OF INTERNET EXPLORER <= 8 SPECIFIC CODE                                *
5746
 *                                                                            *
5747
 *****************************************************************************/
5748
/* ****************************************************************************
5749
 *                                                                            *
5750
 * START OF ANDROID < 3 SPECIFIC CODE. THIS CAN BE REMOVED IF YOU'RE NOT      *
5751
 * TARGETING THAT SYSTEM.                                                     *
5752
 *                                                                            *
5753
 *****************************************************************************/
5754
var CanVGRenderer,
5755
	CanVGController;
5756
 
5757
if (useCanVG) {
5758
	/**
5759
	 * The CanVGRenderer is empty from start to keep the source footprint small.
5760
	 * When requested, the CanVGController downloads the rest of the source packaged
5761
	 * together with the canvg library.
5762
	 */
5763
	Highcharts.CanVGRenderer = CanVGRenderer = function () {
5764
		// Override the global SVG namespace to fake SVG/HTML that accepts CSS
5765
		SVG_NS = 'http://www.w3.org/1999/xhtml';
5766
	};
5767
 
5768
	/**
5769
	 * Start with an empty symbols object. This is needed when exporting is used (exporting.src.js will add a few symbols), but
5770
	 * the implementation from SvgRenderer will not be merged in until first render.
5771
	 */
5772
	CanVGRenderer.prototype.symbols = {};
5773
 
5774
	/**
5775
	 * Handles on demand download of canvg rendering support.
5776
	 */
5777
	CanVGController = (function () {
5778
		// List of renderering calls
5779
		var deferredRenderCalls = [];
5780
 
5781
		/**
5782
		 * When downloaded, we are ready to draw deferred charts.
5783
		 */
5784
		function drawDeferred() {
5785
			var callLength = deferredRenderCalls.length,
5786
				callIndex;
5787
 
5788
			// Draw all pending render calls
5789
			for (callIndex = 0; callIndex < callLength; callIndex++) {
5790
				deferredRenderCalls[callIndex]();
5791
			}
5792
			// Clear the list
5793
			deferredRenderCalls = [];
5794
		}
5795
 
5796
		return {
5797
			push: function (func, scriptLocation) {
5798
				// Only get the script once
5799
				if (deferredRenderCalls.length === 0) {
5800
					getScript(scriptLocation, drawDeferred);
5801
				}
5802
				// Register render call
5803
				deferredRenderCalls.push(func);
5804
			}
5805
		};
5806
	}());
5807
 
5808
	Renderer = CanVGRenderer;
5809
} // end CanVGRenderer
5810
 
5811
/* ****************************************************************************
5812
 *                                                                            *
5813
 * END OF ANDROID < 3 SPECIFIC CODE                                           *
5814
 *                                                                            *
5815
 *****************************************************************************/
5816
 
5817
/**
5818
 * The Tick class
5819
 */
5820
function Tick(axis, pos, type, noLabel) {
5821
	this.axis = axis;
5822
	this.pos = pos;
5823
	this.type = type || '';
5824
	this.isNew = true;
5825
 
5826
	if (!type && !noLabel) {
5827
		this.addLabel();
5828
	}
5829
}
5830
 
5831
Tick.prototype = {
5832
	/**
5833
	 * Write the tick label
5834
	 */
5835
	addLabel: function () {
5836
		var tick = this,
5837
			axis = tick.axis,
5838
			options = axis.options,
5839
			chart = axis.chart,
5840
			categories = axis.categories,
5841
			names = axis.names,
5842
			pos = tick.pos,
5843
			labelOptions = options.labels,
5844
			str,
5845
			tickPositions = axis.tickPositions,
5846
			isFirst = pos === tickPositions[0],
5847
			isLast = pos === tickPositions[tickPositions.length - 1],
5848
			value = categories ?
5849
				pick(categories[pos], names[pos], pos) :
5850
				pos,
5851
			label = tick.label,
5852
			tickPositionInfo = tickPositions.info,
5853
			dateTimeLabelFormat;
5854
 
5855
		// Set the datetime label format. If a higher rank is set for this position, use that. If not,
5856
		// use the general format.
5857
		if (axis.isDatetimeAxis && tickPositionInfo) {
5858
			dateTimeLabelFormat = options.dateTimeLabelFormats[tickPositionInfo.higherRanks[pos] || tickPositionInfo.unitName];
5859
		}
5860
		// set properties for access in render method
5861
		tick.isFirst = isFirst;
5862
		tick.isLast = isLast;
5863
 
5864
		// get the string
5865
		str = axis.labelFormatter.call({
5866
			axis: axis,
5867
			chart: chart,
5868
			isFirst: isFirst,
5869
			isLast: isLast,
5870
			dateTimeLabelFormat: dateTimeLabelFormat,
5871
			value: axis.isLog ? correctFloat(lin2log(value)) : value
5872
		});
5873
 
5874
		// prepare CSS
5875
		//css = width && { width: mathMax(1, mathRound(width - 2 * (labelOptions.padding || 10))) + PX };
5876
 
5877
		// first call
5878
		if (!defined(label)) {
5879
 
5880
			tick.label = label =
5881
				defined(str) && labelOptions.enabled ?
5882
					chart.renderer.text(
5883
							str,
5884
							0,
5885
							0,
5886
							labelOptions.useHTML
5887
						)
5888
						//.attr(attr)
5889
						// without position absolute, IE export sometimes is wrong
5890
						.css(merge(labelOptions.style))
5891
						.add(axis.labelGroup) :
5892
					null;
5893
			tick.labelLength = label && label.getBBox().width; // Un-rotated length
5894
			tick.rotation = 0; // Base value to detect change for new calls to getBBox
5895
 
5896
		// update
5897
		} else if (label) {
5898
			label.attr({ text: str });
5899
		}
5900
	},
5901
 
5902
	/**
5903
	 * Get the offset height or width of the label
5904
	 */
5905
	getLabelSize: function () {
5906
		return this.label ?
5907
			this.label.getBBox()[this.axis.horiz ? 'height' : 'width'] :
5908
			0;
5909
	},
5910
 
5911
	/**
5912
	 * Handle the label overflow by adjusting the labels to the left and right edge, or
5913
	 * hide them if they collide into the neighbour label.
5914
	 */
5915
	handleOverflow: function (xy) {
5916
		var axis = this.axis,
5917
			pxPos = xy.x,
5918
			chartWidth = axis.chart.chartWidth,
5919
			spacing = axis.chart.spacing,
5920
			leftBound = pick(axis.labelLeft, mathMin(axis.pos, spacing[3])),
5921
			rightBound = pick(axis.labelRight, mathMax(axis.pos + axis.len, chartWidth - spacing[1])),
5922
			label = this.label,
5923
			rotation = this.rotation,
5924
			factor = { left: 0, center: 0.5, right: 1 }[axis.labelAlign],
5925
			labelWidth = label.getBBox().width,
5926
			slotWidth = axis.slotWidth,
5927
			xCorrection = factor,
5928
			goRight = 1,
5929
			leftPos,
5930
			rightPos,
5931
			textWidth,
5932
			css = {};
5933
 
5934
		// Check if the label overshoots the chart spacing box. If it does, move it.
5935
		// If it now overshoots the slotWidth, add ellipsis.
5936
		if (!rotation) {
5937
			leftPos = pxPos - factor * labelWidth;
5938
			rightPos = pxPos + (1 - factor) * labelWidth;
5939
 
5940
			if (leftPos < leftBound) {
5941
				slotWidth = xy.x + slotWidth * (1 - factor) - leftBound;
5942
			} else if (rightPos > rightBound) {
5943
				slotWidth = rightBound - xy.x + slotWidth * factor;
5944
				goRight = -1;
5945
			}
5946
 
5947
			slotWidth = mathMin(axis.slotWidth, slotWidth); // #4177
5948
			if (slotWidth < axis.slotWidth && axis.labelAlign === 'center') {
5949
				xy.x += goRight * (axis.slotWidth - slotWidth - xCorrection * (axis.slotWidth - mathMin(labelWidth, slotWidth)));
5950
			}
5951
			// If the label width exceeds the available space, set a text width to be
5952
			// picked up below. Also, if a width has been set before, we need to set a new
5953
			// one because the reported labelWidth will be limited by the box (#3938).
5954
			if (labelWidth > slotWidth || (axis.autoRotation && label.styles.width)) {
5955
				textWidth = slotWidth;
5956
			}
5957
 
5958
		// Add ellipsis to prevent rotated labels to be clipped against the edge of the chart
5959
		} else if (rotation < 0 && pxPos - factor * labelWidth < leftBound) {
5960
			textWidth = mathRound(pxPos / mathCos(rotation * deg2rad) - leftBound);
5961
		} else if (rotation > 0 && pxPos + factor * labelWidth > rightBound) {
5962
			textWidth = mathRound((chartWidth - pxPos) / mathCos(rotation * deg2rad));
5963
		}
5964
 
5965
		if (textWidth) {
5966
			css.width = textWidth;
5967
			if (!axis.options.labels.style.textOverflow) {
5968
				css.textOverflow = 'ellipsis';
5969
			}
5970
			label.css(css);
5971
		}
5972
	},
5973
 
5974
	/**
5975
	 * Get the x and y position for ticks and labels
5976
	 */
5977
	getPosition: function (horiz, pos, tickmarkOffset, old) {
5978
		var axis = this.axis,
5979
			chart = axis.chart,
5980
			cHeight = (old && chart.oldChartHeight) || chart.chartHeight;
5981
 
5982
		return {
5983
			x: horiz ?
5984
				axis.translate(pos + tickmarkOffset, null, null, old) + axis.transB :
5985
				axis.left + axis.offset + (axis.opposite ? ((old && chart.oldChartWidth) || chart.chartWidth) - axis.right - axis.left : 0),
5986
 
5987
			y: horiz ?
5988
				cHeight - axis.bottom + axis.offset - (axis.opposite ? axis.height : 0) :
5989
				cHeight - axis.translate(pos + tickmarkOffset, null, null, old) - axis.transB
5990
		};
5991
 
5992
	},
5993
 
5994
	/**
5995
	 * Get the x, y position of the tick label
5996
	 */
5997
	getLabelPosition: function (x, y, label, horiz, labelOptions, tickmarkOffset, index, step) {
5998
		var axis = this.axis,
5999
			transA = axis.transA,
6000
			reversed = axis.reversed,
6001
			staggerLines = axis.staggerLines,
6002
			rotCorr = axis.tickRotCorr || { x: 0, y: 0 },
6003
			yOffset = pick(labelOptions.y, rotCorr.y + (axis.side === 2 ? 8 : -(label.getBBox().height / 2))),
6004
			line;
6005
 
6006
		x = x + labelOptions.x + rotCorr.x - (tickmarkOffset && horiz ?
6007
			tickmarkOffset * transA * (reversed ? -1 : 1) : 0);
6008
		y = y + yOffset - (tickmarkOffset && !horiz ?
6009
			tickmarkOffset * transA * (reversed ? 1 : -1) : 0);
6010
 
6011
		// Correct for staggered labels
6012
		if (staggerLines) {
6013
			line = (index / (step || 1) % staggerLines);
6014
			y += line * (axis.labelOffset / staggerLines);
6015
		}
6016
 
6017
		return {
6018
			x: x,
6019
			y: mathRound(y)
6020
		};
6021
	},
6022
 
6023
	/**
6024
	 * Extendible method to return the path of the marker
6025
	 */
6026
	getMarkPath: function (x, y, tickLength, tickWidth, horiz, renderer) {
6027
		return renderer.crispLine([
6028
				M,
6029
				x,
6030
				y,
6031
				L,
6032
				x + (horiz ? 0 : -tickLength),
6033
				y + (horiz ? tickLength : 0)
6034
			], tickWidth);
6035
	},
6036
 
6037
	/**
6038
	 * Put everything in place
6039
	 *
6040
	 * @param index {Number}
6041
	 * @param old {Boolean} Use old coordinates to prepare an animation into new position
6042
	 */
6043
	render: function (index, old, opacity) {
6044
		var tick = this,
6045
			axis = tick.axis,
6046
			options = axis.options,
6047
			chart = axis.chart,
6048
			renderer = chart.renderer,
6049
			horiz = axis.horiz,
6050
			type = tick.type,
6051
			label = tick.label,
6052
			pos = tick.pos,
6053
			labelOptions = options.labels,
6054
			gridLine = tick.gridLine,
6055
			gridPrefix = type ? type + 'Grid' : 'grid',
6056
			tickPrefix = type ? type + 'Tick' : 'tick',
6057
			gridLineWidth = options[gridPrefix + 'LineWidth'],
6058
			gridLineColor = options[gridPrefix + 'LineColor'],
6059
			dashStyle = options[gridPrefix + 'LineDashStyle'],
6060
			tickLength = options[tickPrefix + 'Length'],
6061
			tickWidth = pick(options[tickPrefix + 'Width'], !type && axis.isXAxis ? 1 : 0), // X axis defaults to 1
6062
			tickColor = options[tickPrefix + 'Color'],
6063
			tickPosition = options[tickPrefix + 'Position'],
6064
			gridLinePath,
6065
			mark = tick.mark,
6066
			markPath,
6067
			step = /*axis.labelStep || */labelOptions.step,
6068
			attribs,
6069
			show = true,
6070
			tickmarkOffset = axis.tickmarkOffset,
6071
			xy = tick.getPosition(horiz, pos, tickmarkOffset, old),
6072
			x = xy.x,
6073
			y = xy.y,
6074
			reverseCrisp = ((horiz && x === axis.pos + axis.len) || (!horiz && y === axis.pos)) ? -1 : 1; // #1480, #1687
6075
 
6076
		opacity = pick(opacity, 1);
6077
		this.isActive = true;
6078
 
6079
		// create the grid line
6080
		if (gridLineWidth) {
6081
			gridLinePath = axis.getPlotLinePath(pos + tickmarkOffset, gridLineWidth * reverseCrisp, old, true);
6082
 
6083
			if (gridLine === UNDEFINED) {
6084
				attribs = {
6085
					stroke: gridLineColor,
6086
					'stroke-width': gridLineWidth
6087
				};
6088
				if (dashStyle) {
6089
					attribs.dashstyle = dashStyle;
6090
				}
6091
				if (!type) {
6092
					attribs.zIndex = 1;
6093
				}
6094
				if (old) {
6095
					attribs.opacity = 0;
6096
				}
6097
				tick.gridLine = gridLine =
6098
					gridLineWidth ?
6099
						renderer.path(gridLinePath)
6100
							.attr(attribs).add(axis.gridGroup) :
6101
						null;
6102
			}
6103
 
6104
			// If the parameter 'old' is set, the current call will be followed
6105
			// by another call, therefore do not do any animations this time
6106
			if (!old && gridLine && gridLinePath) {
6107
				gridLine[tick.isNew ? 'attr' : 'animate']({
6108
					d: gridLinePath,
6109
					opacity: opacity
6110
				});
6111
			}
6112
		}
6113
 
6114
		// create the tick mark
6115
		if (tickWidth && tickLength) {
6116
 
6117
			// negate the length
6118
			if (tickPosition === 'inside') {
6119
				tickLength = -tickLength;
6120
			}
6121
			if (axis.opposite) {
6122
				tickLength = -tickLength;
6123
			}
6124
 
6125
			markPath = tick.getMarkPath(x, y, tickLength, tickWidth * reverseCrisp, horiz, renderer);
6126
			if (mark) { // updating
6127
				mark.animate({
6128
					d: markPath,
6129
					opacity: opacity
6130
				});
6131
			} else { // first time
6132
				tick.mark = renderer.path(
6133
					markPath
6134
				).attr({
6135
					stroke: tickColor,
6136
					'stroke-width': tickWidth,
6137
					opacity: opacity
6138
				}).add(axis.axisGroup);
6139
			}
6140
		}
6141
 
6142
		// the label is created on init - now move it into place
6143
		if (label && !isNaN(x)) {
6144
			label.xy = xy = tick.getLabelPosition(x, y, label, horiz, labelOptions, tickmarkOffset, index, step);
6145
 
6146
			// Apply show first and show last. If the tick is both first and last, it is
6147
			// a single centered tick, in which case we show the label anyway (#2100).
6148
			if ((tick.isFirst && !tick.isLast && !pick(options.showFirstLabel, 1)) ||
6149
					(tick.isLast && !tick.isFirst && !pick(options.showLastLabel, 1))) {
6150
				show = false;
6151
 
6152
			// Handle label overflow and show or hide accordingly
6153
			} else if (horiz && !axis.isRadial && !labelOptions.step && !labelOptions.rotation && !old && opacity !== 0) {
6154
				tick.handleOverflow(xy);
6155
			}
6156
 
6157
			// apply step
6158
			if (step && index % step) {
6159
				// show those indices dividable by step
6160
				show = false;
6161
			}
6162
 
6163
			// Set the new position, and show or hide
6164
			if (show && !isNaN(xy.y)) {
6165
				xy.opacity = opacity;
6166
				label[tick.isNew ? 'attr' : 'animate'](xy);
6167
				tick.isNew = false;
6168
			} else {
6169
				label.attr('y', -9999); // #1338
6170
			}
6171
		}
6172
	},
6173
 
6174
	/**
6175
	 * Destructor for the tick prototype
6176
	 */
6177
	destroy: function () {
6178
		destroyObjectProperties(this, this.axis);
6179
	}
6180
};
6181
 
6182
/**
6183
 * The object wrapper for plot lines and plot bands
6184
 * @param {Object} options
6185
 */
6186
Highcharts.PlotLineOrBand = function (axis, options) {
6187
	this.axis = axis;
6188
 
6189
	if (options) {
6190
		this.options = options;
6191
		this.id = options.id;
6192
	}
6193
};
6194
 
6195
Highcharts.PlotLineOrBand.prototype = {
6196
 
6197
	/**
6198
	 * Render the plot line or plot band. If it is already existing,
6199
	 * move it.
6200
	 */
6201
	render: function () {
6202
		var plotLine = this,
6203
			axis = plotLine.axis,
6204
			horiz = axis.horiz,
6205
			options = plotLine.options,
6206
			optionsLabel = options.label,
6207
			label = plotLine.label,
6208
			width = options.width,
6209
			to = options.to,
6210
			from = options.from,
6211
			isBand = defined(from) && defined(to),
6212
			value = options.value,
6213
			dashStyle = options.dashStyle,
6214
			svgElem = plotLine.svgElem,
6215
			path = [],
6216
			addEvent,
6217
			eventType,
6218
			xs,
6219
			ys,
6220
			x,
6221
			y,
6222
			color = options.color,
6223
			zIndex = options.zIndex,
6224
			events = options.events,
6225
			attribs = {},
6226
			renderer = axis.chart.renderer;
6227
 
6228
		// logarithmic conversion
6229
		if (axis.isLog) {
6230
			from = log2lin(from);
6231
			to = log2lin(to);
6232
			value = log2lin(value);
6233
		}
6234
 
6235
		// plot line
6236
		if (width) {
6237
			path = axis.getPlotLinePath(value, width);
6238
			attribs = {
6239
				stroke: color,
6240
				'stroke-width': width
6241
			};
6242
			if (dashStyle) {
6243
				attribs.dashstyle = dashStyle;
6244
			}
6245
		} else if (isBand) { // plot band
6246
 
6247
			path = axis.getPlotBandPath(from, to, options);
6248
			if (color) {
6249
				attribs.fill = color;
6250
			}
6251
			if (options.borderWidth) {
6252
				attribs.stroke = options.borderColor;
6253
				attribs['stroke-width'] = options.borderWidth;
6254
			}
6255
		} else {
6256
			return;
6257
		}
6258
		// zIndex
6259
		if (defined(zIndex)) {
6260
			attribs.zIndex = zIndex;
6261
		}
6262
 
6263
		// common for lines and bands
6264
		if (svgElem) {
6265
			if (path) {
6266
				svgElem.animate({
6267
					d: path
6268
				}, null, svgElem.onGetPath);
6269
			} else {
6270
				svgElem.hide();
6271
				svgElem.onGetPath = function () {
6272
					svgElem.show();
6273
				};
6274
				if (label) {
6275
					plotLine.label = label = label.destroy();
6276
				}
6277
			}
6278
		} else if (path && path.length) {
6279
			plotLine.svgElem = svgElem = renderer.path(path)
6280
				.attr(attribs).add();
6281
 
6282
			// events
6283
			if (events) {
6284
				addEvent = function (eventType) {
6285
					svgElem.on(eventType, function (e) {
6286
						events[eventType].apply(plotLine, [e]);
6287
					});
6288
				};
6289
				for (eventType in events) {
6290
					addEvent(eventType);
6291
				}
6292
			}
6293
		}
6294
 
6295
		// the plot band/line label
6296
		if (optionsLabel && defined(optionsLabel.text) && path && path.length && axis.width > 0 && axis.height > 0) {
6297
			// apply defaults
6298
			optionsLabel = merge({
6299
				align: horiz && isBand && 'center',
6300
				x: horiz ? !isBand && 4 : 10,
6301
				verticalAlign : !horiz && isBand && 'middle',
6302
				y: horiz ? isBand ? 16 : 10 : isBand ? 6 : -4,
6303
				rotation: horiz && !isBand && 90
6304
			}, optionsLabel);
6305
 
6306
			// add the SVG element
6307
			if (!label) {
6308
				attribs = {
6309
					align: optionsLabel.textAlign || optionsLabel.align,
6310
					rotation: optionsLabel.rotation
6311
				};
6312
				if (defined(zIndex)) {
6313
					attribs.zIndex = zIndex;
6314
				}
6315
				plotLine.label = label = renderer.text(
6316
						optionsLabel.text,
6317
						0,
6318
						0,
6319
						optionsLabel.useHTML
6320
					)
6321
					.attr(attribs)
6322
					.css(optionsLabel.style)
6323
					.add();
6324
			}
6325
 
6326
			// get the bounding box and align the label
6327
			// #3000 changed to better handle choice between plotband or plotline
6328
			xs = [path[1], path[4], (isBand ? path[6] : path[1])];
6329
			ys = [path[2], path[5], (isBand ? path[7] : path[2])];
6330
			x = arrayMin(xs);
6331
			y = arrayMin(ys);
6332
 
6333
			label.align(optionsLabel, false, {
6334
				x: x,
6335
				y: y,
6336
				width: arrayMax(xs) - x,
6337
				height: arrayMax(ys) - y
6338
			});
6339
			label.show();
6340
 
6341
		} else if (label) { // move out of sight
6342
			label.hide();
6343
		}
6344
 
6345
		// chainable
6346
		return plotLine;
6347
	},
6348
 
6349
	/**
6350
	 * Remove the plot line or band
6351
	 */
6352
	destroy: function () {
6353
		// remove it from the lookup
6354
		erase(this.axis.plotLinesAndBands, this);
6355
 
6356
		delete this.axis;
6357
		destroyObjectProperties(this);
6358
	}
6359
};
6360
 
6361
/**
6362
 * Object with members for extending the Axis prototype
6363
 */
6364
 
6365
AxisPlotLineOrBandExtension = {
6366
 
6367
	/**
6368
	 * Create the path for a plot band
6369
	 */
6370
	getPlotBandPath: function (from, to) {
6371
		var toPath = this.getPlotLinePath(to, null, null, true),
6372
			path = this.getPlotLinePath(from, null, null, true);
6373
 
6374
		if (path && toPath && path.toString() !== toPath.toString()) { // #3836
6375
			path.push(
6376
				toPath[4],
6377
				toPath[5],
6378
				toPath[1],
6379
				toPath[2]
6380
			);
6381
		} else { // outside the axis area
6382
			path = null;
6383
		}
6384
 
6385
		return path;
6386
	},
6387
 
6388
	addPlotBand: function (options) {
6389
		return this.addPlotBandOrLine(options, 'plotBands');
6390
	},
6391
 
6392
	addPlotLine: function (options) {
6393
		return this.addPlotBandOrLine(options, 'plotLines');
6394
	},
6395
 
6396
	/**
6397
	 * Add a plot band or plot line after render time
6398
	 *
6399
	 * @param options {Object} The plotBand or plotLine configuration object
6400
	 */
6401
	addPlotBandOrLine: function (options, coll) {
6402
		var obj = new Highcharts.PlotLineOrBand(this, options).render(),
6403
			userOptions = this.userOptions;
6404
 
6405
		if (obj) { // #2189
6406
			// Add it to the user options for exporting and Axis.update
6407
			if (coll) {
6408
				userOptions[coll] = userOptions[coll] || [];
6409
				userOptions[coll].push(options);
6410
			}
6411
			this.plotLinesAndBands.push(obj);
6412
		}
6413
 
6414
		return obj;
6415
	},
6416
 
6417
	/**
6418
	 * Remove a plot band or plot line from the chart by id
6419
	 * @param {Object} id
6420
	 */
6421
	removePlotBandOrLine: function (id) {
6422
		var plotLinesAndBands = this.plotLinesAndBands,
6423
			options = this.options,
6424
			userOptions = this.userOptions,
6425
			i = plotLinesAndBands.length;
6426
		while (i--) {
6427
			if (plotLinesAndBands[i].id === id) {
6428
				plotLinesAndBands[i].destroy();
6429
			}
6430
		}
6431
		each([options.plotLines || [], userOptions.plotLines || [], options.plotBands || [], userOptions.plotBands || []], function (arr) {
6432
			i = arr.length;
6433
			while (i--) {
6434
				if (arr[i].id === id) {
6435
					erase(arr, arr[i]);
6436
				}
6437
			}
6438
		});
6439
	}
6440
};
6441
 
6442
/**
6443
 * Create a new axis object
6444
 * @param {Object} chart
6445
 * @param {Object} options
6446
 */
6447
var Axis = Highcharts.Axis = function () {
6448
	this.init.apply(this, arguments);
6449
};
6450
 
6451
Axis.prototype = {
6452
 
6453
	/**
6454
	 * Default options for the X axis - the Y axis has extended defaults
6455
	 */
6456
	defaultOptions: {
6457
		// allowDecimals: null,
6458
		// alternateGridColor: null,
6459
		// categories: [],
6460
		dateTimeLabelFormats: {
6461
			millisecond: '%H:%M:%S.%L',
6462
			second: '%H:%M:%S',
6463
			minute: '%H:%M',
6464
			hour: '%H:%M',
6465
			day: '%e. %b',
6466
			week: '%e. %b',
6467
			month: '%b \'%y',
6468
			year: '%Y'
6469
		},
6470
		endOnTick: false,
6471
		gridLineColor: '#D8D8D8',
6472
		// gridLineDashStyle: 'solid',
6473
		// gridLineWidth: 0,
6474
		// reversed: false,
6475
 
6476
		labels: {
6477
			enabled: true,
6478
			// rotation: 0,
6479
			// align: 'center',
6480
			// step: null,
6481
			style: {
6482
				color: '#606060',
6483
				cursor: 'default',
6484
				fontSize: '11px'
6485
			},
6486
			x: 0,
6487
			y: 15
6488
			/*formatter: function () {
6489
				return this.value;
6490
			},*/
6491
		},
6492
		lineColor: '#C0D0E0',
6493
		lineWidth: 1,
6494
		//linkedTo: null,
6495
		//max: undefined,
6496
		//min: undefined,
6497
		minPadding: 0.01,
6498
		maxPadding: 0.01,
6499
		//minRange: null,
6500
		minorGridLineColor: '#E0E0E0',
6501
		// minorGridLineDashStyle: null,
6502
		minorGridLineWidth: 1,
6503
		minorTickColor: '#A0A0A0',
6504
		//minorTickInterval: null,
6505
		minorTickLength: 2,
6506
		minorTickPosition: 'outside', // inside or outside
6507
		//minorTickWidth: 0,
6508
		//opposite: false,
6509
		//offset: 0,
6510
		//plotBands: [{
6511
		//	events: {},
6512
		//	zIndex: 1,
6513
		//	labels: { align, x, verticalAlign, y, style, rotation, textAlign }
6514
		//}],
6515
		//plotLines: [{
6516
		//	events: {}
6517
		//  dashStyle: {}
6518
		//	zIndex:
6519
		//	labels: { align, x, verticalAlign, y, style, rotation, textAlign }
6520
		//}],
6521
		//reversed: false,
6522
		// showFirstLabel: true,
6523
		// showLastLabel: true,
6524
		startOfWeek: 1,
6525
		startOnTick: false,
6526
		tickColor: '#C0D0E0',
6527
		//tickInterval: null,
6528
		tickLength: 10,
6529
		tickmarkPlacement: 'between', // on or between
6530
		tickPixelInterval: 100,
6531
		tickPosition: 'outside',
6532
		//tickWidth: 1,
6533
		title: {
6534
			//text: null,
6535
			align: 'middle', // low, middle or high
6536
			//margin: 0 for horizontal, 10 for vertical axes,
6537
			//rotation: 0,
6538
			//side: 'outside',
6539
			style: {
6540
				color: '#707070'
6541
			}
6542
			//x: 0,
6543
			//y: 0
6544
		},
6545
		type: 'linear' // linear, logarithmic or datetime
6546
	},
6547
 
6548
	/**
6549
	 * This options set extends the defaultOptions for Y axes
6550
	 */
6551
	defaultYAxisOptions: {
6552
		endOnTick: true,
6553
		gridLineWidth: 1,
6554
		tickPixelInterval: 72,
6555
		showLastLabel: true,
6556
		labels: {
6557
			x: -8,
6558
			y: 3
6559
		},
6560
		lineWidth: 0,
6561
		maxPadding: 0.05,
6562
		minPadding: 0.05,
6563
		startOnTick: true,
6564
		//tickWidth: 0,
6565
		title: {
6566
			rotation: 270,
6567
			text: 'Values'
6568
		},
6569
		stackLabels: {
6570
			enabled: false,
6571
			//align: dynamic,
6572
			//y: dynamic,
6573
			//x: dynamic,
6574
			//verticalAlign: dynamic,
6575
			//textAlign: dynamic,
6576
			//rotation: 0,
6577
			formatter: function () {
6578
				return Highcharts.numberFormat(this.total, -1);
6579
			},
6580
			style: merge(defaultPlotOptions.line.dataLabels.style, { color: '#000000' })
6581
		}
6582
	},
6583
 
6584
	/**
6585
	 * These options extend the defaultOptions for left axes
6586
	 */
6587
	defaultLeftAxisOptions: {
6588
		labels: {
6589
			x: -15,
6590
			y: null
6591
		},
6592
		title: {
6593
			rotation: 270
6594
		}
6595
	},
6596
 
6597
	/**
6598
	 * These options extend the defaultOptions for right axes
6599
	 */
6600
	defaultRightAxisOptions: {
6601
		labels: {
6602
			x: 15,
6603
			y: null
6604
		},
6605
		title: {
6606
			rotation: 90
6607
		}
6608
	},
6609
 
6610
	/**
6611
	 * These options extend the defaultOptions for bottom axes
6612
	 */
6613
	defaultBottomAxisOptions: {
6614
		labels: {
6615
			autoRotation: [-45],
6616
			x: 0,
6617
			y: null // based on font size
6618
			// overflow: undefined,
6619
			// staggerLines: null
6620
		},
6621
		title: {
6622
			rotation: 0
6623
		}
6624
	},
6625
	/**
6626
	 * These options extend the defaultOptions for top axes
6627
	 */
6628
	defaultTopAxisOptions: {
6629
		labels: {
6630
			autoRotation: [-45],
6631
			x: 0,
6632
			y: -15
6633
			// overflow: undefined
6634
			// staggerLines: null
6635
		},
6636
		title: {
6637
			rotation: 0
6638
		}
6639
	},
6640
 
6641
	/**
6642
	 * Initialize the axis
6643
	 */
6644
	init: function (chart, userOptions) {
6645
 
6646
 
6647
		var isXAxis = userOptions.isX,
6648
			axis = this;
6649
 
6650
		axis.chart = chart;
6651
 
6652
		// Flag, is the axis horizontal
6653
		axis.horiz = chart.inverted ? !isXAxis : isXAxis;
6654
 
6655
		// Flag, isXAxis
6656
		axis.isXAxis = isXAxis;
6657
		axis.coll = isXAxis ? 'xAxis' : 'yAxis';
6658
 
6659
		axis.opposite = userOptions.opposite; // needed in setOptions
6660
		axis.side = userOptions.side || (axis.horiz ?
6661
				(axis.opposite ? 0 : 2) : // top : bottom
6662
				(axis.opposite ? 1 : 3));  // right : left
6663
 
6664
		axis.setOptions(userOptions);
6665
 
6666
 
6667
		var options = this.options,
6668
			type = options.type,
6669
			isDatetimeAxis = type === 'datetime';
6670
 
6671
		axis.labelFormatter = options.labels.formatter || axis.defaultLabelFormatter; // can be overwritten by dynamic format
6672
 
6673
 
6674
		// Flag, stagger lines or not
6675
		axis.userOptions = userOptions;
6676
 
6677
		//axis.axisTitleMargin = UNDEFINED,// = options.title.margin,
6678
		axis.minPixelPadding = 0;
6679
		//axis.ignoreMinPadding = UNDEFINED; // can be set to true by a column or bar series
6680
		//axis.ignoreMaxPadding = UNDEFINED;
6681
 
6682
		axis.reversed = options.reversed;
6683
		axis.zoomEnabled = options.zoomEnabled !== false;
6684
 
6685
		// Initial categories
6686
		axis.categories = options.categories || type === 'category';
6687
		axis.names = axis.names || []; // Preserve on update (#3830)
6688
 
6689
		// Elements
6690
		//axis.axisGroup = UNDEFINED;
6691
		//axis.gridGroup = UNDEFINED;
6692
		//axis.axisTitle = UNDEFINED;
6693
		//axis.axisLine = UNDEFINED;
6694
 
6695
		// Shorthand types
6696
		axis.isLog = type === 'logarithmic';
6697
		axis.isDatetimeAxis = isDatetimeAxis;
6698
 
6699
		// Flag, if axis is linked to another axis
6700
		axis.isLinked = defined(options.linkedTo);
6701
		// Linked axis.
6702
		//axis.linkedParent = UNDEFINED;
6703
 
6704
		// Tick positions
6705
		//axis.tickPositions = UNDEFINED; // array containing predefined positions
6706
		// Tick intervals
6707
		//axis.tickInterval = UNDEFINED;
6708
		//axis.minorTickInterval = UNDEFINED;
6709
 
6710
 
6711
		// Major ticks
6712
		axis.ticks = {};
6713
		axis.labelEdge = [];
6714
		// Minor ticks
6715
		axis.minorTicks = {};
6716
 
6717
		// List of plotLines/Bands
6718
		axis.plotLinesAndBands = [];
6719
 
6720
		// Alternate bands
6721
		axis.alternateBands = {};
6722
 
6723
		// Axis metrics
6724
		//axis.left = UNDEFINED;
6725
		//axis.top = UNDEFINED;
6726
		//axis.width = UNDEFINED;
6727
		//axis.height = UNDEFINED;
6728
		//axis.bottom = UNDEFINED;
6729
		//axis.right = UNDEFINED;
6730
		//axis.transA = UNDEFINED;
6731
		//axis.transB = UNDEFINED;
6732
		//axis.oldTransA = UNDEFINED;
6733
		axis.len = 0;
6734
		//axis.oldMin = UNDEFINED;
6735
		//axis.oldMax = UNDEFINED;
6736
		//axis.oldUserMin = UNDEFINED;
6737
		//axis.oldUserMax = UNDEFINED;
6738
		//axis.oldAxisLength = UNDEFINED;
6739
		axis.minRange = axis.userMinRange = options.minRange || options.maxZoom;
6740
		axis.range = options.range;
6741
		axis.offset = options.offset || 0;
6742
 
6743
 
6744
		// Dictionary for stacks
6745
		axis.stacks = {};
6746
		axis.oldStacks = {};
6747
		axis.stacksTouched = 0;
6748
 
6749
		// Min and max in the data
6750
		//axis.dataMin = UNDEFINED,
6751
		//axis.dataMax = UNDEFINED,
6752
 
6753
		// The axis range
6754
		axis.max = null;
6755
		axis.min = null;
6756
 
6757
		// User set min and max
6758
		//axis.userMin = UNDEFINED,
6759
		//axis.userMax = UNDEFINED,
6760
 
6761
		// Crosshair options
6762
		axis.crosshair = pick(options.crosshair, splat(chart.options.tooltip.crosshairs)[isXAxis ? 0 : 1], false);
6763
		// Run Axis
6764
 
6765
		var eventType,
6766
			events = axis.options.events;
6767
 
6768
		// Register
6769
		if (inArray(axis, chart.axes) === -1) { // don't add it again on Axis.update()
6770
			if (isXAxis && !this.isColorAxis) { // #2713
6771
				chart.axes.splice(chart.xAxis.length, 0, axis);
6772
			} else {
6773
				chart.axes.push(axis);
6774
			}
6775
 
6776
			chart[axis.coll].push(axis);
6777
		}
6778
 
6779
		axis.series = axis.series || []; // populated by Series
6780
 
6781
		// inverted charts have reversed xAxes as default
6782
		if (chart.inverted && isXAxis && axis.reversed === UNDEFINED) {
6783
			axis.reversed = true;
6784
		}
6785
 
6786
		axis.removePlotBand = axis.removePlotBandOrLine;
6787
		axis.removePlotLine = axis.removePlotBandOrLine;
6788
 
6789
 
6790
		// register event listeners
6791
		for (eventType in events) {
6792
			addEvent(axis, eventType, events[eventType]);
6793
		}
6794
 
6795
		// extend logarithmic axis
6796
		if (axis.isLog) {
6797
			axis.val2lin = log2lin;
6798
			axis.lin2val = lin2log;
6799
		}
6800
	},
6801
 
6802
	/**
6803
	 * Merge and set options
6804
	 */
6805
	setOptions: function (userOptions) {
6806
		this.options = merge(
6807
			this.defaultOptions,
6808
			this.isXAxis ? {} : this.defaultYAxisOptions,
6809
			[this.defaultTopAxisOptions, this.defaultRightAxisOptions,
6810
				this.defaultBottomAxisOptions, this.defaultLeftAxisOptions][this.side],
6811
			merge(
6812
				defaultOptions[this.coll], // if set in setOptions (#1053)
6813
				userOptions
6814
			)
6815
		);
6816
	},
6817
 
6818
	/**
6819
	 * The default label formatter. The context is a special config object for the label.
6820
	 */
6821
	defaultLabelFormatter: function () {
6822
		var axis = this.axis,
6823
			value = this.value,
6824
			categories = axis.categories,
6825
			dateTimeLabelFormat = this.dateTimeLabelFormat,
6826
			numericSymbols = defaultOptions.lang.numericSymbols,
6827
			i = numericSymbols && numericSymbols.length,
6828
			multi,
6829
			ret,
6830
			formatOption = axis.options.labels.format,
6831
 
6832
			// make sure the same symbol is added for all labels on a linear axis
6833
			numericSymbolDetector = axis.isLog ? value : axis.tickInterval;
6834
 
6835
		if (formatOption) {
6836
			ret = format(formatOption, this);
6837
 
6838
		} else if (categories) {
6839
			ret = value;
6840
 
6841
		} else if (dateTimeLabelFormat) { // datetime axis
6842
			ret = dateFormat(dateTimeLabelFormat, value);
6843
 
6844
		} else if (i && numericSymbolDetector >= 1000) {
6845
			// Decide whether we should add a numeric symbol like k (thousands) or M (millions).
6846
			// If we are to enable this in tooltip or other places as well, we can move this
6847
			// logic to the numberFormatter and enable it by a parameter.
6848
			while (i-- && ret === UNDEFINED) {
6849
				multi = Math.pow(1000, i + 1);
6850
				if (numericSymbolDetector >= multi && (value * 10) % multi === 0 && numericSymbols[i] !== null) {
6851
					ret = Highcharts.numberFormat(value / multi, -1) + numericSymbols[i];
6852
				}
6853
			}
6854
		}
6855
 
6856
		if (ret === UNDEFINED) {
6857
			if (mathAbs(value) >= 10000) { // add thousands separators
6858
				ret = Highcharts.numberFormat(value, -1);
6859
 
6860
			} else { // small numbers
6861
				ret = Highcharts.numberFormat(value, -1, UNDEFINED, ''); // #2466
6862
			}
6863
		}
6864
 
6865
		return ret;
6866
	},
6867
 
6868
	/**
6869
	 * Get the minimum and maximum for the series of each axis
6870
	 */
6871
	getSeriesExtremes: function () {
6872
		var axis = this,
6873
			chart = axis.chart;
6874
 
6875
		axis.hasVisibleSeries = false;
6876
 
6877
		// Reset properties in case we're redrawing (#3353)
6878
		axis.dataMin = axis.dataMax = axis.ignoreMinPadding = axis.ignoreMaxPadding = null;
6879
 
6880
		if (axis.buildStacks) {
6881
			axis.buildStacks();
6882
		}
6883
 
6884
		// loop through this axis' series
6885
		each(axis.series, function (series) {
6886
 
6887
			if (series.visible || !chart.options.chart.ignoreHiddenSeries) {
6888
 
6889
				var seriesOptions = series.options,
6890
					xData,
6891
					threshold = seriesOptions.threshold,
6892
					seriesDataMin,
6893
					seriesDataMax;
6894
 
6895
				axis.hasVisibleSeries = true;
6896
 
6897
				// Validate threshold in logarithmic axes
6898
				if (axis.isLog && threshold <= 0) {
6899
					threshold = null;
6900
				}
6901
 
6902
				// Get dataMin and dataMax for X axes
6903
				if (axis.isXAxis) {
6904
					xData = series.xData;
6905
					if (xData.length) {
6906
						axis.dataMin = mathMin(pick(axis.dataMin, xData[0]), arrayMin(xData));
6907
						axis.dataMax = mathMax(pick(axis.dataMax, xData[0]), arrayMax(xData));
6908
					}
6909
 
6910
				// Get dataMin and dataMax for Y axes, as well as handle stacking and processed data
6911
				} else {
6912
 
6913
					// Get this particular series extremes
6914
					series.getExtremes();
6915
					seriesDataMax = series.dataMax;
6916
					seriesDataMin = series.dataMin;
6917
 
6918
					// Get the dataMin and dataMax so far. If percentage is used, the min and max are
6919
					// always 0 and 100. If seriesDataMin and seriesDataMax is null, then series
6920
					// doesn't have active y data, we continue with nulls
6921
					if (defined(seriesDataMin) && defined(seriesDataMax)) {
6922
						axis.dataMin = mathMin(pick(axis.dataMin, seriesDataMin), seriesDataMin);
6923
						axis.dataMax = mathMax(pick(axis.dataMax, seriesDataMax), seriesDataMax);
6924
					}
6925
 
6926
					// Adjust to threshold
6927
					if (defined(threshold)) {
6928
						if (axis.dataMin >= threshold) {
6929
							axis.dataMin = threshold;
6930
							axis.ignoreMinPadding = true;
6931
						} else if (axis.dataMax < threshold) {
6932
							axis.dataMax = threshold;
6933
							axis.ignoreMaxPadding = true;
6934
						}
6935
					}
6936
				}
6937
			}
6938
		});
6939
	},
6940
 
6941
	/**
6942
	 * Translate from axis value to pixel position on the chart, or back
6943
	 *
6944
	 */
6945
	translate: function (val, backwards, cvsCoord, old, handleLog, pointPlacement) {
6946
		var axis = this.linkedParent || this, // #1417
6947
			sign = 1,
6948
			cvsOffset = 0,
6949
			localA = old ? axis.oldTransA : axis.transA,
6950
			localMin = old ? axis.oldMin : axis.min,
6951
			returnValue,
6952
			minPixelPadding = axis.minPixelPadding,
6953
			doPostTranslate = (axis.doPostTranslate || (axis.isLog && handleLog)) && axis.lin2val;
6954
 
6955
		if (!localA) {
6956
			localA = axis.transA;
6957
		}
6958
 
6959
		// In vertical axes, the canvas coordinates start from 0 at the top like in
6960
		// SVG.
6961
		if (cvsCoord) {
6962
			sign *= -1; // canvas coordinates inverts the value
6963
			cvsOffset = axis.len;
6964
		}
6965
 
6966
		// Handle reversed axis
6967
		if (axis.reversed) {
6968
			sign *= -1;
6969
			cvsOffset -= sign * (axis.sector || axis.len);
6970
		}
6971
 
6972
		// From pixels to value
6973
		if (backwards) { // reverse translation
6974
 
6975
			val = val * sign + cvsOffset;
6976
			val -= minPixelPadding;
6977
			returnValue = val / localA + localMin; // from chart pixel to value
6978
			if (doPostTranslate) { // log and ordinal axes
6979
				returnValue = axis.lin2val(returnValue);
6980
			}
6981
 
6982
		// From value to pixels
6983
		} else {
6984
			if (doPostTranslate) { // log and ordinal axes
6985
				val = axis.val2lin(val);
6986
			}
6987
			if (pointPlacement === 'between') {
6988
				pointPlacement = 0.5;
6989
			}
6990
			returnValue = sign * (val - localMin) * localA + cvsOffset + (sign * minPixelPadding) +
6991
				(isNumber(pointPlacement) ? localA * pointPlacement * axis.pointRange : 0);
6992
		}
6993
 
6994
		return returnValue;
6995
	},
6996
 
6997
	/**
6998
	 * Utility method to translate an axis value to pixel position.
6999
	 * @param {Number} value A value in terms of axis units
7000
	 * @param {Boolean} paneCoordinates Whether to return the pixel coordinate relative to the chart
7001
	 *        or just the axis/pane itself.
7002
	 */
7003
	toPixels: function (value, paneCoordinates) {
7004
		return this.translate(value, false, !this.horiz, null, true) + (paneCoordinates ? 0 : this.pos);
7005
	},
7006
 
7007
	/*
7008
	 * Utility method to translate a pixel position in to an axis value
7009
	 * @param {Number} pixel The pixel value coordinate
7010
	 * @param {Boolean} paneCoordiantes Whether the input pixel is relative to the chart or just the
7011
	 *        axis/pane itself.
7012
	 */
7013
	toValue: function (pixel, paneCoordinates) {
7014
		return this.translate(pixel - (paneCoordinates ? 0 : this.pos), true, !this.horiz, null, true);
7015
	},
7016
 
7017
	/**
7018
	 * Create the path for a plot line that goes from the given value on
7019
	 * this axis, across the plot to the opposite side
7020
	 * @param {Number} value
7021
	 * @param {Number} lineWidth Used for calculation crisp line
7022
	 * @param {Number] old Use old coordinates (for resizing and rescaling)
7023
	 */
7024
	getPlotLinePath: function (value, lineWidth, old, force, translatedValue) {
7025
		var axis = this,
7026
			chart = axis.chart,
7027
			axisLeft = axis.left,
7028
			axisTop = axis.top,
7029
			x1,
7030
			y1,
7031
			x2,
7032
			y2,
7033
			cHeight = (old && chart.oldChartHeight) || chart.chartHeight,
7034
			cWidth = (old && chart.oldChartWidth) || chart.chartWidth,
7035
			skip,
7036
			transB = axis.transB,
7037
			/**
7038
			 * Check if x is between a and b. If not, either move to a/b or skip,
7039
			 * depending on the force parameter.
7040
			 */
7041
			between = function (x, a, b) {
7042
				if (x < a || x > b) {
7043
					if (force) {
7044
						x = mathMin(mathMax(a, x), b);
7045
					} else {
7046
						skip = true;
7047
					}
7048
				}
7049
				return x;
7050
			};
7051
 
7052
		translatedValue = pick(translatedValue, axis.translate(value, null, null, old));
7053
		x1 = x2 = mathRound(translatedValue + transB);
7054
		y1 = y2 = mathRound(cHeight - translatedValue - transB);
7055
 
7056
		if (isNaN(translatedValue)) { // no min or max
7057
			skip = true;
7058
 
7059
		} else if (axis.horiz) {
7060
			y1 = axisTop;
7061
			y2 = cHeight - axis.bottom;
7062
			x1 = x2 = between(x1, axisLeft, axisLeft + axis.width);
7063
		} else {
7064
			x1 = axisLeft;
7065
			x2 = cWidth - axis.right;
7066
			y1 = y2 = between(y1, axisTop, axisTop + axis.height);
7067
		}
7068
		return skip && !force ?
7069
			null :
7070
			chart.renderer.crispLine([M, x1, y1, L, x2, y2], lineWidth || 1);
7071
	},
7072
 
7073
	/**
7074
	 * Set the tick positions of a linear axis to round values like whole tens or every five.
7075
	 */
7076
	getLinearTickPositions: function (tickInterval, min, max) {
7077
		var pos,
7078
			lastPos,
7079
			roundedMin = correctFloat(mathFloor(min / tickInterval) * tickInterval),
7080
			roundedMax = correctFloat(mathCeil(max / tickInterval) * tickInterval),
7081
			tickPositions = [];
7082
 
7083
		// For single points, add a tick regardless of the relative position (#2662)
7084
		if (min === max && isNumber(min)) {
7085
			return [min];
7086
		}
7087
 
7088
		// Populate the intermediate values
7089
		pos = roundedMin;
7090
		while (pos <= roundedMax) {
7091
 
7092
			// Place the tick on the rounded value
7093
			tickPositions.push(pos);
7094
 
7095
			// Always add the raw tickInterval, not the corrected one.
7096
			pos = correctFloat(pos + tickInterval);
7097
 
7098
			// If the interval is not big enough in the current min - max range to actually increase
7099
			// the loop variable, we need to break out to prevent endless loop. Issue #619
7100
			if (pos === lastPos) {
7101
				break;
7102
			}
7103
 
7104
			// Record the last value
7105
			lastPos = pos;
7106
		}
7107
		return tickPositions;
7108
	},
7109
 
7110
	/**
7111
	 * Return the minor tick positions. For logarithmic axes, reuse the same logic
7112
	 * as for major ticks.
7113
	 */
7114
	getMinorTickPositions: function () {
7115
		var axis = this,
7116
			options = axis.options,
7117
			tickPositions = axis.tickPositions,
7118
			minorTickInterval = axis.minorTickInterval,
7119
			minorTickPositions = [],
7120
			pos,
7121
			i,
7122
			pointRangePadding = axis.pointRangePadding || 0,
7123
			min = axis.min - pointRangePadding, // #1498
7124
			max = axis.max + pointRangePadding, // #1498
7125
			range = max - min,
7126
			len;
7127
 
7128
		// If minor ticks get too dense, they are hard to read, and may cause long running script. So we don't draw them.
7129
		if (range && range / minorTickInterval < axis.len / 3) { // #3875
7130
 
7131
			if (axis.isLog) {
7132
				len = tickPositions.length;
7133
				for (i = 1; i < len; i++) {
7134
					minorTickPositions = minorTickPositions.concat(
7135
						axis.getLogTickPositions(minorTickInterval, tickPositions[i - 1], tickPositions[i], true)
7136
					);
7137
				}
7138
			} else if (axis.isDatetimeAxis && options.minorTickInterval === 'auto') { // #1314
7139
				minorTickPositions = minorTickPositions.concat(
7140
					axis.getTimeTicks(
7141
						axis.normalizeTimeTickInterval(minorTickInterval),
7142
						min,
7143
						max,
7144
						options.startOfWeek
7145
					)
7146
				);
7147
			} else {
7148
				for (pos = min + (tickPositions[0] - min) % minorTickInterval; pos <= max; pos += minorTickInterval) {
7149
					minorTickPositions.push(pos);
7150
				}
7151
			}
7152
		}
7153
 
7154
		if(minorTickPositions.length !== 0) { // don't change the extremes, when there is no minor ticks
7155
			axis.trimTicks(minorTickPositions, options.startOnTick, options.endOnTick); // #3652 #3743 #1498
7156
		}
7157
		return minorTickPositions;
7158
	},
7159
 
7160
	/**
7161
	 * Adjust the min and max for the minimum range. Keep in mind that the series data is
7162
	 * not yet processed, so we don't have information on data cropping and grouping, or
7163
	 * updated axis.pointRange or series.pointRange. The data can't be processed until
7164
	 * we have finally established min and max.
7165
	 */
7166
	adjustForMinRange: function () {
7167
		var axis = this,
7168
			options = axis.options,
7169
			min = axis.min,
7170
			max = axis.max,
7171
			zoomOffset,
7172
			spaceAvailable = axis.dataMax - axis.dataMin >= axis.minRange,
7173
			closestDataRange,
7174
			i,
7175
			distance,
7176
			xData,
7177
			loopLength,
7178
			minArgs,
7179
			maxArgs;
7180
 
7181
		// Set the automatic minimum range based on the closest point distance
7182
		if (axis.isXAxis && axis.minRange === UNDEFINED && !axis.isLog) {
7183
 
7184
			if (defined(options.min) || defined(options.max)) {
7185
				axis.minRange = null; // don't do this again
7186
 
7187
			} else {
7188
 
7189
				// Find the closest distance between raw data points, as opposed to
7190
				// closestPointRange that applies to processed points (cropped and grouped)
7191
				each(axis.series, function (series) {
7192
					xData = series.xData;
7193
					loopLength = series.xIncrement ? 1 : xData.length - 1;
7194
					for (i = loopLength; i > 0; i--) {
7195
						distance = xData[i] - xData[i - 1];
7196
						if (closestDataRange === UNDEFINED || distance < closestDataRange) {
7197
							closestDataRange = distance;
7198
						}
7199
					}
7200
				});
7201
				axis.minRange = mathMin(closestDataRange * 5, axis.dataMax - axis.dataMin);
7202
			}
7203
		}
7204
 
7205
		// if minRange is exceeded, adjust
7206
		if (max - min < axis.minRange) {
7207
			var minRange = axis.minRange;
7208
			zoomOffset = (minRange - max + min) / 2;
7209
 
7210
			// if min and max options have been set, don't go beyond it
7211
			minArgs = [min - zoomOffset, pick(options.min, min - zoomOffset)];
7212
			if (spaceAvailable) { // if space is available, stay within the data range
7213
				minArgs[2] = axis.dataMin;
7214
			}
7215
			min = arrayMax(minArgs);
7216
 
7217
			maxArgs = [min + minRange, pick(options.max, min + minRange)];
7218
			if (spaceAvailable) { // if space is availabe, stay within the data range
7219
				maxArgs[2] = axis.dataMax;
7220
			}
7221
 
7222
			max = arrayMin(maxArgs);
7223
 
7224
			// now if the max is adjusted, adjust the min back
7225
			if (max - min < minRange) {
7226
				minArgs[0] = max - minRange;
7227
				minArgs[1] = pick(options.min, max - minRange);
7228
				min = arrayMax(minArgs);
7229
			}
7230
		}
7231
 
7232
		// Record modified extremes
7233
		axis.min = min;
7234
		axis.max = max;
7235
	},
7236
 
7237
	/**
7238
	 * Update translation information
7239
	 */
7240
	setAxisTranslation: function (saveOld) {
7241
		var axis = this,
7242
			range = axis.max - axis.min,
7243
			pointRange = axis.axisPointRange || 0,
7244
			closestPointRange,
7245
			minPointOffset = 0,
7246
			pointRangePadding = 0,
7247
			linkedParent = axis.linkedParent,
7248
			ordinalCorrection,
7249
			hasCategories = !!axis.categories,
7250
			transA = axis.transA,
7251
			isXAxis = axis.isXAxis;
7252
 
7253
		// Adjust translation for padding. Y axis with categories need to go through the same (#1784).
7254
		if (isXAxis || hasCategories || pointRange) {
7255
			if (linkedParent) {
7256
				minPointOffset = linkedParent.minPointOffset;
7257
				pointRangePadding = linkedParent.pointRangePadding;
7258
 
7259
			} else {
7260
				each(axis.series, function (series) {
7261
					var seriesPointRange = hasCategories ? 1 : (isXAxis ? series.pointRange : (axis.axisPointRange || 0)), // #2806
7262
						pointPlacement = series.options.pointPlacement,
7263
						seriesClosestPointRange = series.closestPointRange;
7264
 
7265
					if (seriesPointRange > range) { // #1446
7266
						seriesPointRange = 0;
7267
					}
7268
					pointRange = mathMax(pointRange, seriesPointRange);
7269
 
7270
					if (!axis.single) {
7271
						// minPointOffset is the value padding to the left of the axis in order to make
7272
						// room for points with a pointRange, typically columns. When the pointPlacement option
7273
						// is 'between' or 'on', this padding does not apply.
7274
						minPointOffset = mathMax(
7275
							minPointOffset,
7276
							isString(pointPlacement) ? 0 : seriesPointRange / 2
7277
						);
7278
 
7279
						// Determine the total padding needed to the length of the axis to make room for the
7280
						// pointRange. If the series' pointPlacement is 'on', no padding is added.
7281
						pointRangePadding = mathMax(
7282
							pointRangePadding,
7283
							pointPlacement === 'on' ? 0 : seriesPointRange
7284
						);
7285
					}
7286
 
7287
					// Set the closestPointRange
7288
					if (!series.noSharedTooltip && defined(seriesClosestPointRange)) {
7289
						closestPointRange = defined(closestPointRange) ?
7290
							mathMin(closestPointRange, seriesClosestPointRange) :
7291
							seriesClosestPointRange;
7292
					}
7293
				});
7294
			}
7295
 
7296
			// Record minPointOffset and pointRangePadding
7297
			ordinalCorrection = axis.ordinalSlope && closestPointRange ? axis.ordinalSlope / closestPointRange : 1; // #988, #1853
7298
			axis.minPointOffset = minPointOffset = minPointOffset * ordinalCorrection;
7299
			axis.pointRangePadding = pointRangePadding = pointRangePadding * ordinalCorrection;
7300
 
7301
			// pointRange means the width reserved for each point, like in a column chart
7302
			axis.pointRange = mathMin(pointRange, range);
7303
 
7304
			// closestPointRange means the closest distance between points. In columns
7305
			// it is mostly equal to pointRange, but in lines pointRange is 0 while closestPointRange
7306
			// is some other value
7307
			if (isXAxis) {
7308
				axis.closestPointRange = closestPointRange;
7309
			}
7310
		}
7311
 
7312
		// Secondary values
7313
		if (saveOld) {
7314
			axis.oldTransA = transA;
7315
		}
7316
		axis.translationSlope = axis.transA = transA = axis.len / ((range + pointRangePadding) || 1);
7317
		axis.transB = axis.horiz ? axis.left : axis.bottom; // translation addend
7318
		axis.minPixelPadding = transA * minPointOffset;
7319
	},
7320
 
7321
	minFromRange: function () {
7322
		return this.max - this.range;
7323
	},
7324
 
7325
	/**
7326
	 * Set the tick positions to round values and optionally extend the extremes
7327
	 * to the nearest tick
7328
	 */
7329
	setTickInterval: function (secondPass) {
7330
		var axis = this,
7331
			chart = axis.chart,
7332
			options = axis.options,
7333
			isLog = axis.isLog,
7334
			isDatetimeAxis = axis.isDatetimeAxis,
7335
			isXAxis = axis.isXAxis,
7336
			isLinked = axis.isLinked,
7337
			maxPadding = options.maxPadding,
7338
			minPadding = options.minPadding,
7339
			length,
7340
			linkedParentExtremes,
7341
			tickIntervalOption = options.tickInterval,
7342
			minTickInterval,
7343
			tickPixelIntervalOption = options.tickPixelInterval,
7344
			categories = axis.categories;
7345
 
7346
		if (!isDatetimeAxis && !categories && !isLinked) {
7347
			this.getTickAmount();
7348
		}
7349
 
7350
		// linked axis gets the extremes from the parent axis
7351
		if (isLinked) {
7352
			axis.linkedParent = chart[axis.coll][options.linkedTo];
7353
			linkedParentExtremes = axis.linkedParent.getExtremes();
7354
			axis.min = pick(linkedParentExtremes.min, linkedParentExtremes.dataMin);
7355
			axis.max = pick(linkedParentExtremes.max, linkedParentExtremes.dataMax);
7356
			if (options.type !== axis.linkedParent.options.type) {
7357
				error(11, 1); // Can't link axes of different type
7358
			}
7359
		} else { // initial min and max from the extreme data values
7360
			axis.min = pick(axis.userMin, options.min, axis.dataMin);
7361
			axis.max = pick(axis.userMax, options.max, axis.dataMax);
7362
		}
7363
 
7364
		if (isLog) {
7365
			if (!secondPass && mathMin(axis.min, pick(axis.dataMin, axis.min)) <= 0) { // #978
7366
				error(10, 1); // Can't plot negative values on log axis
7367
			}
7368
			// The correctFloat cures #934, float errors on full tens. But it
7369
			// was too aggressive for #4360 because of conversion back to lin,
7370
			// therefore use precision 15.
7371
			axis.min = correctFloat(log2lin(axis.min), 15);
7372
			axis.max = correctFloat(log2lin(axis.max), 15);
7373
		}
7374
 
7375
		// handle zoomed range
7376
		if (axis.range && defined(axis.max)) {
7377
			axis.userMin = axis.min = mathMax(axis.min, axis.minFromRange()); // #618
7378
			axis.userMax = axis.max;
7379
 
7380
			axis.range = null;  // don't use it when running setExtremes
7381
		}
7382
 
7383
		// Hook for adjusting this.min and this.max. Used by bubble series.
7384
		if (axis.beforePadding) {
7385
			axis.beforePadding();
7386
		}
7387
 
7388
		// adjust min and max for the minimum range
7389
		axis.adjustForMinRange();
7390
 
7391
		// Pad the values to get clear of the chart's edges. To avoid tickInterval taking the padding
7392
		// into account, we do this after computing tick interval (#1337).
7393
		if (!categories && !axis.axisPointRange && !axis.usePercentage && !isLinked && defined(axis.min) && defined(axis.max)) {
7394
			length = axis.max - axis.min;
7395
			if (length) {
7396
				if (!defined(options.min) && !defined(axis.userMin) && minPadding && (axis.dataMin < 0 || !axis.ignoreMinPadding)) {
7397
					axis.min -= length * minPadding;
7398
				}
7399
				if (!defined(options.max) && !defined(axis.userMax)  && maxPadding && (axis.dataMax > 0 || !axis.ignoreMaxPadding)) {
7400
					axis.max += length * maxPadding;
7401
				}
7402
			}
7403
		}
7404
 
7405
		// Stay within floor and ceiling
7406
		if (isNumber(options.floor)) {
7407
			axis.min = mathMax(axis.min, options.floor);
7408
		}
7409
		if (isNumber(options.ceiling)) {
7410
			axis.max = mathMin(axis.max, options.ceiling);
7411
		}
7412
 
7413
		// get tickInterval
7414
		if (axis.min === axis.max || axis.min === undefined || axis.max === undefined) {
7415
			axis.tickInterval = 1;
7416
		} else if (isLinked && !tickIntervalOption &&
7417
				tickPixelIntervalOption === axis.linkedParent.options.tickPixelInterval) {
7418
			axis.tickInterval = tickIntervalOption = axis.linkedParent.tickInterval;
7419
		} else {
7420
			axis.tickInterval = pick(
7421
				tickIntervalOption,
7422
				this.tickAmount ? ((axis.max - axis.min) / mathMax(this.tickAmount - 1, 1)) : undefined,
7423
				categories ? // for categoried axis, 1 is default, for linear axis use tickPix
7424
					1 :
7425
					// don't let it be more than the data range
7426
					(axis.max - axis.min) * tickPixelIntervalOption / mathMax(axis.len, tickPixelIntervalOption)
7427
			);
7428
		}
7429
 
7430
		// Now we're finished detecting min and max, crop and group series data. This
7431
		// is in turn needed in order to find tick positions in ordinal axes.
7432
		if (isXAxis && !secondPass) {
7433
			each(axis.series, function (series) {
7434
				series.processData(axis.min !== axis.oldMin || axis.max !== axis.oldMax);
7435
			});
7436
		}
7437
 
7438
		// set the translation factor used in translate function
7439
		axis.setAxisTranslation(true);
7440
 
7441
		// hook for ordinal axes and radial axes
7442
		if (axis.beforeSetTickPositions) {
7443
			axis.beforeSetTickPositions();
7444
		}
7445
 
7446
		// hook for extensions, used in Highstock ordinal axes
7447
		if (axis.postProcessTickInterval) {
7448
			axis.tickInterval = axis.postProcessTickInterval(axis.tickInterval);
7449
		}
7450
 
7451
		// In column-like charts, don't cramp in more ticks than there are points (#1943)
7452
		if (axis.pointRange) {
7453
			axis.tickInterval = mathMax(axis.pointRange, axis.tickInterval);
7454
		}
7455
 
7456
		// Before normalizing the tick interval, handle minimum tick interval. This applies only if tickInterval is not defined.
7457
		minTickInterval = pick(options.minTickInterval, axis.isDatetimeAxis && axis.closestPointRange);
7458
		if (!tickIntervalOption && axis.tickInterval < minTickInterval) {
7459
			axis.tickInterval = minTickInterval;
7460
		}
7461
 
7462
		// for linear axes, get magnitude and normalize the interval
7463
		if (!isDatetimeAxis && !isLog && !tickIntervalOption) {
7464
			axis.tickInterval = normalizeTickInterval(
7465
				axis.tickInterval,
7466
				null,
7467
				getMagnitude(axis.tickInterval),
7468
				// If the tick interval is between 0.5 and 5 and the axis max is in the order of
7469
				// thousands, chances are we are dealing with years. Don't allow decimals. #3363.
7470
				pick(options.allowDecimals, !(axis.tickInterval > 0.5 && axis.tickInterval < 5 && axis.max > 1000 && axis.max < 9999)),
7471
				!!this.tickAmount
7472
			);
7473
		}
7474
 
7475
		// Prevent ticks from getting so close that we can't draw the labels
7476
		if (!this.tickAmount && this.len) { // Color axis with disabled legend has no length
7477
			axis.tickInterval = axis.unsquish();
7478
		}
7479
 
7480
		this.setTickPositions();
7481
	},
7482
 
7483
	/**
7484
	 * Now we have computed the normalized tickInterval, get the tick positions
7485
	 */
7486
	setTickPositions: function () {
7487
 
7488
		var options = this.options,
7489
			tickPositions,
7490
			tickPositionsOption = options.tickPositions,
7491
			tickPositioner = options.tickPositioner,
7492
			startOnTick = options.startOnTick,
7493
			endOnTick = options.endOnTick,
7494
			single;
7495
 
7496
		// Set the tickmarkOffset
7497
		this.tickmarkOffset = (this.categories && options.tickmarkPlacement === 'between' &&
7498
			this.tickInterval === 1) ? 0.5 : 0; // #3202
7499
 
7500
 
7501
		// get minorTickInterval
7502
		this.minorTickInterval = options.minorTickInterval === 'auto' && this.tickInterval ?
7503
			this.tickInterval / 5 : options.minorTickInterval;
7504
 
7505
		// Find the tick positions
7506
		this.tickPositions = tickPositions = tickPositionsOption && tickPositionsOption.slice(); // Work on a copy (#1565)
7507
		if (!tickPositions) {
7508
 
7509
			if (this.isDatetimeAxis) {
7510
				tickPositions = this.getTimeTicks(
7511
					this.normalizeTimeTickInterval(this.tickInterval, options.units),
7512
					this.min,
7513
					this.max,
7514
					options.startOfWeek,
7515
					this.ordinalPositions,
7516
					this.closestPointRange,
7517
					true
7518
				);
7519
			} else if (this.isLog) {
7520
				tickPositions = this.getLogTickPositions(this.tickInterval, this.min, this.max);
7521
			} else {
7522
				tickPositions = this.getLinearTickPositions(this.tickInterval, this.min, this.max);
7523
			}
7524
 
7525
			// Too dense ticks, keep only the first and last (#4477)
7526
			if (tickPositions.length > this.len) {
7527
				tickPositions = [tickPositions[0], tickPositions.pop()];
7528
			}
7529
 
7530
			this.tickPositions = tickPositions;
7531
 
7532
			// Run the tick positioner callback, that allows modifying auto tick positions.
7533
			if (tickPositioner) {
7534
				tickPositioner = tickPositioner.apply(this, [this.min, this.max]);
7535
				if (tickPositioner) {
7536
					this.tickPositions = tickPositions = tickPositioner;
7537
				}
7538
			}
7539
 
7540
		}
7541
 
7542
		if (!this.isLinked) {
7543
 
7544
			// reset min/max or remove extremes based on start/end on tick
7545
			this.trimTicks(tickPositions, startOnTick, endOnTick);
7546
 
7547
			// When there is only one point, or all points have the same value on this axis, then min
7548
			// and max are equal and tickPositions.length is 0 or 1. In this case, add some padding
7549
			// in order to center the point, but leave it with one tick. #1337.
7550
			if (this.min === this.max && defined(this.min) && !this.tickAmount) {
7551
				// Substract half a unit (#2619, #2846, #2515, #3390)
7552
				single = true;
7553
				this.min -= 0.5;
7554
				this.max += 0.5;
7555
			}
7556
			this.single = single;
7557
 
7558
			if (!tickPositionsOption && !tickPositioner) {
7559
				this.adjustTickAmount();
7560
			}
7561
		}
7562
	},
7563
 
7564
	/**
7565
	 * Handle startOnTick and endOnTick by either adapting to padding min/max or rounded min/max
7566
	 */
7567
	trimTicks: function (tickPositions, startOnTick, endOnTick) {
7568
		var roundedMin = tickPositions[0],
7569
			roundedMax = tickPositions[tickPositions.length - 1],
7570
			minPointOffset = this.minPointOffset || 0;
7571
 
7572
		if (startOnTick) {
7573
			this.min = roundedMin;
7574
		} else if (this.min - minPointOffset > roundedMin) {
7575
			tickPositions.shift();
7576
		}
7577
 
7578
		if (endOnTick) {
7579
			this.max = roundedMax;
7580
		} else if (this.max + minPointOffset < roundedMax) {
7581
			tickPositions.pop();
7582
		}
7583
 
7584
		// If no tick are left, set one tick in the middle (#3195)
7585
		if (tickPositions.length === 0 && defined(roundedMin)) {
7586
			tickPositions.push((roundedMax + roundedMin) / 2);
7587
		}
7588
	},
7589
 
7590
	/**
7591
	 * Set the max ticks of either the x and y axis collection
7592
	 */
7593
	getTickAmount: function () {
7594
		var others = {}, // Whether there is another axis to pair with this one
7595
			hasOther,
7596
			options = this.options,
7597
			tickAmount = options.tickAmount,
7598
			tickPixelInterval = options.tickPixelInterval;
7599
 
7600
		if (!defined(options.tickInterval) && this.len < tickPixelInterval && !this.isRadial &&
7601
				!this.isLog && options.startOnTick && options.endOnTick) {
7602
			tickAmount = 2;
7603
		}
7604
 
7605
		if (!tickAmount && this.chart.options.chart.alignTicks !== false && options.alignTicks !== false) {
7606
			// Check if there are multiple axes in the same pane
7607
			each(this.chart[this.coll], function (axis) {
7608
				var options = axis.options,
7609
					horiz = axis.horiz,
7610
					key = [horiz ? options.left : options.top, horiz ? options.width : options.height, options.pane].join(',');
7611
 
7612
				if (others[key]) {
7613
					if (axis.series.length) {
7614
						hasOther = true; // #4201
7615
					}
7616
				} else {
7617
					others[key] = 1;
7618
				}
7619
			});
7620
 
7621
			if (hasOther) {
7622
				// Add 1 because 4 tick intervals require 5 ticks (including first and last)
7623
				tickAmount = mathCeil(this.len / tickPixelInterval) + 1;
7624
			}
7625
		}
7626
 
7627
		// For tick amounts of 2 and 3, compute five ticks and remove the intermediate ones. This
7628
		// prevents the axis from adding ticks that are too far away from the data extremes.
7629
		if (tickAmount < 4) {
7630
			this.finalTickAmt = tickAmount;
7631
			tickAmount = 5;
7632
		}
7633
 
7634
		this.tickAmount = tickAmount;
7635
	},
7636
 
7637
	/**
7638
	 * When using multiple axes, adjust the number of ticks to match the highest
7639
	 * number of ticks in that group
7640
	 */
7641
	adjustTickAmount: function () {
7642
		var tickInterval = this.tickInterval,
7643
			tickPositions = this.tickPositions,
7644
			tickAmount = this.tickAmount,
7645
			finalTickAmt = this.finalTickAmt,
7646
			currentTickAmount = tickPositions && tickPositions.length,
7647
			i,
7648
			len;
7649
 
7650
		if (currentTickAmount < tickAmount) { // TODO: Check #3411
7651
			while (tickPositions.length < tickAmount) {
7652
				tickPositions.push(correctFloat(
7653
					tickPositions[tickPositions.length - 1] + tickInterval
7654
				));
7655
			}
7656
			this.transA *= (currentTickAmount - 1) / (tickAmount - 1);
7657
			this.max = tickPositions[tickPositions.length - 1];
7658
 
7659
		// We have too many ticks, run second pass to try to reduce ticks
7660
		} else if (currentTickAmount > tickAmount) {
7661
			this.tickInterval *= 2;
7662
			this.setTickPositions();
7663
		}
7664
 
7665
		// The finalTickAmt property is set in getTickAmount
7666
		if (defined(finalTickAmt)) {
7667
			i = len = tickPositions.length;
7668
			while (i--) {
7669
				if (
7670
					(finalTickAmt === 3 && i % 2 === 1) || // Remove every other tick
7671
					(finalTickAmt <= 2 && i > 0 && i < len - 1) // Remove all but first and last
7672
				) {
7673
					tickPositions.splice(i, 1);
7674
				}
7675
			}
7676
			this.finalTickAmt = UNDEFINED;
7677
		}
7678
	},
7679
 
7680
	/**
7681
	 * Set the scale based on data min and max, user set min and max or options
7682
	 *
7683
	 */
7684
	setScale: function () {
7685
		var axis = this,
7686
			isDirtyData,
7687
			isDirtyAxisLength;
7688
 
7689
		axis.oldMin = axis.min;
7690
		axis.oldMax = axis.max;
7691
		axis.oldAxisLength = axis.len;
7692
 
7693
		// set the new axisLength
7694
		axis.setAxisSize();
7695
		//axisLength = horiz ? axisWidth : axisHeight;
7696
		isDirtyAxisLength = axis.len !== axis.oldAxisLength;
7697
 
7698
		// is there new data?
7699
		each(axis.series, function (series) {
7700
			if (series.isDirtyData || series.isDirty ||
7701
					series.xAxis.isDirty) { // when x axis is dirty, we need new data extremes for y as well
7702
				isDirtyData = true;
7703
			}
7704
		});
7705
 
7706
		// do we really need to go through all this?
7707
		if (isDirtyAxisLength || isDirtyData || axis.isLinked || axis.forceRedraw ||
7708
			axis.userMin !== axis.oldUserMin || axis.userMax !== axis.oldUserMax) {
7709
 
7710
			if (axis.resetStacks) {
7711
				axis.resetStacks();
7712
			}
7713
 
7714
			axis.forceRedraw = false;
7715
 
7716
			// get data extremes if needed
7717
			axis.getSeriesExtremes();
7718
 
7719
			// get fixed positions based on tickInterval
7720
			axis.setTickInterval();
7721
 
7722
			// record old values to decide whether a rescale is necessary later on (#540)
7723
			axis.oldUserMin = axis.userMin;
7724
			axis.oldUserMax = axis.userMax;
7725
 
7726
			// Mark as dirty if it is not already set to dirty and extremes have changed. #595.
7727
			if (!axis.isDirty) {
7728
				axis.isDirty = isDirtyAxisLength || axis.min !== axis.oldMin || axis.max !== axis.oldMax;
7729
			}
7730
		} else if (axis.cleanStacks) {
7731
			axis.cleanStacks();
7732
		}
7733
	},
7734
 
7735
	/**
7736
	 * Set the extremes and optionally redraw
7737
	 * @param {Number} newMin
7738
	 * @param {Number} newMax
7739
	 * @param {Boolean} redraw
7740
	 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
7741
	 *    configuration
7742
	 * @param {Object} eventArguments
7743
	 *
7744
	 */
7745
	setExtremes: function (newMin, newMax, redraw, animation, eventArguments) {
7746
		var axis = this,
7747
			chart = axis.chart;
7748
 
7749
		redraw = pick(redraw, true); // defaults to true
7750
 
7751
		each(axis.series, function (serie) {
7752
			delete serie.kdTree;
7753
		});
7754
 
7755
		// Extend the arguments with min and max
7756
		eventArguments = extend(eventArguments, {
7757
			min: newMin,
7758
			max: newMax
7759
		});
7760
 
7761
		// Fire the event
7762
		fireEvent(axis, 'setExtremes', eventArguments, function () { // the default event handler
7763
 
7764
			axis.userMin = newMin;
7765
			axis.userMax = newMax;
7766
			axis.eventArgs = eventArguments;
7767
 
7768
			if (redraw) {
7769
				chart.redraw(animation);
7770
			}
7771
		});
7772
	},
7773
 
7774
	/**
7775
	 * Overridable method for zooming chart. Pulled out in a separate method to allow overriding
7776
	 * in stock charts.
7777
	 */
7778
	zoom: function (newMin, newMax) {
7779
		var dataMin = this.dataMin,
7780
			dataMax = this.dataMax,
7781
			options = this.options,
7782
			min = mathMin(dataMin, pick(options.min, dataMin)),
7783
			max = mathMax(dataMax, pick(options.max, dataMax));
7784
 
7785
		// Prevent pinch zooming out of range. Check for defined is for #1946. #1734.
7786
		if (!this.allowZoomOutside) {
7787
			if (defined(dataMin) && newMin <= min) {
7788
				newMin = min;
7789
			}
7790
			if (defined(dataMax) && newMax >= max) {
7791
				newMax = max;
7792
			}
7793
		}
7794
 
7795
		// In full view, displaying the reset zoom button is not required
7796
		this.displayBtn = newMin !== UNDEFINED || newMax !== UNDEFINED;
7797
 
7798
		// Do it
7799
		this.setExtremes(
7800
			newMin,
7801
			newMax,
7802
			false,
7803
			UNDEFINED,
7804
			{ trigger: 'zoom' }
7805
		);
7806
		return true;
7807
	},
7808
 
7809
	/**
7810
	 * Update the axis metrics
7811
	 */
7812
	setAxisSize: function () {
7813
		var chart = this.chart,
7814
			options = this.options,
7815
			offsetLeft = options.offsetLeft || 0,
7816
			offsetRight = options.offsetRight || 0,
7817
			horiz = this.horiz,
7818
			width = pick(options.width, chart.plotWidth - offsetLeft + offsetRight),
7819
			height = pick(options.height, chart.plotHeight),
7820
			top = pick(options.top, chart.plotTop),
7821
			left = pick(options.left, chart.plotLeft + offsetLeft),
7822
			percentRegex = /%$/;
7823
 
7824
		// Check for percentage based input values
7825
		if (percentRegex.test(height)) {
7826
			height = parseFloat(height) / 100 * chart.plotHeight;
7827
		}
7828
		if (percentRegex.test(top)) {
7829
			top = parseFloat(top) / 100 * chart.plotHeight + chart.plotTop;
7830
		}
7831
 
7832
		// Expose basic values to use in Series object and navigator
7833
		this.left = left;
7834
		this.top = top;
7835
		this.width = width;
7836
		this.height = height;
7837
		this.bottom = chart.chartHeight - height - top;
7838
		this.right = chart.chartWidth - width - left;
7839
 
7840
		// Direction agnostic properties
7841
		this.len = mathMax(horiz ? width : height, 0); // mathMax fixes #905
7842
		this.pos = horiz ? left : top; // distance from SVG origin
7843
	},
7844
 
7845
	/**
7846
	 * Get the actual axis extremes
7847
	 */
7848
	getExtremes: function () {
7849
		var axis = this,
7850
			isLog = axis.isLog;
7851
 
7852
		return {
7853
			min: isLog ? correctFloat(lin2log(axis.min)) : axis.min,
7854
			max: isLog ? correctFloat(lin2log(axis.max)) : axis.max,
7855
			dataMin: axis.dataMin,
7856
			dataMax: axis.dataMax,
7857
			userMin: axis.userMin,
7858
			userMax: axis.userMax
7859
		};
7860
	},
7861
 
7862
	/**
7863
	 * Get the zero plane either based on zero or on the min or max value.
7864
	 * Used in bar and area plots
7865
	 */
7866
	getThreshold: function (threshold) {
7867
		var axis = this,
7868
			isLog = axis.isLog,
7869
			realMin = isLog ? lin2log(axis.min) : axis.min,
7870
			realMax = isLog ? lin2log(axis.max) : axis.max;
7871
 
7872
		// With a threshold of null, make the columns/areas rise from the top or bottom
7873
		// depending on the value, assuming an actual threshold of 0 (#4233).
7874
		if (threshold === null) {
7875
			threshold = realMax < 0 ? realMax : realMin;
7876
		} else if (realMin > threshold) {
7877
			threshold = realMin;
7878
		} else if (realMax < threshold) {
7879
			threshold = realMax;
7880
		}
7881
 
7882
		return axis.translate(threshold, 0, 1, 0, 1);
7883
	},
7884
 
7885
	/**
7886
	 * Compute auto alignment for the axis label based on which side the axis is on
7887
	 * and the given rotation for the label
7888
	 */
7889
	autoLabelAlign: function (rotation) {
7890
		var ret,
7891
			angle = (pick(rotation, 0) - (this.side * 90) + 720) % 360;
7892
 
7893
		if (angle > 15 && angle < 165) {
7894
			ret = 'right';
7895
		} else if (angle > 195 && angle < 345) {
7896
			ret = 'left';
7897
		} else {
7898
			ret = 'center';
7899
		}
7900
		return ret;
7901
	},
7902
 
7903
	/**
7904
	 * Prevent the ticks from getting so close we can't draw the labels. On a horizontal
7905
	 * axis, this is handled by rotating the labels, removing ticks and adding ellipsis.
7906
	 * On a vertical axis remove ticks and add ellipsis.
7907
	 */
7908
	unsquish: function () {
7909
		var chart = this.chart,
7910
			ticks = this.ticks,
7911
			labelOptions = this.options.labels,
7912
			horiz = this.horiz,
7913
			tickInterval = this.tickInterval,
7914
			newTickInterval = tickInterval,
7915
			slotSize = this.len / (((this.categories ? 1 : 0) + this.max - this.min) / tickInterval),
7916
			rotation,
7917
			rotationOption = labelOptions.rotation,
7918
			labelMetrics = chart.renderer.fontMetrics(labelOptions.style.fontSize, ticks[0] && ticks[0].label),
7919
			step,
7920
			bestScore = Number.MAX_VALUE,
7921
			autoRotation,
7922
			// Return the multiple of tickInterval that is needed to avoid collision
7923
			getStep = function (spaceNeeded) {
7924
				var step = spaceNeeded / (slotSize || 1);
7925
				step = step > 1 ? mathCeil(step) : 1;
7926
				return step * tickInterval;
7927
			};
7928
 
7929
		if (horiz) {
7930
			autoRotation = defined(rotationOption) ?
7931
				[rotationOption] :
7932
				slotSize < pick(labelOptions.autoRotationLimit, 80) && !labelOptions.staggerLines && !labelOptions.step && labelOptions.autoRotation;
7933
 
7934
			if (autoRotation) {
7935
 
7936
				// Loop over the given autoRotation options, and determine which gives the best score. The
7937
				// best score is that with the lowest number of steps and a rotation closest to horizontal.
7938
				each(autoRotation, function (rot) {
7939
					var score;
7940
 
7941
					if (rot === rotationOption || (rot && rot >= -90 && rot <= 90)) { // #3891
7942
 
7943
						step = getStep(mathAbs(labelMetrics.h / mathSin(deg2rad * rot)));
7944
 
7945
						score = step + mathAbs(rot / 360);
7946
 
7947
						if (score < bestScore) {
7948
							bestScore = score;
7949
							rotation = rot;
7950
							newTickInterval = step;
7951
						}
7952
					}
7953
				});
7954
			}
7955
 
7956
		} else if (!labelOptions.step) { // #4411
7957
			newTickInterval = getStep(labelMetrics.h);
7958
		}
7959
 
7960
		this.autoRotation = autoRotation;
7961
		this.labelRotation = rotation;
7962
 
7963
		return newTickInterval;
7964
	},
7965
 
7966
	renderUnsquish: function () {
7967
		var chart = this.chart,
7968
			renderer = chart.renderer,
7969
			tickPositions = this.tickPositions,
7970
			ticks = this.ticks,
7971
			labelOptions = this.options.labels,
7972
			horiz = this.horiz,
7973
			margin = chart.margin,
7974
			slotCount = this.categories ? tickPositions.length : tickPositions.length - 1,
7975
			slotWidth = this.slotWidth = (horiz && !labelOptions.step && !labelOptions.rotation &&
7976
				((this.staggerLines || 1) * chart.plotWidth) / slotCount) ||
7977
				(!horiz && ((margin[3] && (margin[3] - chart.spacing[3])) || chart.chartWidth * 0.33)), // #1580, #1931,
7978
			innerWidth = mathMax(1, mathRound(slotWidth - 2 * (labelOptions.padding || 5))),
7979
			attr = {},
7980
			labelMetrics = renderer.fontMetrics(labelOptions.style.fontSize, ticks[0] && ticks[0].label),
7981
			textOverflowOption = labelOptions.style.textOverflow,
7982
			css,
7983
			labelLength = 0,
7984
			label,
7985
			i,
7986
			pos;
7987
 
7988
		// Set rotation option unless it is "auto", like in gauges
7989
		if (!isString(labelOptions.rotation)) {
7990
			attr.rotation = labelOptions.rotation || 0; // #4443
7991
		}
7992
 
7993
		// Handle auto rotation on horizontal axis
7994
		if (this.autoRotation) {
7995
 
7996
			// Get the longest label length
7997
			each(tickPositions, function (tick) {
7998
				tick = ticks[tick];
7999
				if (tick && tick.labelLength > labelLength) {
8000
					labelLength = tick.labelLength;
8001
				}
8002
			});
8003
 
8004
			// Apply rotation only if the label is too wide for the slot, and
8005
			// the label is wider than its height.
8006
			if (labelLength > innerWidth && labelLength > labelMetrics.h) {
8007
				attr.rotation = this.labelRotation;
8008
			} else {
8009
				this.labelRotation = 0;
8010
			}
8011
 
8012
		// Handle word-wrap or ellipsis on vertical axis
8013
		} else if (slotWidth) {
8014
			// For word-wrap or ellipsis
8015
			css = { width: innerWidth + PX };
8016
 
8017
			if (!textOverflowOption) {
8018
				css.textOverflow = 'clip';
8019
 
8020
				// On vertical axis, only allow word wrap if there is room for more lines.
8021
				i = tickPositions.length;
8022
				while (!horiz && i--) {
8023
					pos = tickPositions[i];
8024
					label = ticks[pos].label;
8025
					if (label) {
8026
						// Reset ellipsis in order to get the correct bounding box (#4070)
8027
						if (label.styles.textOverflow === 'ellipsis') {
8028
							label.css({ textOverflow: 'clip' });
8029
						}
8030
						if (label.getBBox().height > this.len / tickPositions.length - (labelMetrics.h - labelMetrics.f)) {
8031
							label.specCss = { textOverflow: 'ellipsis' };
8032
						}
8033
					}
8034
				}
8035
			}
8036
		}
8037
 
8038
 
8039
		// Add ellipsis if the label length is significantly longer than ideal
8040
		if (attr.rotation) {
8041
			css = {
8042
				width: (labelLength > chart.chartHeight * 0.5 ? chart.chartHeight * 0.33 : chart.chartHeight) + PX
8043
			};
8044
			if (!textOverflowOption) {
8045
				css.textOverflow = 'ellipsis';
8046
			}
8047
		}
8048
 
8049
		// Set the explicit or automatic label alignment
8050
		this.labelAlign = attr.align = labelOptions.align || this.autoLabelAlign(this.labelRotation);
8051
 
8052
		// Apply general and specific CSS
8053
		each(tickPositions, function (pos) {
8054
			var tick = ticks[pos],
8055
				label = tick && tick.label;
8056
			if (label) {
8057
				if (css) {
8058
					label.css(merge(css, label.specCss));
8059
				}
8060
				delete label.specCss;
8061
				label.attr(attr);
8062
				tick.rotation = attr.rotation;
8063
			}
8064
		});
8065
 
8066
		// TODO: Why not part of getLabelPosition?
8067
		this.tickRotCorr = renderer.rotCorr(labelMetrics.b, this.labelRotation || 0, this.side === 2);
8068
	},
8069
 
8070
	/**
8071
	 * Return true if the axis has associated data
8072
	 */
8073
	hasData: function () {
8074
		return this.hasVisibleSeries || (defined(this.min) && defined(this.max) && !!this.tickPositions);
8075
	},
8076
 
8077
	/**
8078
	 * Render the tick labels to a preliminary position to get their sizes
8079
	 */
8080
	getOffset: function () {
8081
		var axis = this,
8082
			chart = axis.chart,
8083
			renderer = chart.renderer,
8084
			options = axis.options,
8085
			tickPositions = axis.tickPositions,
8086
			ticks = axis.ticks,
8087
			horiz = axis.horiz,
8088
			side = axis.side,
8089
			invertedSide = chart.inverted ? [1, 0, 3, 2][side] : side,
8090
			hasData,
8091
			showAxis,
8092
			titleOffset = 0,
8093
			titleOffsetOption,
8094
			titleMargin = 0,
8095
			axisTitleOptions = options.title,
8096
			labelOptions = options.labels,
8097
			labelOffset = 0, // reset
8098
			labelOffsetPadded,
8099
			axisOffset = chart.axisOffset,
8100
			clipOffset = chart.clipOffset,
8101
			clip,
8102
			directionFactor = [-1, 1, 1, -1][side],
8103
			n,
8104
			lineHeightCorrection;
8105
 
8106
		// For reuse in Axis.render
8107
		hasData = axis.hasData();
8108
		axis.showAxis = showAxis = hasData || pick(options.showEmpty, true);
8109
 
8110
		// Set/reset staggerLines
8111
		axis.staggerLines = axis.horiz && labelOptions.staggerLines;
8112
 
8113
		// Create the axisGroup and gridGroup elements on first iteration
8114
		if (!axis.axisGroup) {
8115
			axis.gridGroup = renderer.g('grid')
8116
				.attr({ zIndex: options.gridZIndex || 1 })
8117
				.add();
8118
			axis.axisGroup = renderer.g('axis')
8119
				.attr({ zIndex: options.zIndex || 2 })
8120
				.add();
8121
			axis.labelGroup = renderer.g('axis-labels')
8122
				.attr({ zIndex: labelOptions.zIndex || 7 })
8123
				.addClass(PREFIX + axis.coll.toLowerCase() + '-labels')
8124
				.add();
8125
		}
8126
 
8127
		if (hasData || axis.isLinked) {
8128
 
8129
			// Generate ticks
8130
			each(tickPositions, function (pos) {
8131
				if (!ticks[pos]) {
8132
					ticks[pos] = new Tick(axis, pos);
8133
				} else {
8134
					ticks[pos].addLabel(); // update labels depending on tick interval
8135
				}
8136
			});
8137
 
8138
			axis.renderUnsquish();
8139
 
8140
			each(tickPositions, function (pos) {
8141
				// left side must be align: right and right side must have align: left for labels
8142
				if (side === 0 || side === 2 || { 1: 'left', 3: 'right' }[side] === axis.labelAlign) {
8143
 
8144
					// get the highest offset
8145
					labelOffset = mathMax(
8146
						ticks[pos].getLabelSize(),
8147
						labelOffset
8148
					);
8149
				}
8150
			});
8151
 
8152
			if (axis.staggerLines) {
8153
				labelOffset *= axis.staggerLines;
8154
				axis.labelOffset = labelOffset;
8155
			}
8156
 
8157
 
8158
		} else { // doesn't have data
8159
			for (n in ticks) {
8160
				ticks[n].destroy();
8161
				delete ticks[n];
8162
			}
8163
		}
8164
 
8165
		if (axisTitleOptions && axisTitleOptions.text && axisTitleOptions.enabled !== false) {
8166
			if (!axis.axisTitle) {
8167
				axis.axisTitle = renderer.text(
8168
					axisTitleOptions.text,
8169
					0,
8170
					0,
8171
					axisTitleOptions.useHTML
8172
				)
8173
				.attr({
8174
					zIndex: 7,
8175
					rotation: axisTitleOptions.rotation || 0,
8176
					align:
8177
						axisTitleOptions.textAlign ||
8178
						{ low: 'left', middle: 'center', high: 'right' }[axisTitleOptions.align]
8179
				})
8180
				.addClass(PREFIX + this.coll.toLowerCase() + '-title')
8181
				.css(axisTitleOptions.style)
8182
				.add(axis.axisGroup);
8183
				axis.axisTitle.isNew = true;
8184
			}
8185
 
8186
			if (showAxis) {
8187
				titleOffset = axis.axisTitle.getBBox()[horiz ? 'height' : 'width'];
8188
				titleOffsetOption = axisTitleOptions.offset;
8189
				titleMargin = defined(titleOffsetOption) ? 0 : pick(axisTitleOptions.margin, horiz ? 5 : 10);
8190
			}
8191
 
8192
			// hide or show the title depending on whether showEmpty is set
8193
			axis.axisTitle[showAxis ? 'show' : 'hide']();
8194
		}
8195
 
8196
		// handle automatic or user set offset
8197
		axis.offset = directionFactor * pick(options.offset, axisOffset[side]);
8198
 
8199
		axis.tickRotCorr = axis.tickRotCorr || { x: 0, y: 0 }; // polar
8200
		lineHeightCorrection = side === 2 ? axis.tickRotCorr.y : 0;
8201
		labelOffsetPadded = labelOffset + titleMargin +
8202
			(labelOffset && (directionFactor * (horiz ? pick(labelOptions.y, axis.tickRotCorr.y + 8) : labelOptions.x) - lineHeightCorrection));
8203
		axis.axisTitleMargin = pick(titleOffsetOption, labelOffsetPadded);
8204
 
8205
		axisOffset[side] = mathMax(
8206
			axisOffset[side],
8207
			axis.axisTitleMargin + titleOffset + directionFactor * axis.offset,
8208
			labelOffsetPadded // #3027
8209
		);
8210
 
8211
		// Decide the clipping needed to keep the graph inside the plot area and axis lines
8212
		clip = options.offset ? 0 : mathFloor(options.lineWidth / 2) * 2; // #4308, #4371
8213
		clipOffset[invertedSide] = mathMax(clipOffset[invertedSide], clip);
8214
	},
8215
 
8216
	/**
8217
	 * Get the path for the axis line
8218
	 */
8219
	getLinePath: function (lineWidth) {
8220
		var chart = this.chart,
8221
			opposite = this.opposite,
8222
			offset = this.offset,
8223
			horiz = this.horiz,
8224
			lineLeft = this.left + (opposite ? this.width : 0) + offset,
8225
			lineTop = chart.chartHeight - this.bottom - (opposite ? this.height : 0) + offset;
8226
 
8227
		if (opposite) {
8228
			lineWidth *= -1; // crispify the other way - #1480, #1687
8229
		}
8230
 
8231
		return chart.renderer.crispLine([
8232
				M,
8233
				horiz ?
8234
					this.left :
8235
					lineLeft,
8236
				horiz ?
8237
					lineTop :
8238
					this.top,
8239
				L,
8240
				horiz ?
8241
					chart.chartWidth - this.right :
8242
					lineLeft,
8243
				horiz ?
8244
					lineTop :
8245
					chart.chartHeight - this.bottom
8246
			], lineWidth);
8247
	},
8248
 
8249
	/**
8250
	 * Position the title
8251
	 */
8252
	getTitlePosition: function () {
8253
		// compute anchor points for each of the title align options
8254
		var horiz = this.horiz,
8255
			axisLeft = this.left,
8256
			axisTop = this.top,
8257
			axisLength = this.len,
8258
			axisTitleOptions = this.options.title,
8259
			margin = horiz ? axisLeft : axisTop,
8260
			opposite = this.opposite,
8261
			offset = this.offset,
8262
			xOption = axisTitleOptions.x || 0,
8263
			yOption = axisTitleOptions.y || 0,
8264
			fontSize = pInt(axisTitleOptions.style.fontSize || 12),
8265
 
8266
			// the position in the length direction of the axis
8267
			alongAxis = {
8268
				low: margin + (horiz ? 0 : axisLength),
8269
				middle: margin + axisLength / 2,
8270
				high: margin + (horiz ? axisLength : 0)
8271
			}[axisTitleOptions.align],
8272
 
8273
			// the position in the perpendicular direction of the axis
8274
			offAxis = (horiz ? axisTop + this.height : axisLeft) +
8275
				(horiz ? 1 : -1) * // horizontal axis reverses the margin
8276
				(opposite ? -1 : 1) * // so does opposite axes
8277
				this.axisTitleMargin +
8278
				(this.side === 2 ? fontSize : 0);
8279
 
8280
		return {
8281
			x: horiz ?
8282
				alongAxis + xOption :
8283
				offAxis + (opposite ? this.width : 0) + offset + xOption,
8284
			y: horiz ?
8285
				offAxis + yOption - (opposite ? this.height : 0) + offset :
8286
				alongAxis + yOption
8287
		};
8288
	},
8289
 
8290
	/**
8291
	 * Render the axis
8292
	 */
8293
	render: function () {
8294
		var axis = this,
8295
			chart = axis.chart,
8296
			renderer = chart.renderer,
8297
			options = axis.options,
8298
			isLog = axis.isLog,
8299
			isLinked = axis.isLinked,
8300
			tickPositions = axis.tickPositions,
8301
			axisTitle = axis.axisTitle,
8302
			ticks = axis.ticks,
8303
			minorTicks = axis.minorTicks,
8304
			alternateBands = axis.alternateBands,
8305
			stackLabelOptions = options.stackLabels,
8306
			alternateGridColor = options.alternateGridColor,
8307
			tickmarkOffset = axis.tickmarkOffset,
8308
			lineWidth = options.lineWidth,
8309
			linePath,
8310
			hasRendered = chart.hasRendered,
8311
			slideInTicks = hasRendered && defined(axis.oldMin) && !isNaN(axis.oldMin),
8312
			showAxis = axis.showAxis,
8313
			globalAnimation = renderer.globalAnimation,
8314
			from,
8315
			to;
8316
 
8317
		// Reset
8318
		axis.labelEdge.length = 0;
8319
		//axis.justifyToPlot = overflow === 'justify';
8320
		axis.overlap = false;
8321
 
8322
		// Mark all elements inActive before we go over and mark the active ones
8323
		each([ticks, minorTicks, alternateBands], function (coll) {
8324
			var pos;
8325
			for (pos in coll) {
8326
				coll[pos].isActive = false;
8327
			}
8328
		});
8329
 
8330
		// If the series has data draw the ticks. Else only the line and title
8331
		if (axis.hasData() || isLinked) {
8332
 
8333
			// minor ticks
8334
			if (axis.minorTickInterval && !axis.categories) {
8335
				each(axis.getMinorTickPositions(), function (pos) {
8336
					if (!minorTicks[pos]) {
8337
						minorTicks[pos] = new Tick(axis, pos, 'minor');
8338
					}
8339
 
8340
					// render new ticks in old position
8341
					if (slideInTicks && minorTicks[pos].isNew) {
8342
						minorTicks[pos].render(null, true);
8343
					}
8344
 
8345
					minorTicks[pos].render(null, false, 1);
8346
				});
8347
			}
8348
 
8349
			// Major ticks. Pull out the first item and render it last so that
8350
			// we can get the position of the neighbour label. #808.
8351
			if (tickPositions.length) { // #1300
8352
				each(tickPositions, function (pos, i) {
8353
 
8354
					// linked axes need an extra check to find out if
8355
					if (!isLinked || (pos >= axis.min && pos <= axis.max)) {
8356
 
8357
						if (!ticks[pos]) {
8358
							ticks[pos] = new Tick(axis, pos);
8359
						}
8360
 
8361
						// render new ticks in old position
8362
						if (slideInTicks && ticks[pos].isNew) {
8363
							ticks[pos].render(i, true, 0.1);
8364
						}
8365
 
8366
						ticks[pos].render(i);
8367
					}
8368
 
8369
				});
8370
				// In a categorized axis, the tick marks are displayed between labels. So
8371
				// we need to add a tick mark and grid line at the left edge of the X axis.
8372
				if (tickmarkOffset && (axis.min === 0 || axis.single)) {
8373
					if (!ticks[-1]) {
8374
						ticks[-1] = new Tick(axis, -1, null, true);
8375
					}
8376
					ticks[-1].render(-1);
8377
				}
8378
 
8379
			}
8380
 
8381
			// alternate grid color
8382
			if (alternateGridColor) {
8383
				each(tickPositions, function (pos, i) {
8384
					if (i % 2 === 0 && pos < axis.max) {
8385
						if (!alternateBands[pos]) {
8386
							alternateBands[pos] = new Highcharts.PlotLineOrBand(axis);
8387
						}
8388
						from = pos + tickmarkOffset; // #949
8389
						to = tickPositions[i + 1] !== UNDEFINED ? tickPositions[i + 1] + tickmarkOffset : axis.max;
8390
						alternateBands[pos].options = {
8391
							from: isLog ? lin2log(from) : from,
8392
							to: isLog ? lin2log(to) : to,
8393
							color: alternateGridColor
8394
						};
8395
						alternateBands[pos].render();
8396
						alternateBands[pos].isActive = true;
8397
					}
8398
				});
8399
			}
8400
 
8401
			// custom plot lines and bands
8402
			if (!axis._addedPlotLB) { // only first time
8403
				each((options.plotLines || []).concat(options.plotBands || []), function (plotLineOptions) {
8404
					axis.addPlotBandOrLine(plotLineOptions);
8405
				});
8406
				axis._addedPlotLB = true;
8407
			}
8408
 
8409
		} // end if hasData
8410
 
8411
		// Remove inactive ticks
8412
		each([ticks, minorTicks, alternateBands], function (coll) {
8413
			var pos,
8414
				i,
8415
				forDestruction = [],
8416
				delay = globalAnimation ? globalAnimation.duration || 500 : 0,
8417
				destroyInactiveItems = function () {
8418
					i = forDestruction.length;
8419
					while (i--) {
8420
						// When resizing rapidly, the same items may be destroyed in different timeouts,
8421
						// or the may be reactivated
8422
						if (coll[forDestruction[i]] && !coll[forDestruction[i]].isActive) {
8423
							coll[forDestruction[i]].destroy();
8424
							delete coll[forDestruction[i]];
8425
						}
8426
					}
8427
 
8428
				};
8429
 
8430
			for (pos in coll) {
8431
 
8432
				if (!coll[pos].isActive) {
8433
					// Render to zero opacity
8434
					coll[pos].render(pos, false, 0);
8435
					coll[pos].isActive = false;
8436
					forDestruction.push(pos);
8437
				}
8438
			}
8439
 
8440
			// When the objects are finished fading out, destroy them
8441
			if (coll === alternateBands || !chart.hasRendered || !delay) {
8442
				destroyInactiveItems();
8443
			} else if (delay) {
8444
				setTimeout(destroyInactiveItems, delay);
8445
			}
8446
		});
8447
 
8448
		// Static items. As the axis group is cleared on subsequent calls
8449
		// to render, these items are added outside the group.
8450
		// axis line
8451
		if (lineWidth) {
8452
			linePath = axis.getLinePath(lineWidth);
8453
			if (!axis.axisLine) {
8454
				axis.axisLine = renderer.path(linePath)
8455
					.attr({
8456
						stroke: options.lineColor,
8457
						'stroke-width': lineWidth,
8458
						zIndex: 7
8459
					})
8460
					.add(axis.axisGroup);
8461
			} else {
8462
				axis.axisLine.animate({ d: linePath });
8463
			}
8464
 
8465
			// show or hide the line depending on options.showEmpty
8466
			axis.axisLine[showAxis ? 'show' : 'hide']();
8467
		}
8468
 
8469
		if (axisTitle && showAxis) {
8470
 
8471
			axisTitle[axisTitle.isNew ? 'attr' : 'animate'](
8472
				axis.getTitlePosition()
8473
			);
8474
			axisTitle.isNew = false;
8475
		}
8476
 
8477
		// Stacked totals:
8478
		if (stackLabelOptions && stackLabelOptions.enabled) {
8479
			axis.renderStackTotals();
8480
		}
8481
		// End stacked totals
8482
 
8483
		axis.isDirty = false;
8484
	},
8485
 
8486
	/**
8487
	 * Redraw the axis to reflect changes in the data or axis extremes
8488
	 */
8489
	redraw: function () {
8490
 
8491
		// render the axis
8492
		this.render();
8493
 
8494
		// move plot lines and bands
8495
		each(this.plotLinesAndBands, function (plotLine) {
8496
			plotLine.render();
8497
		});
8498
 
8499
		// mark associated series as dirty and ready for redraw
8500
		each(this.series, function (series) {
8501
			series.isDirty = true;
8502
		});
8503
 
8504
	},
8505
 
8506
	/**
8507
	 * Destroys an Axis instance.
8508
	 */
8509
	destroy: function (keepEvents) {
8510
		var axis = this,
8511
			stacks = axis.stacks,
8512
			stackKey,
8513
			plotLinesAndBands = axis.plotLinesAndBands,
8514
			i;
8515
 
8516
		// Remove the events
8517
		if (!keepEvents) {
8518
			removeEvent(axis);
8519
		}
8520
 
8521
		// Destroy each stack total
8522
		for (stackKey in stacks) {
8523
			destroyObjectProperties(stacks[stackKey]);
8524
 
8525
			stacks[stackKey] = null;
8526
		}
8527
 
8528
		// Destroy collections
8529
		each([axis.ticks, axis.minorTicks, axis.alternateBands], function (coll) {
8530
			destroyObjectProperties(coll);
8531
		});
8532
		i = plotLinesAndBands.length;
8533
		while (i--) { // #1975
8534
			plotLinesAndBands[i].destroy();
8535
		}
8536
 
8537
		// Destroy local variables
8538
		each(['stackTotalGroup', 'axisLine', 'axisTitle', 'axisGroup', 'cross', 'gridGroup', 'labelGroup'], function (prop) {
8539
			if (axis[prop]) {
8540
				axis[prop] = axis[prop].destroy();
8541
			}
8542
		});
8543
 
8544
		// Destroy crosshair
8545
		if (this.cross) {
8546
			this.cross.destroy();
8547
		}
8548
	},
8549
 
8550
	/**
8551
	 * Draw the crosshair
8552
	 */
8553
	drawCrosshair: function (e, point) { // docs: Missing docs for Axis.crosshair. Also for properties.
8554
 
8555
		var path,
8556
			options = this.crosshair,
8557
			animation = options.animation,
8558
			pos,
8559
			attribs,
8560
			categorized;
8561
 
8562
		if (
8563
			// Disabled in options
8564
			!this.crosshair ||
8565
			// Snap
8566
			((defined(point) || !pick(this.crosshair.snap, true)) === false) ||
8567
			// Not on this axis (#4095, #2888)
8568
			(point && point.series && point.series[this.coll] !== this)
8569
		) {
8570
			this.hideCrosshair();
8571
 
8572
		} else {
8573
 
8574
			// Get the path
8575
			if (!pick(options.snap, true)) {
8576
				pos = (this.horiz ? e.chartX - this.pos : this.len - e.chartY + this.pos);
8577
			} else if (defined(point)) {
8578
				/*jslint eqeq: true*/
8579
				pos = this.isXAxis ? point.plotX : this.len - point.plotY; // #3834
8580
				/*jslint eqeq: false*/
8581
			}
8582
 
8583
			if (this.isRadial) {
8584
				path = this.getPlotLinePath(this.isXAxis ? point.x : pick(point.stackY, point.y)) || null; // #3189
8585
			} else {
8586
				path = this.getPlotLinePath(null, null, null, null, pos) || null; // #3189
8587
			}
8588
 
8589
			if (path === null) {
8590
				this.hideCrosshair();
8591
				return;
8592
			}
8593
 
8594
			// Draw the cross
8595
			if (this.cross) {
8596
				this.cross
8597
					.attr({ visibility: VISIBLE })[animation ? 'animate' : 'attr']({ d: path }, animation);
8598
			} else {
8599
				categorized = this.categories && !this.isRadial;
8600
				attribs = {
8601
					'stroke-width': options.width || (categorized ? this.transA : 1),
8602
					stroke: options.color || (categorized ? 'rgba(155,200,255,0.2)' : '#C0C0C0'),
8603
					zIndex: options.zIndex || 2
8604
				};
8605
				if (options.dashStyle) {
8606
					attribs.dashstyle = options.dashStyle;
8607
				}
8608
				this.cross = this.chart.renderer.path(path).attr(attribs).add();
8609
			}
8610
 
8611
		}
8612
 
8613
	},
8614
 
8615
	/**
8616
	 *	Hide the crosshair.
8617
	 */
8618
	hideCrosshair: function () {
8619
		if (this.cross) {
8620
			this.cross.hide();
8621
		}
8622
	}
8623
}; // end Axis
8624
 
8625
extend(Axis.prototype, AxisPlotLineOrBandExtension);
8626
 
8627
/**
8628
 * Set the tick positions to a time unit that makes sense, for example
8629
 * on the first of each month or on every Monday. Return an array
8630
 * with the time positions. Used in datetime axes as well as for grouping
8631
 * data on a datetime axis.
8632
 *
8633
 * @param {Object} normalizedInterval The interval in axis values (ms) and the count
8634
 * @param {Number} min The minimum in axis values
8635
 * @param {Number} max The maximum in axis values
8636
 * @param {Number} startOfWeek
8637
 */
8638
Axis.prototype.getTimeTicks = function (normalizedInterval, min, max, startOfWeek) {
8639
	var tickPositions = [],
8640
		i,
8641
		higherRanks = {},
8642
		useUTC = defaultOptions.global.useUTC,
8643
		minYear, // used in months and years as a basis for Date.UTC()
8644
		minDate = new Date(min - getTZOffset(min)),
8645
		interval = normalizedInterval.unitRange,
8646
		count = normalizedInterval.count;
8647
 
8648
	if (defined(min)) { // #1300
8649
		minDate[setMilliseconds](interval >= timeUnits.second ? 0 : // #3935
8650
			count * mathFloor(minDate.getMilliseconds() / count)); // #3652, #3654
8651
 
8652
		if (interval >= timeUnits.second) { // second
8653
			minDate[setSeconds](interval >= timeUnits.minute ? 0 : // #3935
8654
				count * mathFloor(minDate.getSeconds() / count));
8655
		}
8656
 
8657
		if (interval >= timeUnits.minute) { // minute
8658
			minDate[setMinutes](interval >= timeUnits.hour ? 0 :
8659
				count * mathFloor(minDate[getMinutes]() / count));
8660
		}
8661
 
8662
		if (interval >= timeUnits.hour) { // hour
8663
			minDate[setHours](interval >= timeUnits.day ? 0 :
8664
				count * mathFloor(minDate[getHours]() / count));
8665
		}
8666
 
8667
		if (interval >= timeUnits.day) { // day
8668
			minDate[setDate](interval >= timeUnits.month ? 1 :
8669
				count * mathFloor(minDate[getDate]() / count));
8670
		}
8671
 
8672
		if (interval >= timeUnits.month) { // month
8673
			minDate[setMonth](interval >= timeUnits.year ? 0 :
8674
				count * mathFloor(minDate[getMonth]() / count));
8675
			minYear = minDate[getFullYear]();
8676
		}
8677
 
8678
		if (interval >= timeUnits.year) { // year
8679
			minYear -= minYear % count;
8680
			minDate[setFullYear](minYear);
8681
		}
8682
 
8683
		// week is a special case that runs outside the hierarchy
8684
		if (interval === timeUnits.week) {
8685
			// get start of current week, independent of count
8686
			minDate[setDate](minDate[getDate]() - minDate[getDay]() +
8687
				pick(startOfWeek, 1));
8688
		}
8689
 
8690
 
8691
		// get tick positions
8692
		i = 1;
8693
		if (timezoneOffset || getTimezoneOffset) {
8694
			minDate = minDate.getTime();
8695
			minDate = new Date(minDate + getTZOffset(minDate));
8696
		}
8697
		minYear = minDate[getFullYear]();
8698
		var time = minDate.getTime(),
8699
			minMonth = minDate[getMonth](),
8700
			minDateDate = minDate[getDate](),
8701
			localTimezoneOffset = (timeUnits.day +
8702
					(useUTC ? getTZOffset(minDate) : minDate.getTimezoneOffset() * 60 * 1000)
8703
				) % timeUnits.day; // #950, #3359
8704
 
8705
		// iterate and add tick positions at appropriate values
8706
		while (time < max) {
8707
			tickPositions.push(time);
8708
 
8709
			// if the interval is years, use Date.UTC to increase years
8710
			if (interval === timeUnits.year) {
8711
				time = makeTime(minYear + i * count, 0);
8712
 
8713
			// if the interval is months, use Date.UTC to increase months
8714
			} else if (interval === timeUnits.month) {
8715
				time = makeTime(minYear, minMonth + i * count);
8716
 
8717
			// if we're using global time, the interval is not fixed as it jumps
8718
			// one hour at the DST crossover
8719
			} else if (!useUTC && (interval === timeUnits.day || interval === timeUnits.week)) {
8720
				time = makeTime(minYear, minMonth, minDateDate +
8721
					i * count * (interval === timeUnits.day ? 1 : 7));
8722
 
8723
			// else, the interval is fixed and we use simple addition
8724
			} else {
8725
				time += interval * count;
8726
			}
8727
 
8728
			i++;
8729
		}
8730
 
8731
		// push the last time
8732
		tickPositions.push(time);
8733
 
8734
 
8735
		// mark new days if the time is dividible by day (#1649, #1760)
8736
		each(grep(tickPositions, function (time) {
8737
			return interval <= timeUnits.hour && time % timeUnits.day === localTimezoneOffset;
8738
		}), function (time) {
8739
			higherRanks[time] = 'day';
8740
		});
8741
	}
8742
 
8743
 
8744
	// record information on the chosen unit - for dynamic label formatter
8745
	tickPositions.info = extend(normalizedInterval, {
8746
		higherRanks: higherRanks,
8747
		totalRange: interval * count
8748
	});
8749
 
8750
	return tickPositions;
8751
};
8752
 
8753
/**
8754
 * Get a normalized tick interval for dates. Returns a configuration object with
8755
 * unit range (interval), count and name. Used to prepare data for getTimeTicks.
8756
 * Previously this logic was part of getTimeTicks, but as getTimeTicks now runs
8757
 * of segments in stock charts, the normalizing logic was extracted in order to
8758
 * prevent it for running over again for each segment having the same interval.
8759
 * #662, #697.
8760
 */
8761
Axis.prototype.normalizeTimeTickInterval = function (tickInterval, unitsOption) {
8762
	var units = unitsOption || [[
8763
				'millisecond', // unit name
8764
				[1, 2, 5, 10, 20, 25, 50, 100, 200, 500] // allowed multiples
8765
			], [
8766
				'second',
8767
				[1, 2, 5, 10, 15, 30]
8768
			], [
8769
				'minute',
8770
				[1, 2, 5, 10, 15, 30]
8771
			], [
8772
				'hour',
8773
				[1, 2, 3, 4, 6, 8, 12]
8774
			], [
8775
				'day',
8776
				[1, 2]
8777
			], [
8778
				'week',
8779
				[1, 2]
8780
			], [
8781
				'month',
8782
				[1, 2, 3, 4, 6]
8783
			], [
8784
				'year',
8785
				null
8786
			]],
8787
		unit = units[units.length - 1], // default unit is years
8788
		interval = timeUnits[unit[0]],
8789
		multiples = unit[1],
8790
		count,
8791
		i;
8792
 
8793
	// loop through the units to find the one that best fits the tickInterval
8794
	for (i = 0; i < units.length; i++) {
8795
		unit = units[i];
8796
		interval = timeUnits[unit[0]];
8797
		multiples = unit[1];
8798
 
8799
 
8800
		if (units[i + 1]) {
8801
			// lessThan is in the middle between the highest multiple and the next unit.
8802
			var lessThan = (interval * multiples[multiples.length - 1] +
8803
						timeUnits[units[i + 1][0]]) / 2;
8804
 
8805
			// break and keep the current unit
8806
			if (tickInterval <= lessThan) {
8807
				break;
8808
			}
8809
		}
8810
	}
8811
 
8812
	// prevent 2.5 years intervals, though 25, 250 etc. are allowed
8813
	if (interval === timeUnits.year && tickInterval < 5 * interval) {
8814
		multiples = [1, 2, 5];
8815
	}
8816
 
8817
	// get the count
8818
	count = normalizeTickInterval(
8819
		tickInterval / interval,
8820
		multiples,
8821
		unit[0] === 'year' ? mathMax(getMagnitude(tickInterval / interval), 1) : 1 // #1913, #2360
8822
	);
8823
 
8824
	return {
8825
		unitRange: interval,
8826
		count: count,
8827
		unitName: unit[0]
8828
	};
8829
};/**
8830
 * Methods defined on the Axis prototype
8831
 */
8832
 
8833
/**
8834
 * Set the tick positions of a logarithmic axis
8835
 */
8836
Axis.prototype.getLogTickPositions = function (interval, min, max, minor) {
8837
	var axis = this,
8838
		options = axis.options,
8839
		axisLength = axis.len,
8840
		// Since we use this method for both major and minor ticks,
8841
		// use a local variable and return the result
8842
		positions = [];
8843
 
8844
	// Reset
8845
	if (!minor) {
8846
		axis._minorAutoInterval = null;
8847
	}
8848
 
8849
	// First case: All ticks fall on whole logarithms: 1, 10, 100 etc.
8850
	if (interval >= 0.5) {
8851
		interval = mathRound(interval);
8852
		positions = axis.getLinearTickPositions(interval, min, max);
8853
 
8854
	// Second case: We need intermediary ticks. For example
8855
	// 1, 2, 4, 6, 8, 10, 20, 40 etc.
8856
	} else if (interval >= 0.08) {
8857
		var roundedMin = mathFloor(min),
8858
			intermediate,
8859
			i,
8860
			j,
8861
			len,
8862
			pos,
8863
			lastPos,
8864
			break2;
8865
 
8866
		if (interval > 0.3) {
8867
			intermediate = [1, 2, 4];
8868
		} else if (interval > 0.15) { // 0.2 equals five minor ticks per 1, 10, 100 etc
8869
			intermediate = [1, 2, 4, 6, 8];
8870
		} else { // 0.1 equals ten minor ticks per 1, 10, 100 etc
8871
			intermediate = [1, 2, 3, 4, 5, 6, 7, 8, 9];
8872
		}
8873
 
8874
		for (i = roundedMin; i < max + 1 && !break2; i++) {
8875
			len = intermediate.length;
8876
			for (j = 0; j < len && !break2; j++) {
8877
				pos = log2lin(lin2log(i) * intermediate[j]);
8878
				if (pos > min && (!minor || lastPos <= max) && lastPos !== UNDEFINED) { // #1670, lastPos is #3113
8879
					positions.push(lastPos);
8880
				}
8881
 
8882
				if (lastPos > max) {
8883
					break2 = true;
8884
				}
8885
				lastPos = pos;
8886
			}
8887
		}
8888
 
8889
	// Third case: We are so deep in between whole logarithmic values that
8890
	// we might as well handle the tick positions like a linear axis. For
8891
	// example 1.01, 1.02, 1.03, 1.04.
8892
	} else {
8893
		var realMin = lin2log(min),
8894
			realMax = lin2log(max),
8895
			tickIntervalOption = options[minor ? 'minorTickInterval' : 'tickInterval'],
8896
			filteredTickIntervalOption = tickIntervalOption === 'auto' ? null : tickIntervalOption,
8897
			tickPixelIntervalOption = options.tickPixelInterval / (minor ? 5 : 1),
8898
			totalPixelLength = minor ? axisLength / axis.tickPositions.length : axisLength;
8899
 
8900
		interval = pick(
8901
			filteredTickIntervalOption,
8902
			axis._minorAutoInterval,
8903
			(realMax - realMin) * tickPixelIntervalOption / (totalPixelLength || 1)
8904
		);
8905
 
8906
		interval = normalizeTickInterval(
8907
			interval,
8908
			null,
8909
			getMagnitude(interval)
8910
		);
8911
 
8912
		positions = map(axis.getLinearTickPositions(
8913
			interval,
8914
			realMin,
8915
			realMax
8916
		), log2lin);
8917
 
8918
		if (!minor) {
8919
			axis._minorAutoInterval = interval / 5;
8920
		}
8921
	}
8922
 
8923
	// Set the axis-level tickInterval variable
8924
	if (!minor) {
8925
		axis.tickInterval = interval;
8926
	}
8927
	return positions;
8928
};/**
8929
 * The tooltip object
8930
 * @param {Object} chart The chart instance
8931
 * @param {Object} options Tooltip options
8932
 */
8933
var Tooltip = Highcharts.Tooltip = function () {
8934
	this.init.apply(this, arguments);
8935
};
8936
 
8937
Tooltip.prototype = {
8938
 
8939
	init: function (chart, options) {
8940
 
8941
		var borderWidth = options.borderWidth,
8942
			style = options.style,
8943
			padding = pInt(style.padding);
8944
 
8945
		// Save the chart and options
8946
		this.chart = chart;
8947
		this.options = options;
8948
 
8949
		// Keep track of the current series
8950
		//this.currentSeries = UNDEFINED;
8951
 
8952
		// List of crosshairs
8953
		this.crosshairs = [];
8954
 
8955
		// Current values of x and y when animating
8956
		this.now = { x: 0, y: 0 };
8957
 
8958
		// The tooltip is initially hidden
8959
		this.isHidden = true;
8960
 
8961
 
8962
		// create the label
8963
		this.label = chart.renderer.label('', 0, 0, options.shape || 'callout', null, null, options.useHTML, null, 'tooltip')
8964
			.attr({
8965
				padding: padding,
8966
				fill: options.backgroundColor,
8967
				'stroke-width': borderWidth,
8968
				r: options.borderRadius,
8969
				zIndex: 8
8970
			})
8971
			.css(style)
8972
			.css({ padding: 0 }) // Remove it from VML, the padding is applied as an attribute instead (#1117)
8973
			.add()
8974
			.attr({ y: -9999 }); // #2301, #2657
8975
 
8976
		// When using canVG the shadow shows up as a gray circle
8977
		// even if the tooltip is hidden.
8978
		if (!useCanVG) {
8979
			this.label.shadow(options.shadow);
8980
		}
8981
 
8982
		// Public property for getting the shared state.
8983
		this.shared = options.shared;
8984
	},
8985
 
8986
	/**
8987
	 * Destroy the tooltip and its elements.
8988
	 */
8989
	destroy: function () {
8990
		// Destroy and clear local variables
8991
		if (this.label) {
8992
			this.label = this.label.destroy();
8993
		}
8994
		clearTimeout(this.hideTimer);
8995
		clearTimeout(this.tooltipTimeout);
8996
	},
8997
 
8998
	/**
8999
	 * Provide a soft movement for the tooltip
9000
	 *
9001
	 * @param {Number} x
9002
	 * @param {Number} y
9003
	 * @private
9004
	 */
9005
	move: function (x, y, anchorX, anchorY) {
9006
		var tooltip = this,
9007
			now = tooltip.now,
9008
			animate = tooltip.options.animation !== false && !tooltip.isHidden &&
9009
				// When we get close to the target position, abort animation and land on the right place (#3056)
9010
				(mathAbs(x - now.x) > 1 || mathAbs(y - now.y) > 1),
9011
			skipAnchor = tooltip.followPointer || tooltip.len > 1;
9012
 
9013
		// Get intermediate values for animation
9014
		extend(now, {
9015
			x: animate ? (2 * now.x + x) / 3 : x,
9016
			y: animate ? (now.y + y) / 2 : y,
9017
			anchorX: skipAnchor ? UNDEFINED : animate ? (2 * now.anchorX + anchorX) / 3 : anchorX,
9018
			anchorY: skipAnchor ? UNDEFINED : animate ? (now.anchorY + anchorY) / 2 : anchorY
9019
		});
9020
 
9021
		// Move to the intermediate value
9022
		tooltip.label.attr(now);
9023
 
9024
 
9025
		// Run on next tick of the mouse tracker
9026
		if (animate) {
9027
 
9028
			// Never allow two timeouts
9029
			clearTimeout(this.tooltipTimeout);
9030
 
9031
			// Set the fixed interval ticking for the smooth tooltip
9032
			this.tooltipTimeout = setTimeout(function () {
9033
				// The interval function may still be running during destroy, so check that the chart is really there before calling.
9034
				if (tooltip) {
9035
					tooltip.move(x, y, anchorX, anchorY);
9036
				}
9037
			}, 32);
9038
 
9039
		}
9040
	},
9041
 
9042
	/**
9043
	 * Hide the tooltip
9044
	 */
9045
	hide: function (delay) {
9046
		var tooltip = this,
9047
			hoverPoints;
9048
 
9049
		clearTimeout(this.hideTimer); // disallow duplicate timers (#1728, #1766)
9050
		if (!this.isHidden) {
9051
			hoverPoints = this.chart.hoverPoints;
9052
 
9053
			this.hideTimer = setTimeout(function () {
9054
				tooltip.label.fadeOut();
9055
				tooltip.isHidden = true;
9056
			}, pick(delay, this.options.hideDelay, 500));
9057
		}
9058
	},
9059
 
9060
	/**
9061
	 * Extendable method to get the anchor position of the tooltip
9062
	 * from a point or set of points
9063
	 */
9064
	getAnchor: function (points, mouseEvent) {
9065
		var ret,
9066
			chart = this.chart,
9067
			inverted = chart.inverted,
9068
			plotTop = chart.plotTop,
9069
			plotLeft = chart.plotLeft,
9070
			plotX = 0,
9071
			plotY = 0,
9072
			yAxis,
9073
			xAxis;
9074
 
9075
		points = splat(points);
9076
 
9077
		// Pie uses a special tooltipPos
9078
		ret = points[0].tooltipPos;
9079
 
9080
		// When tooltip follows mouse, relate the position to the mouse
9081
		if (this.followPointer && mouseEvent) {
9082
			if (mouseEvent.chartX === UNDEFINED) {
9083
				mouseEvent = chart.pointer.normalize(mouseEvent);
9084
			}
9085
			ret = [
9086
				mouseEvent.chartX - chart.plotLeft,
9087
				mouseEvent.chartY - plotTop
9088
			];
9089
		}
9090
		// When shared, use the average position
9091
		if (!ret) {
9092
			each(points, function (point) {
9093
				yAxis = point.series.yAxis;
9094
				xAxis = point.series.xAxis;
9095
				plotX += point.plotX  + (!inverted && xAxis ? xAxis.left - plotLeft : 0);
9096
				plotY += (point.plotLow ? (point.plotLow + point.plotHigh) / 2 : point.plotY) +
9097
					(!inverted && yAxis ? yAxis.top - plotTop : 0); // #1151
9098
			});
9099
 
9100
			plotX /= points.length;
9101
			plotY /= points.length;
9102
 
9103
			ret = [
9104
				inverted ? chart.plotWidth - plotY : plotX,
9105
				this.shared && !inverted && points.length > 1 && mouseEvent ?
9106
					mouseEvent.chartY - plotTop : // place shared tooltip next to the mouse (#424)
9107
					inverted ? chart.plotHeight - plotX : plotY
9108
			];
9109
		}
9110
 
9111
		return map(ret, mathRound);
9112
	},
9113
 
9114
	/**
9115
	 * Place the tooltip in a chart without spilling over
9116
	 * and not covering the point it self.
9117
	 */
9118
	getPosition: function (boxWidth, boxHeight, point) {
9119
 
9120
		var chart = this.chart,
9121
			distance = this.distance,
9122
			ret = {},
9123
			h = point.h || 0, // #4117
9124
			swapped,
9125
			first = ['y', chart.chartHeight, boxHeight, point.plotY + chart.plotTop, chart.plotTop, chart.plotTop + chart.plotHeight],
9126
			second = ['x', chart.chartWidth, boxWidth, point.plotX + chart.plotLeft, chart.plotLeft, chart.plotLeft + chart.plotWidth],
9127
			// The far side is right or bottom
9128
			preferFarSide = pick(point.ttBelow, (chart.inverted && !point.negative) || (!chart.inverted && point.negative)),
9129
			/**
9130
			 * Handle the preferred dimension. When the preferred dimension is tooltip
9131
			 * on top or bottom of the point, it will look for space there.
9132
			 */
9133
			firstDimension = function (dim, outerSize, innerSize, point, min, max) {
9134
				var roomLeft = innerSize < point - distance,
9135
					roomRight = point + distance + innerSize < outerSize,
9136
					alignedLeft = point - distance - innerSize,
9137
					alignedRight = point + distance;
9138
 
9139
				if (preferFarSide && roomRight) {
9140
					ret[dim] = alignedRight;
9141
				} else if (!preferFarSide && roomLeft) {
9142
					ret[dim] = alignedLeft;
9143
				} else if (roomLeft) {
9144
					ret[dim] = mathMin(max - innerSize, alignedLeft - h < 0 ? alignedLeft : alignedLeft - h);
9145
				} else if (roomRight) {
9146
					ret[dim] = mathMax(min, alignedRight + h + innerSize > outerSize ? alignedRight : alignedRight + h);
9147
				} else {
9148
					return false;
9149
				}
9150
			},
9151
			/**
9152
			 * Handle the secondary dimension. If the preferred dimension is tooltip
9153
			 * on top or bottom of the point, the second dimension is to align the tooltip
9154
			 * above the point, trying to align center but allowing left or right
9155
			 * align within the chart box.
9156
			 */
9157
			secondDimension = function (dim, outerSize, innerSize, point) {
9158
				// Too close to the edge, return false and swap dimensions
9159
				if (point < distance || point > outerSize - distance) {
9160
					return false;
9161
 
9162
				// Align left/top
9163
				} else if (point < innerSize / 2) {
9164
					ret[dim] = 1;
9165
				// Align right/bottom
9166
				} else if (point > outerSize - innerSize / 2) {
9167
					ret[dim] = outerSize - innerSize - 2;
9168
				// Align center
9169
				} else {
9170
					ret[dim] = point - innerSize / 2;
9171
				}
9172
			},
9173
			/**
9174
			 * Swap the dimensions
9175
			 */
9176
			swap = function (count) {
9177
				var temp = first;
9178
				first = second;
9179
				second = temp;
9180
				swapped = count;
9181
			},
9182
			run = function () {
9183
				if (firstDimension.apply(0, first) !== false) {
9184
					if (secondDimension.apply(0, second) === false && !swapped) {
9185
						swap(true);
9186
						run();
9187
					}
9188
				} else if (!swapped) {
9189
					swap(true);
9190
					run();
9191
				} else {
9192
					ret.x = ret.y = 0;
9193
				}
9194
			};
9195
 
9196
		// Under these conditions, prefer the tooltip on the side of the point
9197
		if (chart.inverted || this.len > 1) {
9198
			swap();
9199
		}
9200
		run();
9201
 
9202
		return ret;
9203
 
9204
	},
9205
 
9206
	/**
9207
	 * In case no user defined formatter is given, this will be used. Note that the context
9208
	 * here is an object holding point, series, x, y etc.
9209
	 */
9210
	defaultFormatter: function (tooltip) {
9211
		var items = this.points || splat(this),
9212
			s;
9213
 
9214
		// build the header
9215
		s = [tooltip.tooltipFooterHeaderFormatter(items[0])]; //#3397: abstraction to enable formatting of footer and header
9216
 
9217
		// build the values
9218
		s = s.concat(tooltip.bodyFormatter(items));
9219
 
9220
		// footer
9221
		s.push(tooltip.tooltipFooterHeaderFormatter(items[0], true)); //#3397: abstraction to enable formatting of footer and header
9222
 
9223
		return s.join('');
9224
	},
9225
 
9226
	/**
9227
	 * Refresh the tooltip's text and position.
9228
	 * @param {Object} point
9229
	 */
9230
	refresh: function (point, mouseEvent) {
9231
		var tooltip = this,
9232
			chart = tooltip.chart,
9233
			label = tooltip.label,
9234
			options = tooltip.options,
9235
			x,
9236
			y,
9237
			anchor,
9238
			textConfig = {},
9239
			text,
9240
			pointConfig = [],
9241
			formatter = options.formatter || tooltip.defaultFormatter,
9242
			hoverPoints = chart.hoverPoints,
9243
			borderColor,
9244
			shared = tooltip.shared,
9245
			currentSeries;
9246
 
9247
		clearTimeout(this.hideTimer);
9248
 
9249
		// get the reference point coordinates (pie charts use tooltipPos)
9250
		tooltip.followPointer = splat(point)[0].series.tooltipOptions.followPointer;
9251
		anchor = tooltip.getAnchor(point, mouseEvent);
9252
		x = anchor[0];
9253
		y = anchor[1];
9254
 
9255
		// shared tooltip, array is sent over
9256
		if (shared && !(point.series && point.series.noSharedTooltip)) {
9257
 
9258
			// hide previous hoverPoints and set new
9259
 
9260
			chart.hoverPoints = point;
9261
			if (hoverPoints) {
9262
				each(hoverPoints, function (point) {
9263
					point.setState();
9264
				});
9265
			}
9266
 
9267
			each(point, function (item) {
9268
				item.setState(HOVER_STATE);
9269
 
9270
				pointConfig.push(item.getLabelConfig());
9271
			});
9272
 
9273
			textConfig = {
9274
				x: point[0].category,
9275
				y: point[0].y
9276
			};
9277
			textConfig.points = pointConfig;
9278
			this.len = pointConfig.length;
9279
			point = point[0];
9280
 
9281
		// single point tooltip
9282
		} else {
9283
			textConfig = point.getLabelConfig();
9284
		}
9285
		text = formatter.call(textConfig, tooltip);
9286
 
9287
		// register the current series
9288
		currentSeries = point.series;
9289
		this.distance = pick(currentSeries.tooltipOptions.distance, 16);
9290
 
9291
		// update the inner HTML
9292
		if (text === false) {
9293
			this.hide();
9294
		} else {
9295
 
9296
			// show it
9297
			if (tooltip.isHidden) {
9298
				stop(label);
9299
				label.attr('opacity', 1).show();
9300
			}
9301
 
9302
			// update text
9303
			label.attr({
9304
				text: text
9305
			});
9306
 
9307
			// set the stroke color of the box
9308
			borderColor = options.borderColor || point.color || currentSeries.color || '#606060';
9309
			label.attr({
9310
				stroke: borderColor
9311
			});
9312
			tooltip.updatePosition({
9313
				plotX: x,
9314
				plotY: y,
9315
				negative: point.negative,
9316
				ttBelow: point.ttBelow,
9317
				h: anchor[2] || 0
9318
			});
9319
 
9320
			this.isHidden = false;
9321
		}
9322
		fireEvent(chart, 'tooltipRefresh', {
9323
				text: text,
9324
				x: x + chart.plotLeft,
9325
				y: y + chart.plotTop,
9326
				borderColor: borderColor
9327
			});
9328
	},
9329
 
9330
	/**
9331
	 * Find the new position and perform the move
9332
	 */
9333
	updatePosition: function (point) {
9334
		var chart = this.chart,
9335
			label = this.label,
9336
			pos = (this.options.positioner || this.getPosition).call(
9337
				this,
9338
				label.width,
9339
				label.height,
9340
				point
9341
			);
9342
 
9343
		// do the move
9344
		this.move(
9345
			mathRound(pos.x),
9346
			mathRound(pos.y || 0), // can be undefined (#3977)
9347
			point.plotX + chart.plotLeft,
9348
			point.plotY + chart.plotTop
9349
		);
9350
	},
9351
 
9352
	/**
9353
	 * Get the best X date format based on the closest point range on the axis.
9354
	 */
9355
	getXDateFormat: function (point, options, xAxis) {
9356
		var xDateFormat,
9357
			dateTimeLabelFormats = options.dateTimeLabelFormats,
9358
			closestPointRange = xAxis && xAxis.closestPointRange,
9359
			n,
9360
			blank = '01-01 00:00:00.000',
9361
			strpos = {
9362
				millisecond: 15,
9363
				second: 12,
9364
				minute: 9,
9365
				hour: 6,
9366
				day: 3
9367
			},
9368
			date,
9369
			lastN = 'millisecond'; // for sub-millisecond data, #4223
9370
 
9371
		if (closestPointRange) {
9372
			date = dateFormat('%m-%d %H:%M:%S.%L', point.x);
9373
			for (n in timeUnits) {
9374
 
9375
				// If the range is exactly one week and we're looking at a Sunday/Monday, go for the week format
9376
				if (closestPointRange === timeUnits.week && +dateFormat('%w', point.x) === xAxis.options.startOfWeek &&
9377
						date.substr(6) === blank.substr(6)) {
9378
					n = 'week';
9379
					break;
9380
 
9381
				// The first format that is too great for the range
9382
				} else if (timeUnits[n] > closestPointRange) {
9383
					n = lastN;
9384
					break;
9385
 
9386
				// If the point is placed every day at 23:59, we need to show
9387
				// the minutes as well. #2637.
9388
				} else if (strpos[n] && date.substr(strpos[n]) !== blank.substr(strpos[n])) {
9389
					break;
9390
				}
9391
 
9392
				// Weeks are outside the hierarchy, only apply them on Mondays/Sundays like in the first condition
9393
				if (n !== 'week') {
9394
					lastN = n;
9395
				}
9396
			}
9397
 
9398
			if (n) {
9399
				xDateFormat = dateTimeLabelFormats[n];
9400
			}
9401
		} else {
9402
			xDateFormat = dateTimeLabelFormats.day;
9403
		}
9404
 
9405
		return xDateFormat || dateTimeLabelFormats.year; // #2546, 2581
9406
	},
9407
 
9408
	/**
9409
	 * Format the footer/header of the tooltip
9410
	 * #3397: abstraction to enable formatting of footer and header
9411
	 */
9412
	tooltipFooterHeaderFormatter: function (point, isFooter) {
9413
		var footOrHead = isFooter ? 'footer' : 'header',
9414
			series = point.series,
9415
			tooltipOptions = series.tooltipOptions,
9416
			xDateFormat = tooltipOptions.xDateFormat,
9417
			xAxis = series.xAxis,
9418
			isDateTime = xAxis && xAxis.options.type === 'datetime' && isNumber(point.key),
9419
			formatString = tooltipOptions[footOrHead+'Format'];
9420
 
9421
		// Guess the best date format based on the closest point distance (#568, #3418)
9422
		if (isDateTime && !xDateFormat) {
9423
			xDateFormat = this.getXDateFormat(point, tooltipOptions, xAxis);
9424
		}
9425
 
9426
		// Insert the footer date format if any
9427
		if (isDateTime && xDateFormat) {
9428
			formatString = formatString.replace('{point.key}', '{point.key:' + xDateFormat + '}');
9429
		}
9430
 
9431
		return format(formatString, {
9432
			point: point,
9433
			series: series
9434
		});
9435
	},
9436
 
9437
	/**
9438
     * Build the body (lines) of the tooltip by iterating over the items and returning one entry for each item,
9439
     * abstracting this functionality allows to easily overwrite and extend it.
9440
	 */
9441
	bodyFormatter: function (items) {
9442
        return map(items, function (item) {
9443
            var tooltipOptions = item.series.tooltipOptions;
9444
            return (tooltipOptions.pointFormatter || item.point.tooltipFormatter).call(item.point, tooltipOptions.pointFormat);
9445
        });
9446
    }
9447
 
9448
};
9449
 
9450
var hoverChartIndex;
9451
 
9452
// Global flag for touch support
9453
hasTouch = doc.documentElement.ontouchstart !== UNDEFINED;
9454
 
9455
/**
9456
 * The mouse tracker object. All methods starting with "on" are primary DOM event handlers.
9457
 * Subsequent methods should be named differently from what they are doing.
9458
 * @param {Object} chart The Chart instance
9459
 * @param {Object} options The root options object
9460
 */
9461
var Pointer = Highcharts.Pointer = function (chart, options) {
9462
	this.init(chart, options);
9463
};
9464
 
9465
Pointer.prototype = {
9466
	/**
9467
	 * Initialize Pointer
9468
	 */
9469
	init: function (chart, options) {
9470
 
9471
		var chartOptions = options.chart,
9472
			chartEvents = chartOptions.events,
9473
			zoomType = useCanVG ? '' : chartOptions.zoomType,
9474
			inverted = chart.inverted,
9475
			zoomX,
9476
			zoomY;
9477
 
9478
		// Store references
9479
		this.options = options;
9480
		this.chart = chart;
9481
 
9482
		// Zoom status
9483
		this.zoomX = zoomX = /x/.test(zoomType);
9484
		this.zoomY = zoomY = /y/.test(zoomType);
9485
		this.zoomHor = (zoomX && !inverted) || (zoomY && inverted);
9486
		this.zoomVert = (zoomY && !inverted) || (zoomX && inverted);
9487
		this.hasZoom = zoomX || zoomY;
9488
 
9489
		// Do we need to handle click on a touch device?
9490
		this.runChartClick = chartEvents && !!chartEvents.click;
9491
 
9492
		this.pinchDown = [];
9493
		this.lastValidTouch = {};
9494
 
9495
		if (Highcharts.Tooltip && options.tooltip.enabled) {
9496
			chart.tooltip = new Tooltip(chart, options.tooltip);
9497
			this.followTouchMove = pick(options.tooltip.followTouchMove, true);
9498
		}
9499
 
9500
		this.setDOMEvents();
9501
	},
9502
 
9503
	/**
9504
	 * Add crossbrowser support for chartX and chartY
9505
	 * @param {Object} e The event object in standard browsers
9506
	 */
9507
	normalize: function (e, chartPosition) {
9508
		var chartX,
9509
			chartY,
9510
			ePos;
9511
 
9512
		// common IE normalizing
9513
		e = e || window.event;
9514
 
9515
		// Framework specific normalizing (#1165)
9516
		e = washMouseEvent(e);
9517
 
9518
		// More IE normalizing, needs to go after washMouseEvent
9519
		if (!e.target) {
9520
			e.target = e.srcElement;
9521
		}
9522
 
9523
		// iOS (#2757)
9524
		ePos = e.touches ?  (e.touches.length ? e.touches.item(0) : e.changedTouches[0]) : e;
9525
 
9526
		// Get mouse position
9527
		if (!chartPosition) {
9528
			this.chartPosition = chartPosition = offset(this.chart.container);
9529
		}
9530
 
9531
		// chartX and chartY
9532
		if (ePos.pageX === UNDEFINED) { // IE < 9. #886.
9533
			chartX = mathMax(e.x, e.clientX - chartPosition.left); // #2005, #2129: the second case is
9534
				// for IE10 quirks mode within framesets
9535
			chartY = e.y;
9536
		} else {
9537
			chartX = ePos.pageX - chartPosition.left;
9538
			chartY = ePos.pageY - chartPosition.top;
9539
		}
9540
 
9541
		return extend(e, {
9542
			chartX: mathRound(chartX),
9543
			chartY: mathRound(chartY)
9544
		});
9545
	},
9546
 
9547
	/**
9548
	 * Get the click position in terms of axis values.
9549
	 *
9550
	 * @param {Object} e A pointer event
9551
	 */
9552
	getCoordinates: function (e) {
9553
		var coordinates = {
9554
				xAxis: [],
9555
				yAxis: []
9556
			};
9557
 
9558
		each(this.chart.axes, function (axis) {
9559
			coordinates[axis.isXAxis ? 'xAxis' : 'yAxis'].push({
9560
				axis: axis,
9561
				value: axis.toValue(e[axis.horiz ? 'chartX' : 'chartY'])
9562
			});
9563
		});
9564
		return coordinates;
9565
	},
9566
 
9567
	/**
9568
	 * With line type charts with a single tracker, get the point closest to the mouse.
9569
	 * Run Point.onMouseOver and display tooltip for the point or points.
9570
	 */
9571
	runPointActions: function (e) {
9572
 
9573
		var pointer = this,
9574
			chart = pointer.chart,
9575
			series = chart.series,
9576
			tooltip = chart.tooltip,
9577
			shared = tooltip ? tooltip.shared : false,
9578
			followPointer,
9579
			hoverPoint = chart.hoverPoint,
9580
			hoverSeries = chart.hoverSeries,
9581
			i,
9582
			distance = chart.chartWidth,
9583
			anchor,
9584
			noSharedTooltip,
9585
			directTouch,
9586
			kdpoints = [],
9587
			kdpoint,
9588
			kdpointT;
9589
 
9590
		// For hovering over the empty parts of the plot area (hoverSeries is undefined).
9591
		// If there is one series with point tracking (combo chart), don't go to nearest neighbour.
9592
		if (!shared && !hoverSeries) {
9593
			for (i = 0; i < series.length; i++) {
9594
				if (series[i].directTouch || !series[i].options.stickyTracking) {
9595
					series = [];
9596
				}
9597
			}
9598
		}
9599
 
9600
		// If it has a hoverPoint and that series requires direct touch (like columns),
9601
		// use the hoverPoint (#3899). Otherwise, search the k-d tree.
9602
		if (!shared && hoverSeries && hoverSeries.directTouch && hoverPoint) {
9603
			kdpoint = hoverPoint;
9604
 
9605
		// Handle shared tooltip or cases where a series is not yet hovered
9606
		} else {
9607
			// Find nearest points on all series
9608
			each(series, function (s) {
9609
				// Skip hidden series
9610
				noSharedTooltip = s.noSharedTooltip && shared;
9611
				directTouch = !shared && s.directTouch;
9612
				if (s.visible && !noSharedTooltip && !directTouch && pick(s.options.enableMouseTracking, true)) { // #3821
9613
					kdpointT = s.searchPoint(e, !noSharedTooltip && s.kdDimensions === 1); // #3828
9614
					if (kdpointT) {
9615
						kdpoints.push(kdpointT);
9616
					}
9617
				}
9618
			});
9619
			// Find absolute nearest point
9620
			each(kdpoints, function (p) {
9621
				if (p && typeof p.dist === 'number' && p.dist < distance) {
9622
					distance = p.dist;
9623
					kdpoint = p;
9624
				}
9625
			});
9626
		}
9627
 
9628
		// Refresh tooltip for kdpoint if new hover point or tooltip was hidden // #3926, #4200
9629
		if (kdpoint && (kdpoint !== this.prevKDPoint || (tooltip && tooltip.isHidden))) {
9630
			// Draw tooltip if necessary
9631
			if (shared && !kdpoint.series.noSharedTooltip) {
9632
				i = kdpoints.length;
9633
				while (i--) {
9634
					if (kdpoints[i].clientX !== kdpoint.clientX || kdpoints[i].series.noSharedTooltip) {
9635
						kdpoints.splice(i, 1);
9636
					}
9637
				}
9638
				if (kdpoints.length && tooltip) {
9639
					tooltip.refresh(kdpoints, e);
9640
				}
9641
 
9642
				// Do mouseover on all points (#3919, #3985, #4410)
9643
				each(kdpoints, function (point) {
9644
					point.onMouseOver(e, point !== ((hoverSeries && hoverSeries.directTouch && hoverPoint) || kdpoint));
9645
				});
9646
			} else {
9647
				if (tooltip) {
9648
					tooltip.refresh(kdpoint, e);
9649
				}
9650
				if(!hoverSeries || !hoverSeries.directTouch) { // #4448
9651
					kdpoint.onMouseOver(e);
9652
				}
9653
			}
9654
			this.prevKDPoint = kdpoint;
9655
 
9656
		// Update positions (regardless of kdpoint or hoverPoint)
9657
		} else {
9658
			followPointer = hoverSeries && hoverSeries.tooltipOptions.followPointer;
9659
			if (tooltip && followPointer && !tooltip.isHidden) {
9660
				anchor = tooltip.getAnchor([{}], e);
9661
				tooltip.updatePosition({ plotX: anchor[0], plotY: anchor[1] });
9662
			}
9663
		}
9664
 
9665
		// Start the event listener to pick up the tooltip
9666
		if (tooltip && !pointer._onDocumentMouseMove) {
9667
			pointer._onDocumentMouseMove = function (e) {
9668
				if (charts[hoverChartIndex]) {
9669
					charts[hoverChartIndex].pointer.onDocumentMouseMove(e);
9670
				}
9671
			};
9672
			addEvent(doc, 'mousemove', pointer._onDocumentMouseMove);
9673
		}
9674
 
9675
		// Crosshair
9676
		each(chart.axes, function (axis) {
9677
			axis.drawCrosshair(e, pick(kdpoint, hoverPoint));
9678
		});
9679
 
9680
 
9681
	},
9682
 
9683
 
9684
 
9685
	/**
9686
	 * Reset the tracking by hiding the tooltip, the hover series state and the hover point
9687
	 *
9688
	 * @param allowMove {Boolean} Instead of destroying the tooltip altogether, allow moving it if possible
9689
	 */
9690
	reset: function (allowMove, delay) {
9691
		var pointer = this,
9692
			chart = pointer.chart,
9693
			hoverSeries = chart.hoverSeries,
9694
			hoverPoint = chart.hoverPoint,
9695
			hoverPoints = chart.hoverPoints,
9696
			tooltip = chart.tooltip,
9697
			tooltipPoints = tooltip && tooltip.shared ? hoverPoints : hoverPoint;
9698
 
9699
		// Narrow in allowMove
9700
		allowMove = allowMove && tooltip && tooltipPoints;
9701
 
9702
		// Check if the points have moved outside the plot area, #1003
9703
		if (allowMove  && splat(tooltipPoints)[0].plotX === UNDEFINED) {
9704
			allowMove = false;
9705
		}
9706
		// Just move the tooltip, #349
9707
		if (allowMove) {
9708
			tooltip.refresh(tooltipPoints);
9709
			if (hoverPoint) { // #2500
9710
				hoverPoint.setState(hoverPoint.state, true);
9711
				each(chart.axes, function (axis) {
9712
					if (pick(axis.options.crosshair && axis.options.crosshair.snap, true)) {
9713
						axis.drawCrosshair(null, hoverPoint);
9714
					}  else {
9715
						axis.hideCrosshair();
9716
					}
9717
				});
9718
 
9719
			}
9720
 
9721
		// Full reset
9722
		} else {
9723
 
9724
			if (hoverPoint) {
9725
				hoverPoint.onMouseOut();
9726
			}
9727
 
9728
			if (hoverPoints) {
9729
				each(hoverPoints, function (point) {
9730
					point.setState();
9731
				});
9732
			}
9733
 
9734
			if (hoverSeries) {
9735
				hoverSeries.onMouseOut();
9736
			}
9737
 
9738
			if (tooltip) {
9739
				tooltip.hide(delay);
9740
			}
9741
 
9742
			if (pointer._onDocumentMouseMove) {
9743
				removeEvent(doc, 'mousemove', pointer._onDocumentMouseMove);
9744
				pointer._onDocumentMouseMove = null;
9745
			}
9746
 
9747
			// Remove crosshairs
9748
			each(chart.axes, function (axis) {
9749
				axis.hideCrosshair();
9750
			});
9751
 
9752
			pointer.hoverX = chart.hoverPoints = chart.hoverPoint = null;
9753
 
9754
		}
9755
	},
9756
 
9757
	/**
9758
	 * Scale series groups to a certain scale and translation
9759
	 */
9760
	scaleGroups: function (attribs, clip) {
9761
 
9762
		var chart = this.chart,
9763
			seriesAttribs;
9764
 
9765
		// Scale each series
9766
		each(chart.series, function (series) {
9767
			seriesAttribs = attribs || series.getPlotBox(); // #1701
9768
			if (series.xAxis && series.xAxis.zoomEnabled) {
9769
				series.group.attr(seriesAttribs);
9770
				if (series.markerGroup) {
9771
					series.markerGroup.attr(seriesAttribs);
9772
					series.markerGroup.clip(clip ? chart.clipRect : null);
9773
				}
9774
				if (series.dataLabelsGroup) {
9775
					series.dataLabelsGroup.attr(seriesAttribs);
9776
				}
9777
			}
9778
		});
9779
 
9780
		// Clip
9781
		chart.clipRect.attr(clip || chart.clipBox);
9782
	},
9783
 
9784
	/**
9785
	 * Start a drag operation
9786
	 */
9787
	dragStart: function (e) {
9788
		var chart = this.chart;
9789
 
9790
		// Record the start position
9791
		chart.mouseIsDown = e.type;
9792
		chart.cancelClick = false;
9793
		chart.mouseDownX = this.mouseDownX = e.chartX;
9794
		chart.mouseDownY = this.mouseDownY = e.chartY;
9795
	},
9796
 
9797
	/**
9798
	 * Perform a drag operation in response to a mousemove event while the mouse is down
9799
	 */
9800
	drag: function (e) {
9801
 
9802
		var chart = this.chart,
9803
			chartOptions = chart.options.chart,
9804
			chartX = e.chartX,
9805
			chartY = e.chartY,
9806
			zoomHor = this.zoomHor,
9807
			zoomVert = this.zoomVert,
9808
			plotLeft = chart.plotLeft,
9809
			plotTop = chart.plotTop,
9810
			plotWidth = chart.plotWidth,
9811
			plotHeight = chart.plotHeight,
9812
			clickedInside,
9813
			size,
9814
			selectionMarker = this.selectionMarker,
9815
			mouseDownX = this.mouseDownX,
9816
			mouseDownY = this.mouseDownY,
9817
			panKey = chartOptions.panKey && e[chartOptions.panKey + 'Key'];
9818
 
9819
		// If the device supports both touch and mouse (like IE11), and we are touch-dragging
9820
		// inside the plot area, don't handle the mouse event. #4339.
9821
		if (selectionMarker && selectionMarker.touch) {
9822
			return;
9823
		}
9824
 
9825
		// If the mouse is outside the plot area, adjust to cooordinates
9826
		// inside to prevent the selection marker from going outside
9827
		if (chartX < plotLeft) {
9828
			chartX = plotLeft;
9829
		} else if (chartX > plotLeft + plotWidth) {
9830
			chartX = plotLeft + plotWidth;
9831
		}
9832
 
9833
		if (chartY < plotTop) {
9834
			chartY = plotTop;
9835
		} else if (chartY > plotTop + plotHeight) {
9836
			chartY = plotTop + plotHeight;
9837
		}
9838
 
9839
		// determine if the mouse has moved more than 10px
9840
		this.hasDragged = Math.sqrt(
9841
			Math.pow(mouseDownX - chartX, 2) +
9842
			Math.pow(mouseDownY - chartY, 2)
9843
		);
9844
 
9845
		if (this.hasDragged > 10) {
9846
			clickedInside = chart.isInsidePlot(mouseDownX - plotLeft, mouseDownY - plotTop);
9847
 
9848
			// make a selection
9849
			if (chart.hasCartesianSeries && (this.zoomX || this.zoomY) && clickedInside && !panKey) {
9850
				if (!selectionMarker) {
9851
					this.selectionMarker = selectionMarker = chart.renderer.rect(
9852
						plotLeft,
9853
						plotTop,
9854
						zoomHor ? 1 : plotWidth,
9855
						zoomVert ? 1 : plotHeight,
9856
 
9857
					)
9858
					.attr({
9859
						fill: chartOptions.selectionMarkerFill || 'rgba(69,114,167,0.25)',
9860
						zIndex: 7
9861
					})
9862
					.add();
9863
				}
9864
			}
9865
 
9866
			// adjust the width of the selection marker
9867
			if (selectionMarker && zoomHor) {
9868
				size = chartX - mouseDownX;
9869
				selectionMarker.attr({
9870
					width: mathAbs(size),
9871
					x: (size > 0 ? 0 : size) + mouseDownX
9872
				});
9873
			}
9874
			// adjust the height of the selection marker
9875
			if (selectionMarker && zoomVert) {
9876
				size = chartY - mouseDownY;
9877
				selectionMarker.attr({
9878
					height: mathAbs(size),
9879
					y: (size > 0 ? 0 : size) + mouseDownY
9880
				});
9881
			}
9882
 
9883
			// panning
9884
			if (clickedInside && !selectionMarker && chartOptions.panning) {
9885
				chart.pan(e, chartOptions.panning);
9886
			}
9887
		}
9888
	},
9889
 
9890
	/**
9891
	 * On mouse up or touch end across the entire document, drop the selection.
9892
	 */
9893
	drop: function (e) {
9894
		var pointer = this,
9895
			chart = this.chart,
9896
			hasPinched = this.hasPinched;
9897
 
9898
		if (this.selectionMarker) {
9899
			var selectionData = {
9900
					xAxis: [],
9901
					yAxis: [],
9902
					originalEvent: e.originalEvent || e
9903
				},
9904
				selectionBox = this.selectionMarker,
9905
				selectionLeft = selectionBox.attr ? selectionBox.attr('x') : selectionBox.x,
9906
				selectionTop = selectionBox.attr ? selectionBox.attr('y') : selectionBox.y,
9907
				selectionWidth = selectionBox.attr ? selectionBox.attr('width') : selectionBox.width,
9908
				selectionHeight = selectionBox.attr ? selectionBox.attr('height') : selectionBox.height,
9909
				runZoom;
9910
 
9911
			// a selection has been made
9912
			if (this.hasDragged || hasPinched) {
9913
 
9914
				// record each axis' min and max
9915
				each(chart.axes, function (axis) {
9916
					if (axis.zoomEnabled && defined(axis.min) && (hasPinched || pointer[{ xAxis: 'zoomX', yAxis: 'zoomY' }[axis.coll]])) { // #859, #3569
9917
						var horiz = axis.horiz,
9918
							minPixelPadding = e.type === 'touchend' ? axis.minPixelPadding: 0, // #1207, #3075
9919
							selectionMin = axis.toValue((horiz ? selectionLeft : selectionTop) + minPixelPadding),
9920
							selectionMax = axis.toValue((horiz ? selectionLeft + selectionWidth : selectionTop + selectionHeight) - minPixelPadding);
9921
 
9922
						selectionData[axis.coll].push({
9923
							axis: axis,
9924
							min: mathMin(selectionMin, selectionMax), // for reversed axes
9925
							max: mathMax(selectionMin, selectionMax)
9926
						});
9927
						runZoom = true;
9928
					}
9929
				});
9930
				if (runZoom) {
9931
					fireEvent(chart, 'selection', selectionData, function (args) {
9932
						chart.zoom(extend(args, hasPinched ? { animation: false } : null));
9933
					});
9934
				}
9935
 
9936
			}
9937
			this.selectionMarker = this.selectionMarker.destroy();
9938
 
9939
			// Reset scaling preview
9940
			if (hasPinched) {
9941
				this.scaleGroups();
9942
			}
9943
		}
9944
 
9945
		// Reset all
9946
		if (chart) { // it may be destroyed on mouse up - #877
9947
			css(chart.container, { cursor: chart._cursor });
9948
			chart.cancelClick = this.hasDragged > 10; // #370
9949
			chart.mouseIsDown = this.hasDragged = this.hasPinched = false;
9950
			this.pinchDown = [];
9951
		}
9952
	},
9953
 
9954
	onContainerMouseDown: function (e) {
9955
 
9956
		e = this.normalize(e);
9957
 
9958
		// issue #295, dragging not always working in Firefox
9959
		if (e.preventDefault) {
9960
			e.preventDefault();
9961
		}
9962
 
9963
		this.dragStart(e);
9964
	},
9965
 
9966
 
9967
 
9968
	onDocumentMouseUp: function (e) {
9969
		if (charts[hoverChartIndex]) {
9970
			charts[hoverChartIndex].pointer.drop(e);
9971
		}
9972
	},
9973
 
9974
	/**
9975
	 * Special handler for mouse move that will hide the tooltip when the mouse leaves the plotarea.
9976
	 * Issue #149 workaround. The mouseleave event does not always fire.
9977
	 */
9978
	onDocumentMouseMove: function (e) {
9979
		var chart = this.chart,
9980
			chartPosition = this.chartPosition;
9981
 
9982
		e = this.normalize(e, chartPosition);
9983
 
9984
		// If we're outside, hide the tooltip
9985
		if (chartPosition && !this.inClass(e.target, 'highcharts-tracker') &&
9986
				!chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) {
9987
			this.reset();
9988
		}
9989
	},
9990
 
9991
	/**
9992
	 * When mouse leaves the container, hide the tooltip.
9993
	 */
9994
	onContainerMouseLeave: function () {
9995
		var chart = charts[hoverChartIndex];
9996
		if (chart) {
9997
			chart.pointer.reset();
9998
			chart.pointer.chartPosition = null; // also reset the chart position, used in #149 fix
9999
		}
10000
	},
10001
 
10002
	// The mousemove, touchmove and touchstart event handler
10003
	onContainerMouseMove: function (e) {
10004
 
10005
		var chart = this.chart;
10006
 
10007
		hoverChartIndex = chart.index;
10008
 
10009
		e = this.normalize(e);
10010
		e.returnValue = false; // #2251, #3224
10011
 
10012
		if (chart.mouseIsDown === 'mousedown') {
10013
			this.drag(e);
10014
		}
10015
 
10016
		// Show the tooltip and run mouse over events (#977)
10017
		if ((this.inClass(e.target, 'highcharts-tracker') ||
10018
				chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) && !chart.openMenu) {
10019
			this.runPointActions(e);
10020
		}
10021
	},
10022
 
10023
	/**
10024
	 * Utility to detect whether an element has, or has a parent with, a specific
10025
	 * class name. Used on detection of tracker objects and on deciding whether
10026
	 * hovering the tooltip should cause the active series to mouse out.
10027
	 */
10028
	inClass: function (element, className) {
10029
		var elemClassName;
10030
		while (element) {
10031
			elemClassName = attr(element, 'class');
10032
			if (elemClassName) {
10033
				if (elemClassName.indexOf(className) !== -1) {
10034
					return true;
10035
				} else if (elemClassName.indexOf(PREFIX + 'container') !== -1) {
10036
					return false;
10037
				}
10038
			}
10039
			element = element.parentNode;
10040
		}
10041
	},
10042
 
10043
	onTrackerMouseOut: function (e) {
10044
		var series = this.chart.hoverSeries,
10045
			relatedTarget = e.relatedTarget || e.toElement,
10046
			relatedSeries = relatedTarget && relatedTarget.point && relatedTarget.point.series; // #2499
10047
 
10048
		if (series && !series.options.stickyTracking && !this.inClass(relatedTarget, PREFIX + 'tooltip') &&
10049
				relatedSeries !== series) {
10050
			series.onMouseOut();
10051
		}
10052
	},
10053
 
10054
	onContainerClick: function (e) {
10055
		var chart = this.chart,
10056
			hoverPoint = chart.hoverPoint,
10057
			plotLeft = chart.plotLeft,
10058
			plotTop = chart.plotTop;
10059
 
10060
		e = this.normalize(e);
10061
		e.originalEvent = e; // #3913
10062
 
10063
		if (!chart.cancelClick) {
10064
 
10065
			// On tracker click, fire the series and point events. #783, #1583
10066
			if (hoverPoint && this.inClass(e.target, PREFIX + 'tracker')) {
10067
 
10068
				// the series click event
10069
				fireEvent(hoverPoint.series, 'click', extend(e, {
10070
					point: hoverPoint
10071
				}));
10072
 
10073
				// the point click event
10074
				if (chart.hoverPoint) { // it may be destroyed (#1844)
10075
					hoverPoint.firePointEvent('click', e);
10076
				}
10077
 
10078
			// When clicking outside a tracker, fire a chart event
10079
			} else {
10080
				extend(e, this.getCoordinates(e));
10081
 
10082
				// fire a click event in the chart
10083
				if (chart.isInsidePlot(e.chartX - plotLeft, e.chartY - plotTop)) {
10084
					fireEvent(chart, 'click', e);
10085
				}
10086
			}
10087
 
10088
 
10089
		}
10090
	},
10091
 
10092
	/**
10093
	 * Set the JS DOM events on the container and document. This method should contain
10094
	 * a one-to-one assignment between methods and their handlers. Any advanced logic should
10095
	 * be moved to the handler reflecting the event's name.
10096
	 */
10097
	setDOMEvents: function () {
10098
 
10099
		var pointer = this,
10100
			container = pointer.chart.container;
10101
 
10102
		container.onmousedown = function (e) {
10103
			pointer.onContainerMouseDown(e);
10104
		};
10105
		container.onmousemove = function (e) {
10106
			pointer.onContainerMouseMove(e);
10107
		};
10108
		container.onclick = function (e) {
10109
			pointer.onContainerClick(e);
10110
		};
10111
		addEvent(container, 'mouseleave', pointer.onContainerMouseLeave);
10112
		if (chartCount === 1) {
10113
			addEvent(doc, 'mouseup', pointer.onDocumentMouseUp);
10114
		}
10115
		if (hasTouch) {
10116
			container.ontouchstart = function (e) {
10117
				pointer.onContainerTouchStart(e);
10118
			};
10119
			container.ontouchmove = function (e) {
10120
				pointer.onContainerTouchMove(e);
10121
			};
10122
			if (chartCount === 1) {
10123
				addEvent(doc, 'touchend', pointer.onDocumentTouchEnd);
10124
			}
10125
		}
10126
 
10127
	},
10128
 
10129
	/**
10130
	 * Destroys the Pointer object and disconnects DOM events.
10131
	 */
10132
	destroy: function () {
10133
		var prop;
10134
 
10135
		removeEvent(this.chart.container, 'mouseleave', this.onContainerMouseLeave);
10136
		if (!chartCount) {
10137
			removeEvent(doc, 'mouseup', this.onDocumentMouseUp);
10138
			removeEvent(doc, 'touchend', this.onDocumentTouchEnd);
10139
		}
10140
 
10141
		// memory and CPU leak
10142
		clearInterval(this.tooltipTimeout);
10143
 
10144
		for (prop in this) {
10145
			this[prop] = null;
10146
		}
10147
	}
10148
};
10149
 
10150
 
10151
/* Support for touch devices */
10152
extend(Highcharts.Pointer.prototype, {
10153
 
10154
	/**
10155
	 * Run translation operations
10156
	 */
10157
	pinchTranslate: function (pinchDown, touches, transform, selectionMarker, clip, lastValidTouch) {
10158
		if (this.zoomHor || this.pinchHor) {
10159
			this.pinchTranslateDirection(true, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch);
10160
		}
10161
		if (this.zoomVert || this.pinchVert) {
10162
			this.pinchTranslateDirection(false, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch);
10163
		}
10164
	},
10165
 
10166
	/**
10167
	 * Run translation operations for each direction (horizontal and vertical) independently
10168
	 */
10169
	pinchTranslateDirection: function (horiz, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch, forcedScale) {
10170
		var chart = this.chart,
10171
			xy = horiz ? 'x' : 'y',
10172
			XY = horiz ? 'X' : 'Y',
10173
			sChartXY = 'chart' + XY,
10174
			wh = horiz ? 'width' : 'height',
10175
			plotLeftTop = chart['plot' + (horiz ? 'Left' : 'Top')],
10176
			selectionWH,
10177
			selectionXY,
10178
			clipXY,
10179
			scale = forcedScale || 1,
10180
			inverted = chart.inverted,
10181
			bounds = chart.bounds[horiz ? 'h' : 'v'],
10182
			singleTouch = pinchDown.length === 1,
10183
			touch0Start = pinchDown[0][sChartXY],
10184
			touch0Now = touches[0][sChartXY],
10185
			touch1Start = !singleTouch && pinchDown[1][sChartXY],
10186
			touch1Now = !singleTouch && touches[1][sChartXY],
10187
			outOfBounds,
10188
			transformScale,
10189
			scaleKey,
10190
			setScale = function () {
10191
				if (!singleTouch && mathAbs(touch0Start - touch1Start) > 20) { // Don't zoom if fingers are too close on this axis
10192
					scale = forcedScale || mathAbs(touch0Now - touch1Now) / mathAbs(touch0Start - touch1Start);
10193
				}
10194
 
10195
				clipXY = ((plotLeftTop - touch0Now) / scale) + touch0Start;
10196
				selectionWH = chart['plot' + (horiz ? 'Width' : 'Height')] / scale;
10197
			};
10198
 
10199
		// Set the scale, first pass
10200
		setScale();
10201
 
10202
		selectionXY = clipXY; // the clip position (x or y) is altered if out of bounds, the selection position is not
10203
 
10204
		// Out of bounds
10205
		if (selectionXY < bounds.min) {
10206
			selectionXY = bounds.min;
10207
			outOfBounds = true;
10208
		} else if (selectionXY + selectionWH > bounds.max) {
10209
			selectionXY = bounds.max - selectionWH;
10210
			outOfBounds = true;
10211
		}
10212
 
10213
		// Is the chart dragged off its bounds, determined by dataMin and dataMax?
10214
		if (outOfBounds) {
10215
 
10216
			// Modify the touchNow position in order to create an elastic drag movement. This indicates
10217
			// to the user that the chart is responsive but can't be dragged further.
10218
			touch0Now -= 0.8 * (touch0Now - lastValidTouch[xy][0]);
10219
			if (!singleTouch) {
10220
				touch1Now -= 0.8 * (touch1Now - lastValidTouch[xy][1]);
10221
			}
10222
 
10223
			// Set the scale, second pass to adapt to the modified touchNow positions
10224
			setScale();
10225
 
10226
		} else {
10227
			lastValidTouch[xy] = [touch0Now, touch1Now];
10228
		}
10229
 
10230
		// Set geometry for clipping, selection and transformation
10231
		if (!inverted) { // TODO: implement clipping for inverted charts
10232
			clip[xy] = clipXY - plotLeftTop;
10233
			clip[wh] = selectionWH;
10234
		}
10235
		scaleKey = inverted ? (horiz ? 'scaleY' : 'scaleX') : 'scale' + XY;
10236
		transformScale = inverted ? 1 / scale : scale;
10237
 
10238
		selectionMarker[wh] = selectionWH;
10239
		selectionMarker[xy] = selectionXY;
10240
		transform[scaleKey] = scale;
10241
		transform['translate' + XY] = (transformScale * plotLeftTop) + (touch0Now - (transformScale * touch0Start));
10242
	},
10243
 
10244
	/**
10245
	 * Handle touch events with two touches
10246
	 */
10247
	pinch: function (e) {
10248
 
10249
		var self = this,
10250
			chart = self.chart,
10251
			pinchDown = self.pinchDown,
10252
			touches = e.touches,
10253
			touchesLength = touches.length,
10254
			lastValidTouch = self.lastValidTouch,
10255
			hasZoom = self.hasZoom,
10256
			selectionMarker = self.selectionMarker,
10257
			transform = {},
10258
			fireClickEvent = touchesLength === 1 && ((self.inClass(e.target, PREFIX + 'tracker') &&
10259
				chart.runTrackerClick) || self.runChartClick),
10260
			clip = {};
10261
 
10262
		// Don't initiate panning until the user has pinched. This prevents us from
10263
		// blocking page scrolling as users scroll down a long page (#4210).
10264
		if (touchesLength > 1) {
10265
			self.initiated = true;
10266
		}
10267
 
10268
		// On touch devices, only proceed to trigger click if a handler is defined
10269
		if (hasZoom && self.initiated && !fireClickEvent) {
10270
			e.preventDefault();
10271
		}
10272
 
10273
		// Normalize each touch
10274
		map(touches, function (e) {
10275
			return self.normalize(e);
10276
		});
10277
 
10278
		// Register the touch start position
10279
		if (e.type === 'touchstart') {
10280
			each(touches, function (e, i) {
10281
				pinchDown[i] = { chartX: e.chartX, chartY: e.chartY };
10282
			});
10283
			lastValidTouch.x = [pinchDown[0].chartX, pinchDown[1] && pinchDown[1].chartX];
10284
			lastValidTouch.y = [pinchDown[0].chartY, pinchDown[1] && pinchDown[1].chartY];
10285
 
10286
			// Identify the data bounds in pixels
10287
			each(chart.axes, function (axis) {
10288
				if (axis.zoomEnabled) {
10289
					var bounds = chart.bounds[axis.horiz ? 'h' : 'v'],
10290
						minPixelPadding = axis.minPixelPadding,
10291
						min = axis.toPixels(pick(axis.options.min, axis.dataMin)),
10292
						max = axis.toPixels(pick(axis.options.max, axis.dataMax)),
10293
						absMin = mathMin(min, max),
10294
						absMax = mathMax(min, max);
10295
 
10296
					// Store the bounds for use in the touchmove handler
10297
					bounds.min = mathMin(axis.pos, absMin - minPixelPadding);
10298
					bounds.max = mathMax(axis.pos + axis.len, absMax + minPixelPadding);
10299
				}
10300
			});
10301
			self.res = true; // reset on next move
10302
 
10303
		// Event type is touchmove, handle panning and pinching
10304
		} else if (pinchDown.length) { // can be 0 when releasing, if touchend fires first
10305
 
10306
 
10307
			// Set the marker
10308
			if (!selectionMarker) {
10309
				self.selectionMarker = selectionMarker = extend({
10310
					destroy: noop,
10311
					touch: true
10312
				}, chart.plotBox);
10313
			}
10314
 
10315
			self.pinchTranslate(pinchDown, touches, transform, selectionMarker, clip, lastValidTouch);
10316
 
10317
			self.hasPinched = hasZoom;
10318
 
10319
			// Scale and translate the groups to provide visual feedback during pinching
10320
			self.scaleGroups(transform, clip);
10321
 
10322
			// Optionally move the tooltip on touchmove
10323
			if (!hasZoom && self.followTouchMove && touchesLength === 1) {
10324
				this.runPointActions(self.normalize(e));
10325
			} else if (self.res) {
10326
				self.res = false;
10327
				this.reset(false, 0);
10328
			}
10329
		}
10330
	},
10331
 
10332
	/**
10333
	 * General touch handler shared by touchstart and touchmove.
10334
	 */
10335
	touch: function (e, start) {
10336
		var chart = this.chart;
10337
 
10338
		hoverChartIndex = chart.index;
10339
 
10340
		if (e.touches.length === 1) {
10341
 
10342
			e = this.normalize(e);
10343
 
10344
			if (chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop) && !chart.openMenu) {
10345
 
10346
				// Run mouse events and display tooltip etc
10347
				if (start) {
10348
					this.runPointActions(e);
10349
				}
10350
 
10351
				this.pinch(e);
10352
 
10353
			} else if (start) {
10354
				// Hide the tooltip on touching outside the plot area (#1203)
10355
				this.reset();
10356
			}
10357
 
10358
		} else if (e.touches.length === 2) {
10359
			this.pinch(e);
10360
		}
10361
	},
10362
 
10363
	onContainerTouchStart: function (e) {
10364
		this.touch(e, true);
10365
	},
10366
 
10367
	onContainerTouchMove: function (e) {
10368
		this.touch(e);
10369
	},
10370
 
10371
	onDocumentTouchEnd: function (e) {
10372
		if (charts[hoverChartIndex]) {
10373
			charts[hoverChartIndex].pointer.drop(e);
10374
		}
10375
	}
10376
 
10377
});
10378
if (win.PointerEvent || win.MSPointerEvent) {
10379
 
10380
	// The touches object keeps track of the points being touched at all times
10381
	var touches = {},
10382
		hasPointerEvent = !!win.PointerEvent,
10383
		getWebkitTouches = function () {
10384
			var key, fake = [];
10385
			fake.item = function (i) { return this[i]; };
10386
			for (key in touches) {
10387
				if (touches.hasOwnProperty(key)) {
10388
					fake.push({
10389
						pageX: touches[key].pageX,
10390
						pageY: touches[key].pageY,
10391
						target: touches[key].target
10392
					});
10393
				}
10394
			}
10395
			return fake;
10396
		},
10397
		translateMSPointer = function (e, method, wktype, callback) {
10398
			var p;
10399
			e = e.originalEvent || e;
10400
			if ((e.pointerType === 'touch' || e.pointerType === e.MSPOINTER_TYPE_TOUCH) && charts[hoverChartIndex]) {
10401
				callback(e);
10402
				p = charts[hoverChartIndex].pointer;
10403
				p[method]({
10404
					type: wktype,
10405
					target: e.currentTarget,
10406
					preventDefault: noop,
10407
					touches: getWebkitTouches()
10408
				});
10409
			}
10410
		};
10411
 
10412
	/**
10413
	 * Extend the Pointer prototype with methods for each event handler and more
10414
	 */
10415
	extend(Pointer.prototype, {
10416
		onContainerPointerDown: function (e) {
10417
			translateMSPointer(e, 'onContainerTouchStart', 'touchstart', function (e) {
10418
				touches[e.pointerId] = { pageX: e.pageX, pageY: e.pageY, target: e.currentTarget };
10419
			});
10420
		},
10421
		onContainerPointerMove: function (e) {
10422
			translateMSPointer(e, 'onContainerTouchMove', 'touchmove', function (e) {
10423
				touches[e.pointerId] = { pageX: e.pageX, pageY: e.pageY };
10424
				if (!touches[e.pointerId].target) {
10425
					touches[e.pointerId].target = e.currentTarget;
10426
				}
10427
			});
10428
		},
10429
		onDocumentPointerUp: function (e) {
10430
			translateMSPointer(e, 'onDocumentTouchEnd', 'touchend', function (e) {
10431
				delete touches[e.pointerId];
10432
			});
10433
		},
10434
 
10435
		/**
10436
		 * Add or remove the MS Pointer specific events
10437
		 */
10438
		batchMSEvents: function (fn) {
10439
			fn(this.chart.container, hasPointerEvent ? 'pointerdown' : 'MSPointerDown', this.onContainerPointerDown);
10440
			fn(this.chart.container, hasPointerEvent ? 'pointermove' : 'MSPointerMove', this.onContainerPointerMove);
10441
			fn(doc, hasPointerEvent ? 'pointerup' : 'MSPointerUp', this.onDocumentPointerUp);
10442
		}
10443
	});
10444
 
10445
	// Disable default IE actions for pinch and such on chart element
10446
	wrap(Pointer.prototype, 'init', function (proceed, chart, options) {
10447
		proceed.call(this, chart, options);
10448
		if (this.hasZoom) { // #4014
10449
			css(chart.container, {
10450
				'-ms-touch-action': NONE,
10451
				'touch-action': NONE
10452
			});
10453
		}
10454
	});
10455
 
10456
	// Add IE specific touch events to chart
10457
	wrap(Pointer.prototype, 'setDOMEvents', function (proceed) {
10458
		proceed.apply(this);
10459
		if (this.hasZoom || this.followTouchMove) {
10460
			this.batchMSEvents(addEvent);
10461
		}
10462
	});
10463
	// Destroy MS events also
10464
	wrap(Pointer.prototype, 'destroy', function (proceed) {
10465
		this.batchMSEvents(removeEvent);
10466
		proceed.call(this);
10467
	});
10468
}
10469
/**
10470
 * The overview of the chart's series
10471
 */
10472
var Legend = Highcharts.Legend = function (chart, options) {
10473
	this.init(chart, options);
10474
};
10475
 
10476
Legend.prototype = {
10477
 
10478
	/**
10479
	 * Initialize the legend
10480
	 */
10481
	init: function (chart, options) {
10482
 
10483
		var legend = this,
10484
			itemStyle = options.itemStyle,
10485
			padding,
10486
			itemMarginTop = options.itemMarginTop || 0;
10487
 
10488
		this.options = options;
10489
 
10490
		if (!options.enabled) {
10491
			return;
10492
		}
10493
 
10494
		legend.itemStyle = itemStyle;
10495
		legend.itemHiddenStyle = merge(itemStyle, options.itemHiddenStyle);
10496
		legend.itemMarginTop = itemMarginTop;
10497
		legend.padding = padding = pick(options.padding, 8);
10498
		legend.initialItemX = padding;
10499
		legend.initialItemY = padding - 5; // 5 is the number of pixels above the text
10500
		legend.maxItemWidth = 0;
10501
		legend.chart = chart;
10502
		legend.itemHeight = 0;
10503
		legend.symbolWidth = pick(options.symbolWidth, 16);
10504
		legend.pages = [];
10505
 
10506
 
10507
		// Render it
10508
		legend.render();
10509
 
10510
		// move checkboxes
10511
		addEvent(legend.chart, 'endResize', function () {
10512
			legend.positionCheckboxes();
10513
		});
10514
 
10515
	},
10516
 
10517
	/**
10518
	 * Set the colors for the legend item
10519
	 * @param {Object} item A Series or Point instance
10520
	 * @param {Object} visible Dimmed or colored
10521
	 */
10522
	colorizeItem: function (item, visible) {
10523
		var legend = this,
10524
			options = legend.options,
10525
			legendItem = item.legendItem,
10526
			legendLine = item.legendLine,
10527
			legendSymbol = item.legendSymbol,
10528
			hiddenColor = legend.itemHiddenStyle.color,
10529
			textColor = visible ? options.itemStyle.color : hiddenColor,
10530
			symbolColor = visible ? (item.legendColor || item.color || '#CCC') : hiddenColor,
10531
			markerOptions = item.options && item.options.marker,
10532
			symbolAttr = { fill: symbolColor },
10533
			key,
10534
			val;
10535
 
10536
		if (legendItem) {
10537
			legendItem.css({ fill: textColor, color: textColor }); // color for #1553, oldIE
10538
		}
10539
		if (legendLine) {
10540
			legendLine.attr({ stroke: symbolColor });
10541
		}
10542
 
10543
		if (legendSymbol) {
10544
 
10545
			// Apply marker options
10546
			if (markerOptions && legendSymbol.isMarker) { // #585
10547
				symbolAttr.stroke = symbolColor;
10548
				markerOptions = item.convertAttribs(markerOptions);
10549
				for (key in markerOptions) {
10550
					val = markerOptions[key];
10551
					if (val !== UNDEFINED) {
10552
						symbolAttr[key] = val;
10553
					}
10554
				}
10555
			}
10556
 
10557
			legendSymbol.attr(symbolAttr);
10558
		}
10559
	},
10560
 
10561
	/**
10562
	 * Position the legend item
10563
	 * @param {Object} item A Series or Point instance
10564
	 */
10565
	positionItem: function (item) {
10566
		var legend = this,
10567
			options = legend.options,
10568
			symbolPadding = options.symbolPadding,
10569
			ltr = !options.rtl,
10570
			legendItemPos = item._legendItemPos,
10571
			itemX = legendItemPos[0],
10572
			itemY = legendItemPos[1],
10573
			checkbox = item.checkbox,
10574
			legendGroup = item.legendGroup;
10575
 
10576
		if (legendGroup && legendGroup.element) {
10577
			legendGroup.translate(
10578
				ltr ? itemX : legend.legendWidth - itemX - 2 * symbolPadding - 4,
10579
				itemY
10580
			);
10581
		}
10582
 
10583
		if (checkbox) {
10584
			checkbox.x = itemX;
10585
			checkbox.y = itemY;
10586
		}
10587
	},
10588
 
10589
	/**
10590
	 * Destroy a single legend item
10591
	 * @param {Object} item The series or point
10592
	 */
10593
	destroyItem: function (item) {
10594
		var checkbox = item.checkbox;
10595
 
10596
		// destroy SVG elements
10597
		each(['legendItem', 'legendLine', 'legendSymbol', 'legendGroup'], function (key) {
10598
			if (item[key]) {
10599
				item[key] = item[key].destroy();
10600
			}
10601
		});
10602
 
10603
		if (checkbox) {
10604
			discardElement(item.checkbox);
10605
		}
10606
	},
10607
 
10608
	/**
10609
	 * Destroys the legend.
10610
	 */
10611
	destroy: function () {
10612
		var legend = this,
10613
			legendGroup = legend.group,
10614
			box = legend.box;
10615
 
10616
		if (box) {
10617
			legend.box = box.destroy();
10618
		}
10619
 
10620
		if (legendGroup) {
10621
			legend.group = legendGroup.destroy();
10622
		}
10623
	},
10624
 
10625
	/**
10626
	 * Position the checkboxes after the width is determined
10627
	 */
10628
	positionCheckboxes: function (scrollOffset) {
10629
		var alignAttr = this.group.alignAttr,
10630
			translateY,
10631
			clipHeight = this.clipHeight || this.legendHeight;
10632
 
10633
		if (alignAttr) {
10634
			translateY = alignAttr.translateY;
10635
			each(this.allItems, function (item) {
10636
				var checkbox = item.checkbox,
10637
					top;
10638
 
10639
				if (checkbox) {
10640
					top = (translateY + checkbox.y + (scrollOffset || 0) + 3);
10641
					css(checkbox, {
10642
						left: (alignAttr.translateX + item.checkboxOffset + checkbox.x - 20) + PX,
10643
						top: top + PX,
10644
						display: top > translateY - 6 && top < translateY + clipHeight - 6 ? '' : NONE
10645
					});
10646
				}
10647
			});
10648
		}
10649
	},
10650
 
10651
	/**
10652
	 * Render the legend title on top of the legend
10653
	 */
10654
	renderTitle: function () {
10655
		var options = this.options,
10656
			padding = this.padding,
10657
			titleOptions = options.title,
10658
			titleHeight = 0,
10659
			bBox;
10660
 
10661
		if (titleOptions.text) {
10662
			if (!this.title) {
10663
				this.title = this.chart.renderer.label(titleOptions.text, padding - 3, padding - 4, null, null, null, null, null, 'legend-title')
10664
					.attr({ zIndex: 1 })
10665
					.css(titleOptions.style)
10666
					.add(this.group);
10667
			}
10668
			bBox = this.title.getBBox();
10669
			titleHeight = bBox.height;
10670
			this.offsetWidth = bBox.width; // #1717
10671
			this.contentGroup.attr({ translateY: titleHeight });
10672
		}
10673
		this.titleHeight = titleHeight;
10674
	},
10675
 
10676
	/**
10677
	 * Set the legend item text
10678
	 */
10679
	setText: function (item) {
10680
		var options = this.options;
10681
		item.legendItem.attr({
10682
			text: options.labelFormat ? format(options.labelFormat, item) : options.labelFormatter.call(item)
10683
		});
10684
	},
10685
 
10686
	/**
10687
	 * Render a single specific legend item
10688
	 * @param {Object} item A series or point
10689
	 */
10690
	renderItem: function (item) {
10691
		var legend = this,
10692
			chart = legend.chart,
10693
			renderer = chart.renderer,
10694
			options = legend.options,
10695
			horizontal = options.layout === 'horizontal',
10696
			symbolWidth = legend.symbolWidth,
10697
			symbolPadding = options.symbolPadding,
10698
			itemStyle = legend.itemStyle,
10699
			itemHiddenStyle = legend.itemHiddenStyle,
10700
			padding = legend.padding,
10701
			itemDistance = horizontal ? pick(options.itemDistance, 20) : 0,
10702
			ltr = !options.rtl,
10703
			itemHeight,
10704
			widthOption = options.width,
10705
			itemMarginBottom = options.itemMarginBottom || 0,
10706
			itemMarginTop = legend.itemMarginTop,
10707
			initialItemX = legend.initialItemX,
10708
			bBox,
10709
			itemWidth,
10710
			li = item.legendItem,
10711
			series = item.series && item.series.drawLegendSymbol ? item.series : item,
10712
			seriesOptions = series.options,
10713
			showCheckbox = legend.createCheckboxForItem && seriesOptions && seriesOptions.showCheckbox,
10714
			useHTML = options.useHTML;
10715
 
10716
		if (!li) { // generate it once, later move it
10717
 
10718
			// Generate the group box
10719
			// A group to hold the symbol and text. Text is to be appended in Legend class.
10720
			item.legendGroup = renderer.g('legend-item')
10721
				.attr({ zIndex: 1 })
10722
				.add(legend.scrollGroup);
10723
 
10724
			// Generate the list item text and add it to the group
10725
			item.legendItem = li = renderer.text(
10726
					'',
10727
					ltr ? symbolWidth + symbolPadding : -symbolPadding,
10728
					legend.baseline || 0,
10729
					useHTML
10730
				)
10731
				.css(merge(item.visible ? itemStyle : itemHiddenStyle)) // merge to prevent modifying original (#1021)
10732
				.attr({
10733
					align: ltr ? 'left' : 'right',
10734
					zIndex: 2
10735
				})
10736
				.add(item.legendGroup);
10737
 
10738
			// Get the baseline for the first item - the font size is equal for all
10739
			if (!legend.baseline) {
10740
				legend.fontMetrics = renderer.fontMetrics(itemStyle.fontSize, li);
10741
				legend.baseline = legend.fontMetrics.f + 3 + itemMarginTop;
10742
				li.attr('y', legend.baseline);
10743
			}
10744
 
10745
			// Draw the legend symbol inside the group box
10746
			series.drawLegendSymbol(legend, item);
10747
 
10748
			if (legend.setItemEvents) {
10749
				legend.setItemEvents(item, li, useHTML, itemStyle, itemHiddenStyle);
10750
			}
10751
 
10752
			// Colorize the items
10753
			legend.colorizeItem(item, item.visible);
10754
 
10755
			// add the HTML checkbox on top
10756
			if (showCheckbox) {
10757
				legend.createCheckboxForItem(item);
10758
			}
10759
		}
10760
 
10761
		// Always update the text
10762
		legend.setText(item);
10763
 
10764
		// calculate the positions for the next line
10765
		bBox = li.getBBox();
10766
 
10767
		itemWidth = item.checkboxOffset =
10768
			options.itemWidth ||
10769
			item.legendItemWidth ||
10770
			symbolWidth + symbolPadding + bBox.width + itemDistance + (showCheckbox ? 20 : 0);
10771
		legend.itemHeight = itemHeight = mathRound(item.legendItemHeight || bBox.height);
10772
 
10773
		// if the item exceeds the width, start a new line
10774
		if (horizontal && legend.itemX - initialItemX + itemWidth >
10775
				(widthOption || (chart.chartWidth - 2 * padding - initialItemX - options.x))) {
10776
			legend.itemX = initialItemX;
10777
			legend.itemY += itemMarginTop + legend.lastLineHeight + itemMarginBottom;
10778
			legend.lastLineHeight = 0; // reset for next line (#915, #3976)
10779
		}
10780
 
10781
		// If the item exceeds the height, start a new column
10782
		/*if (!horizontal && legend.itemY + options.y + itemHeight > chart.chartHeight - spacingTop - spacingBottom) {
10783
			legend.itemY = legend.initialItemY;
10784
			legend.itemX += legend.maxItemWidth;
10785
			legend.maxItemWidth = 0;
10786
		}*/
10787
 
10788
		// Set the edge positions
10789
		legend.maxItemWidth = mathMax(legend.maxItemWidth, itemWidth);
10790
		legend.lastItemY = itemMarginTop + legend.itemY + itemMarginBottom;
10791
		legend.lastLineHeight = mathMax(itemHeight, legend.lastLineHeight); // #915
10792
 
10793
		// cache the position of the newly generated or reordered items
10794
		item._legendItemPos = [legend.itemX, legend.itemY];
10795
 
10796
		// advance
10797
		if (horizontal) {
10798
			legend.itemX += itemWidth;
10799
 
10800
		} else {
10801
			legend.itemY += itemMarginTop + itemHeight + itemMarginBottom;
10802
			legend.lastLineHeight = itemHeight;
10803
		}
10804
 
10805
		// the width of the widest item
10806
		legend.offsetWidth = widthOption || mathMax(
10807
			(horizontal ? legend.itemX - initialItemX - itemDistance : itemWidth) + padding,
10808
			legend.offsetWidth
10809
		);
10810
	},
10811
 
10812
	/**
10813
	 * Get all items, which is one item per series for normal series and one item per point
10814
	 * for pie series.
10815
	 */
10816
	getAllItems: function () {
10817
		var allItems = [];
10818
		each(this.chart.series, function (series) {
10819
			var seriesOptions = series.options;
10820
 
10821
			// Handle showInLegend. If the series is linked to another series, defaults to false.
10822
			if (!pick(seriesOptions.showInLegend, !defined(seriesOptions.linkedTo) ? UNDEFINED : false, true)) {
10823
				return;
10824
			}
10825
 
10826
			// use points or series for the legend item depending on legendType
10827
			allItems = allItems.concat(
10828
					series.legendItems ||
10829
					(seriesOptions.legendType === 'point' ?
10830
							series.data :
10831
							series)
10832
			);
10833
		});
10834
		return allItems;
10835
	},
10836
 
10837
	/**
10838
	 * Adjust the chart margins by reserving space for the legend on only one side
10839
	 * of the chart. If the position is set to a corner, top or bottom is reserved
10840
	 * for horizontal legends and left or right for vertical ones.
10841
	 */
10842
	adjustMargins: function (margin, spacing) {
10843
		var chart = this.chart,
10844
			options = this.options,
10845
			// Use the first letter of each alignment option in order to detect the side
10846
			alignment = options.align.charAt(0) + options.verticalAlign.charAt(0) + options.layout.charAt(0); // #4189 - use charAt(x) notation instead of [x] for IE7
10847
 
10848
		if (this.display && !options.floating) {
10849
 
10850
			each([
10851
				/(lth|ct|rth)/,
10852
				/(rtv|rm|rbv)/,
10853
				/(rbh|cb|lbh)/,
10854
				/(lbv|lm|ltv)/
10855
			], function (alignments, side) {
10856
				if (alignments.test(alignment) && !defined(margin[side])) {
10857
					// Now we have detected on which side of the chart we should reserve space for the legend
10858
					chart[marginNames[side]] = mathMax(
10859
						chart[marginNames[side]],
10860
						chart.legend[(side + 1) % 2 ? 'legendHeight' : 'legendWidth'] +
10861
							[1, -1, -1, 1][side] * options[(side % 2) ? 'x' : 'y'] +
10862
							pick(options.margin, 12) +
10863
							spacing[side]
10864
					);
10865
				}
10866
			});
10867
		}
10868
	},
10869
 
10870
	/**
10871
	 * Render the legend. This method can be called both before and after
10872
	 * chart.render. If called after, it will only rearrange items instead
10873
	 * of creating new ones.
10874
	 */
10875
	render: function () {
10876
		var legend = this,
10877
			chart = legend.chart,
10878
			renderer = chart.renderer,
10879
			legendGroup = legend.group,
10880
			allItems,
10881
			display,
10882
			legendWidth,
10883
			legendHeight,
10884
			box = legend.box,
10885
			options = legend.options,
10886
			padding = legend.padding,
10887
			legendBorderWidth = options.borderWidth,
10888
			legendBackgroundColor = options.backgroundColor;
10889
 
10890
		legend.itemX = legend.initialItemX;
10891
		legend.itemY = legend.initialItemY;
10892
		legend.offsetWidth = 0;
10893
		legend.lastItemY = 0;
10894
 
10895
		if (!legendGroup) {
10896
			legend.group = legendGroup = renderer.g('legend')
10897
				.attr({ zIndex: 7 })
10898
				.add();
10899
			legend.contentGroup = renderer.g()
10900
				.attr({ zIndex: 1 }) // above background
10901
				.add(legendGroup);
10902
			legend.scrollGroup = renderer.g()
10903
				.add(legend.contentGroup);
10904
		}
10905
 
10906
		legend.renderTitle();
10907
 
10908
		// add each series or point
10909
		allItems = legend.getAllItems();
10910
 
10911
		// sort by legendIndex
10912
		stableSort(allItems, function (a, b) {
10913
			return ((a.options && a.options.legendIndex) || 0) - ((b.options && b.options.legendIndex) || 0);
10914
		});
10915
 
10916
		// reversed legend
10917
		if (options.reversed) {
10918
			allItems.reverse();
10919
		}
10920
 
10921
		legend.allItems = allItems;
10922
		legend.display = display = !!allItems.length;
10923
 
10924
		// render the items
10925
		legend.lastLineHeight = 0;
10926
		each(allItems, function (item) {
10927
			legend.renderItem(item);
10928
		});
10929
 
10930
		// Get the box
10931
		legendWidth = (options.width || legend.offsetWidth) + padding;
10932
		legendHeight = legend.lastItemY + legend.lastLineHeight + legend.titleHeight;
10933
		legendHeight = legend.handleOverflow(legendHeight);
10934
		legendHeight += padding;
10935
 
10936
		// Draw the border and/or background
10937
		if (legendBorderWidth || legendBackgroundColor) {
10938
 
10939
			if (!box) {
10940
				legend.box = box = renderer.rect(
10941
					0,
10942
					0,
10943
					legendWidth,
10944
					legendHeight,
10945
					options.borderRadius,
10946
					legendBorderWidth || 0
10947
				).attr({
10948
					stroke: options.borderColor,
10949
					'stroke-width': legendBorderWidth || 0,
10950
					fill: legendBackgroundColor || NONE
10951
				})
10952
				.add(legendGroup)
10953
				.shadow(options.shadow);
10954
				box.isNew = true;
10955
 
10956
			} else if (legendWidth > 0 && legendHeight > 0) {
10957
				box[box.isNew ? 'attr' : 'animate'](
10958
					box.crisp({ width: legendWidth, height: legendHeight })
10959
				);
10960
				box.isNew = false;
10961
			}
10962
 
10963
			// hide the border if no items
10964
			box[display ? 'show' : 'hide']();
10965
		}
10966
 
10967
		legend.legendWidth = legendWidth;
10968
		legend.legendHeight = legendHeight;
10969
 
10970
		// Now that the legend width and height are established, put the items in the
10971
		// final position
10972
		each(allItems, function (item) {
10973
			legend.positionItem(item);
10974
		});
10975
 
10976
		// 1.x compatibility: positioning based on style
10977
		/*var props = ['left', 'right', 'top', 'bottom'],
10978
			prop,
10979
			i = 4;
10980
		while (i--) {
10981
			prop = props[i];
10982
			if (options.style[prop] && options.style[prop] !== 'auto') {
10983
				options[i < 2 ? 'align' : 'verticalAlign'] = prop;
10984
				options[i < 2 ? 'x' : 'y'] = pInt(options.style[prop]) * (i % 2 ? -1 : 1);
10985
			}
10986
		}*/
10987
 
10988
		if (display) {
10989
			legendGroup.align(extend({
10990
				width: legendWidth,
10991
				height: legendHeight
10992
			}, options), true, 'spacingBox');
10993
		}
10994
 
10995
		if (!chart.isResizing) {
10996
			this.positionCheckboxes();
10997
		}
10998
	},
10999
 
11000
	/**
11001
	 * Set up the overflow handling by adding navigation with up and down arrows below the
11002
	 * legend.
11003
	 */
11004
	handleOverflow: function (legendHeight) {
11005
		var legend = this,
11006
			chart = this.chart,
11007
			renderer = chart.renderer,
11008
			options = this.options,
11009
			optionsY = options.y,
11010
			alignTop = options.verticalAlign === 'top',
11011
			spaceHeight = chart.spacingBox.height + (alignTop ? -optionsY : optionsY) - this.padding,
11012
			maxHeight = options.maxHeight,
11013
			clipHeight,
11014
			clipRect = this.clipRect,
11015
			navOptions = options.navigation,
11016
			animation = pick(navOptions.animation, true),
11017
			arrowSize = navOptions.arrowSize || 12,
11018
			nav = this.nav,
11019
			pages = this.pages,
11020
			padding = this.padding,
11021
			lastY,
11022
			allItems = this.allItems,
11023
			clipToHeight = function (height) {
11024
				clipRect.attr({
11025
					height: height
11026
				});
11027
 
11028
				// useHTML
11029
				if (legend.contentGroup.div) {
11030
					legend.contentGroup.div.style.clip = 'rect(' + padding + 'px,9999px,' + (padding + height) + 'px,0)';
11031
				}
11032
			};
11033
 
11034
 
11035
		// Adjust the height
11036
		if (options.layout === 'horizontal') {
11037
			spaceHeight /= 2;
11038
		}
11039
		if (maxHeight) {
11040
			spaceHeight = mathMin(spaceHeight, maxHeight);
11041
		}
11042
 
11043
		// Reset the legend height and adjust the clipping rectangle
11044
		pages.length = 0;
11045
		if (legendHeight > spaceHeight) {
11046
 
11047
			this.clipHeight = clipHeight = mathMax(spaceHeight - 20 - this.titleHeight - padding, 0);
11048
			this.currentPage = pick(this.currentPage, 1);
11049
			this.fullHeight = legendHeight;
11050
 
11051
			// Fill pages with Y positions so that the top of each a legend item defines
11052
			// the scroll top for each page (#2098)
11053
			each(allItems, function (item, i) {
11054
				var y = item._legendItemPos[1],
11055
					h = mathRound(item.legendItem.getBBox().height),
11056
					len = pages.length;
11057
 
11058
				if (!len || (y - pages[len - 1] > clipHeight && (lastY || y) !== pages[len - 1])) {
11059
					pages.push(lastY || y);
11060
					len++;
11061
				}
11062
 
11063
				if (i === allItems.length - 1 && y + h - pages[len - 1] > clipHeight) {
11064
					pages.push(y);
11065
				}
11066
				if (y !== lastY) {
11067
					lastY = y;
11068
				}
11069
			});
11070
 
11071
			// Only apply clipping if needed. Clipping causes blurred legend in PDF export (#1787)
11072
			if (!clipRect) {
11073
				clipRect = legend.clipRect = renderer.clipRect(0, padding, 9999, 0);
11074
				legend.contentGroup.clip(clipRect);
11075
			}
11076
 
11077
			clipToHeight(clipHeight);
11078
 
11079
			// Add navigation elements
11080
			if (!nav) {
11081
				this.nav = nav = renderer.g().attr({ zIndex: 1 }).add(this.group);
11082
				this.up = renderer.symbol('triangle', 0, 0, arrowSize, arrowSize)
11083
					.on('click', function () {
11084
						legend.scroll(-1, animation);
11085
					})
11086
					.add(nav);
11087
				this.pager = renderer.text('', 15, 10)
11088
					.css(navOptions.style)
11089
					.add(nav);
11090
				this.down = renderer.symbol('triangle-down', 0, 0, arrowSize, arrowSize)
11091
					.on('click', function () {
11092
						legend.scroll(1, animation);
11093
					})
11094
					.add(nav);
11095
			}
11096
 
11097
			// Set initial position
11098
			legend.scroll(0);
11099
 
11100
			legendHeight = spaceHeight;
11101
 
11102
		} else if (nav) {
11103
			clipToHeight(chart.chartHeight);
11104
			nav.hide();
11105
			this.scrollGroup.attr({
11106
				translateY: 1
11107
			});
11108
			this.clipHeight = 0; // #1379
11109
		}
11110
 
11111
		return legendHeight;
11112
	},
11113
 
11114
	/**
11115
	 * Scroll the legend by a number of pages
11116
	 * @param {Object} scrollBy
11117
	 * @param {Object} animation
11118
	 */
11119
	scroll: function (scrollBy, animation) {
11120
		var pages = this.pages,
11121
			pageCount = pages.length,
11122
			currentPage = this.currentPage + scrollBy,
11123
			clipHeight = this.clipHeight,
11124
			navOptions = this.options.navigation,
11125
			activeColor = navOptions.activeColor,
11126
			inactiveColor = navOptions.inactiveColor,
11127
			pager = this.pager,
11128
			padding = this.padding,
11129
			scrollOffset;
11130
 
11131
		// When resizing while looking at the last page
11132
		if (currentPage > pageCount) {
11133
			currentPage = pageCount;
11134
		}
11135
 
11136
		if (currentPage > 0) {
11137
 
11138
			if (animation !== UNDEFINED) {
11139
				setAnimation(animation, this.chart);
11140
			}
11141
 
11142
			this.nav.attr({
11143
				translateX: padding,
11144
				translateY: clipHeight + this.padding + 7 + this.titleHeight,
11145
				visibility: VISIBLE
11146
			});
11147
			this.up.attr({
11148
					fill: currentPage === 1 ? inactiveColor : activeColor
11149
				})
11150
				.css({
11151
					cursor: currentPage === 1 ? 'default' : 'pointer'
11152
				});
11153
			pager.attr({
11154
				text: currentPage + '/' + pageCount
11155
			});
11156
			this.down.attr({
11157
					x: 18 + this.pager.getBBox().width, // adjust to text width
11158
					fill: currentPage === pageCount ? inactiveColor : activeColor
11159
				})
11160
				.css({
11161
					cursor: currentPage === pageCount ? 'default' : 'pointer'
11162
				});
11163
 
11164
			scrollOffset = -pages[currentPage - 1] + this.initialItemY;
11165
 
11166
			this.scrollGroup.animate({
11167
				translateY: scrollOffset
11168
			});
11169
 
11170
			this.currentPage = currentPage;
11171
			this.positionCheckboxes(scrollOffset);
11172
		}
11173
 
11174
	}
11175
 
11176
};
11177
 
11178
/*
11179
 * LegendSymbolMixin
11180
 */
11181
 
11182
var LegendSymbolMixin = Highcharts.LegendSymbolMixin = {
11183
 
11184
	/**
11185
	 * Get the series' symbol in the legend
11186
	 *
11187
	 * @param {Object} legend The legend object
11188
	 * @param {Object} item The series (this) or point
11189
	 */
11190
	drawRectangle: function (legend, item) {
11191
		var symbolHeight = legend.options.symbolHeight || legend.fontMetrics.f;
11192
 
11193
		item.legendSymbol = this.chart.renderer.rect(
11194
			0,
11195
			legend.baseline - symbolHeight + 1, // #3988
11196
			legend.symbolWidth,
11197
			symbolHeight,
11198
			legend.options.symbolRadius || 0
11199
		).attr({
11200
			zIndex: 3
11201
		}).add(item.legendGroup);
11202
 
11203
	},
11204
 
11205
	/**
11206
	 * Get the series' symbol in the legend. This method should be overridable to create custom
11207
	 * symbols through Highcharts.seriesTypes[type].prototype.drawLegendSymbols.
11208
	 *
11209
	 * @param {Object} legend The legend object
11210
	 */
11211
	drawLineMarker: function (legend) {
11212
 
11213
		var options = this.options,
11214
			markerOptions = options.marker,
11215
			radius,
11216
			legendSymbol,
11217
			symbolWidth = legend.symbolWidth,
11218
			renderer = this.chart.renderer,
11219
			legendItemGroup = this.legendGroup,
11220
			verticalCenter = legend.baseline - mathRound(legend.fontMetrics.b * 0.3),
11221
			attr;
11222
 
11223
		// Draw the line
11224
		if (options.lineWidth) {
11225
			attr = {
11226
				'stroke-width': options.lineWidth
11227
			};
11228
			if (options.dashStyle) {
11229
				attr.dashstyle = options.dashStyle;
11230
			}
11231
			this.legendLine = renderer.path([
11232
				M,
11233
				0,
11234
				verticalCenter,
11235
				L,
11236
				symbolWidth,
11237
				verticalCenter
11238
			])
11239
			.attr(attr)
11240
			.add(legendItemGroup);
11241
		}
11242
 
11243
		// Draw the marker
11244
		if (markerOptions && markerOptions.enabled !== false) {
11245
			radius = markerOptions.radius;
11246
			this.legendSymbol = legendSymbol = renderer.symbol(
11247
				this.symbol,
11248
				(symbolWidth / 2) - radius,
11249
				verticalCenter - radius,
11250
				2 * radius,
11251
				2 * radius
11252
			)
11253
			.add(legendItemGroup);
11254
			legendSymbol.isMarker = true;
11255
		}
11256
	}
11257
};
11258
 
11259
// Workaround for #2030, horizontal legend items not displaying in IE11 Preview,
11260
// and for #2580, a similar drawing flaw in Firefox 26.
11261
// TODO: Explore if there's a general cause for this. The problem may be related
11262
// to nested group elements, as the legend item texts are within 4 group elements.
11263
if (/Trident\/7\.0/.test(userAgent) || isFirefox) {
11264
	wrap(Legend.prototype, 'positionItem', function (proceed, item) {
11265
		var legend = this,
11266
			runPositionItem = function () { // If chart destroyed in sync, this is undefined (#2030)
11267
				if (item._legendItemPos) {
11268
					proceed.call(legend, item);
11269
				}
11270
			};
11271
 
11272
		// Do it now, for export and to get checkbox placement
11273
		runPositionItem();
11274
 
11275
		// Do it after to work around the core issue
11276
		setTimeout(runPositionItem);
11277
	});
11278
}
11279
/**
11280
 * The chart class
11281
 * @param {Object} options
11282
 * @param {Function} callback Function to run when the chart has loaded
11283
 */
11284
var Chart = Highcharts.Chart = function () {
11285
	this.init.apply(this, arguments);
11286
};
11287
 
11288
Chart.prototype = {
11289
 
11290
	/**
11291
	 * Hook for modules
11292
	 */
11293
	callbacks: [],
11294
 
11295
	/**
11296
	 * Initialize the chart
11297
	 */
11298
	init: function (userOptions, callback) {
11299
 
11300
		// Handle regular options
11301
		var options,
11302
			seriesOptions = userOptions.series; // skip merging data points to increase performance
11303
 
11304
		userOptions.series = null;
11305
		options = merge(defaultOptions, userOptions); // do the merge
11306
		options.series = userOptions.series = seriesOptions; // set back the series data
11307
		this.userOptions = userOptions;
11308
 
11309
		var optionsChart = options.chart;
11310
 
11311
		// Create margin & spacing array
11312
		this.margin = this.splashArray('margin', optionsChart);
11313
		this.spacing = this.splashArray('spacing', optionsChart);
11314
 
11315
		var chartEvents = optionsChart.events;
11316
 
11317
		//this.runChartClick = chartEvents && !!chartEvents.click;
11318
		this.bounds = { h: {}, v: {} }; // Pixel data bounds for touch zoom
11319
 
11320
		this.callback = callback;
11321
		this.isResizing = 0;
11322
		this.options = options;
11323
		//chartTitleOptions = UNDEFINED;
11324
		//chartSubtitleOptions = UNDEFINED;
11325
 
11326
		this.axes = [];
11327
		this.series = [];
11328
		this.hasCartesianSeries = optionsChart.showAxes;
11329
		//this.axisOffset = UNDEFINED;
11330
		//this.maxTicks = UNDEFINED; // handle the greatest amount of ticks on grouped axes
11331
		//this.inverted = UNDEFINED;
11332
		//this.loadingShown = UNDEFINED;
11333
		//this.container = UNDEFINED;
11334
		//this.chartWidth = UNDEFINED;
11335
		//this.chartHeight = UNDEFINED;
11336
		//this.marginRight = UNDEFINED;
11337
		//this.marginBottom = UNDEFINED;
11338
		//this.containerWidth = UNDEFINED;
11339
		//this.containerHeight = UNDEFINED;
11340
		//this.oldChartWidth = UNDEFINED;
11341
		//this.oldChartHeight = UNDEFINED;
11342
 
11343
		//this.renderTo = UNDEFINED;
11344
		//this.renderToClone = UNDEFINED;
11345
 
11346
		//this.spacingBox = UNDEFINED
11347
 
11348
		//this.legend = UNDEFINED;
11349
 
11350
		// Elements
11351
		//this.chartBackground = UNDEFINED;
11352
		//this.plotBackground = UNDEFINED;
11353
		//this.plotBGImage = UNDEFINED;
11354
		//this.plotBorder = UNDEFINED;
11355
		//this.loadingDiv = UNDEFINED;
11356
		//this.loadingSpan = UNDEFINED;
11357
 
11358
		var chart = this,
11359
			eventType;
11360
 
11361
		// Add the chart to the global lookup
11362
		chart.index = charts.length;
11363
		charts.push(chart);
11364
		chartCount++;
11365
 
11366
		// Set up auto resize
11367
		if (optionsChart.reflow !== false) {
11368
			addEvent(chart, 'load', function () {
11369
				chart.initReflow();
11370
			});
11371
		}
11372
 
11373
		// Chart event handlers
11374
		if (chartEvents) {
11375
			for (eventType in chartEvents) {
11376
				addEvent(chart, eventType, chartEvents[eventType]);
11377
			}
11378
		}
11379
 
11380
		chart.xAxis = [];
11381
		chart.yAxis = [];
11382
 
11383
		// Expose methods and variables
11384
		chart.animation = useCanVG ? false : pick(optionsChart.animation, true);
11385
		chart.pointCount = chart.colorCounter = chart.symbolCounter = 0;
11386
 
11387
		chart.firstRender();
11388
	},
11389
 
11390
	/**
11391
	 * Initialize an individual series, called internally before render time
11392
	 */
11393
	initSeries: function (options) {
11394
		var chart = this,
11395
			optionsChart = chart.options.chart,
11396
			type = options.type || optionsChart.type || optionsChart.defaultSeriesType,
11397
			series,
11398
			constr = seriesTypes[type];
11399
 
11400
		// No such series type
11401
		if (!constr) {
11402
			error(17, true);
11403
		}
11404
 
11405
		series = new constr();
11406
		series.init(this, options);
11407
		return series;
11408
	},
11409
 
11410
	/**
11411
	 * Check whether a given point is within the plot area
11412
	 *
11413
	 * @param {Number} plotX Pixel x relative to the plot area
11414
	 * @param {Number} plotY Pixel y relative to the plot area
11415
	 * @param {Boolean} inverted Whether the chart is inverted
11416
	 */
11417
	isInsidePlot: function (plotX, plotY, inverted) {
11418
		var x = inverted ? plotY : plotX,
11419
			y = inverted ? plotX : plotY;
11420
 
11421
		return x >= 0 &&
11422
			x <= this.plotWidth &&
11423
			y >= 0 &&
11424
			y <= this.plotHeight;
11425
	},
11426
 
11427
	/**
11428
	 * Redraw legend, axes or series based on updated data
11429
	 *
11430
	 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
11431
	 *    configuration
11432
	 */
11433
	redraw: function (animation) {
11434
		var chart = this,
11435
			axes = chart.axes,
11436
			series = chart.series,
11437
			pointer = chart.pointer,
11438
			legend = chart.legend,
11439
			redrawLegend = chart.isDirtyLegend,
11440
			hasStackedSeries,
11441
			hasDirtyStacks,
11442
			hasCartesianSeries = chart.hasCartesianSeries,
11443
			isDirtyBox = chart.isDirtyBox, // todo: check if it has actually changed?
11444
			seriesLength = series.length,
11445
			i = seriesLength,
11446
			serie,
11447
			renderer = chart.renderer,
11448
			isHiddenChart = renderer.isHidden(),
11449
			afterRedraw = [];
11450
 
11451
		setAnimation(animation, chart);
11452
 
11453
		if (isHiddenChart) {
11454
			chart.cloneRenderTo();
11455
		}
11456
 
11457
		// Adjust title layout (reflow multiline text)
11458
		chart.layOutTitles();
11459
 
11460
		// link stacked series
11461
		while (i--) {
11462
			serie = series[i];
11463
 
11464
			if (serie.options.stacking) {
11465
				hasStackedSeries = true;
11466
 
11467
				if (serie.isDirty) {
11468
					hasDirtyStacks = true;
11469
					break;
11470
				}
11471
			}
11472
		}
11473
		if (hasDirtyStacks) { // mark others as dirty
11474
			i = seriesLength;
11475
			while (i--) {
11476
				serie = series[i];
11477
				if (serie.options.stacking) {
11478
					serie.isDirty = true;
11479
				}
11480
			}
11481
		}
11482
 
11483
		// Handle updated data in the series
11484
		each(series, function (serie) {
11485
			if (serie.isDirty) {
11486
				if (serie.options.legendType === 'point') {
11487
					if (serie.updateTotals) {
11488
						serie.updateTotals();
11489
					}
11490
					redrawLegend = true;
11491
				}
11492
			}
11493
		});
11494
 
11495
		// handle added or removed series
11496
		if (redrawLegend && legend.options.enabled) { // series or pie points are added or removed
11497
			// draw legend graphics
11498
			legend.render();
11499
 
11500
			chart.isDirtyLegend = false;
11501
		}
11502
 
11503
		// reset stacks
11504
		if (hasStackedSeries) {
11505
			chart.getStacks();
11506
		}
11507
 
11508
 
11509
		if (hasCartesianSeries) {
11510
			if (!chart.isResizing) {
11511
 
11512
				// reset maxTicks
11513
				chart.maxTicks = null;
11514
 
11515
				// set axes scales
11516
				each(axes, function (axis) {
11517
					axis.setScale();
11518
				});
11519
			}
11520
		}
11521
 
11522
		chart.getMargins(); // #3098
11523
 
11524
		if (hasCartesianSeries) {
11525
			// If one axis is dirty, all axes must be redrawn (#792, #2169)
11526
			each(axes, function (axis) {
11527
				if (axis.isDirty) {
11528
					isDirtyBox = true;
11529
				}
11530
			});
11531
 
11532
			// redraw axes
11533
			each(axes, function (axis) {
11534
 
11535
				// Fire 'afterSetExtremes' only if extremes are set
11536
				var key = axis.min + ',' + axis.max;
11537
				if (axis.extKey !== key) { // #821, #4452
11538
					axis.extKey = key;
11539
					afterRedraw.push(function () { // prevent a recursive call to chart.redraw() (#1119)
11540
						fireEvent(axis, 'afterSetExtremes', extend(axis.eventArgs, axis.getExtremes())); // #747, #751
11541
						delete axis.eventArgs;
11542
					});
11543
				}
11544
				if (isDirtyBox || hasStackedSeries) {
11545
					axis.redraw();
11546
				}
11547
			});
11548
		}
11549
 
11550
		// the plot areas size has changed
11551
		if (isDirtyBox) {
11552
			chart.drawChartBox();
11553
		}
11554
 
11555
 
11556
		// redraw affected series
11557
		each(series, function (serie) {
11558
			if (serie.isDirty && serie.visible &&
11559
					(!serie.isCartesian || serie.xAxis)) { // issue #153
11560
				serie.redraw();
11561
			}
11562
		});
11563
 
11564
		// move tooltip or reset
11565
		if (pointer) {
11566
			pointer.reset(true);
11567
		}
11568
 
11569
		// redraw if canvas
11570
		renderer.draw();
11571
 
11572
		// fire the event
11573
		fireEvent(chart, 'redraw'); // jQuery breaks this when calling it from addEvent. Overwrites chart.redraw
11574
 
11575
		if (isHiddenChart) {
11576
			chart.cloneRenderTo(true);
11577
		}
11578
 
11579
		// Fire callbacks that are put on hold until after the redraw
11580
		each(afterRedraw, function (callback) {
11581
			callback.call();
11582
		});
11583
	},
11584
 
11585
	/**
11586
	 * Get an axis, series or point object by id.
11587
	 * @param id {String} The id as given in the configuration options
11588
	 */
11589
	get: function (id) {
11590
		var chart = this,
11591
			axes = chart.axes,
11592
			series = chart.series;
11593
 
11594
		var i,
11595
			j,
11596
			points;
11597
 
11598
		// search axes
11599
		for (i = 0; i < axes.length; i++) {
11600
			if (axes[i].options.id === id) {
11601
				return axes[i];
11602
			}
11603
		}
11604
 
11605
		// search series
11606
		for (i = 0; i < series.length; i++) {
11607
			if (series[i].options.id === id) {
11608
				return series[i];
11609
			}
11610
		}
11611
 
11612
		// search points
11613
		for (i = 0; i < series.length; i++) {
11614
			points = series[i].points || [];
11615
			for (j = 0; j < points.length; j++) {
11616
				if (points[j].id === id) {
11617
					return points[j];
11618
				}
11619
			}
11620
		}
11621
		return null;
11622
	},
11623
 
11624
	/**
11625
	 * Create the Axis instances based on the config options
11626
	 */
11627
	getAxes: function () {
11628
		var chart = this,
11629
			options = this.options,
11630
			xAxisOptions = options.xAxis = splat(options.xAxis || {}),
11631
			yAxisOptions = options.yAxis = splat(options.yAxis || {}),
11632
			optionsArray,
11633
			axis;
11634
 
11635
		// make sure the options are arrays and add some members
11636
		each(xAxisOptions, function (axis, i) {
11637
			axis.index = i;
11638
			axis.isX = true;
11639
		});
11640
 
11641
		each(yAxisOptions, function (axis, i) {
11642
			axis.index = i;
11643
		});
11644
 
11645
		// concatenate all axis options into one array
11646
		optionsArray = xAxisOptions.concat(yAxisOptions);
11647
 
11648
		each(optionsArray, function (axisOptions) {
11649
			axis = new Axis(chart, axisOptions);
11650
		});
11651
	},
11652
 
11653
 
11654
	/**
11655
	 * Get the currently selected points from all series
11656
	 */
11657
	getSelectedPoints: function () {
11658
		var points = [];
11659
		each(this.series, function (serie) {
11660
			points = points.concat(grep(serie.points || [], function (point) {
11661
				return point.selected;
11662
			}));
11663
		});
11664
		return points;
11665
	},
11666
 
11667
	/**
11668
	 * Get the currently selected series
11669
	 */
11670
	getSelectedSeries: function () {
11671
		return grep(this.series, function (serie) {
11672
			return serie.selected;
11673
		});
11674
	},
11675
 
11676
	/**
11677
	 * Show the title and subtitle of the chart
11678
	 *
11679
	 * @param titleOptions {Object} New title options
11680
	 * @param subtitleOptions {Object} New subtitle options
11681
	 *
11682
	 */
11683
	setTitle: function (titleOptions, subtitleOptions, redraw) {
11684
		var chart = this,
11685
			options = chart.options,
11686
			chartTitleOptions,
11687
			chartSubtitleOptions;
11688
 
11689
		chartTitleOptions = options.title = merge(options.title, titleOptions);
11690
		chartSubtitleOptions = options.subtitle = merge(options.subtitle, subtitleOptions);
11691
 
11692
		// add title and subtitle
11693
		each([
11694
			['title', titleOptions, chartTitleOptions],
11695
			['subtitle', subtitleOptions, chartSubtitleOptions]
11696
		], function (arr) {
11697
			var name = arr[0],
11698
				title = chart[name],
11699
				titleOptions = arr[1],
11700
				chartTitleOptions = arr[2];
11701
 
11702
			if (title && titleOptions) {
11703
				chart[name] = title = title.destroy(); // remove old
11704
			}
11705
 
11706
			if (chartTitleOptions && chartTitleOptions.text && !title) {
11707
				chart[name] = chart.renderer.text(
11708
					chartTitleOptions.text,
11709
					0,
11710
					0,
11711
					chartTitleOptions.useHTML
11712
				)
11713
				.attr({
11714
					align: chartTitleOptions.align,
11715
					'class': PREFIX + name,
11716
					zIndex: chartTitleOptions.zIndex || 4
11717
				})
11718
				.css(chartTitleOptions.style)
11719
				.add();
11720
			}
11721
		});
11722
		chart.layOutTitles(redraw);
11723
	},
11724
 
11725
	/**
11726
	 * Lay out the chart titles and cache the full offset height for use in getMargins
11727
	 */
11728
	layOutTitles: function (redraw) {
11729
		var titleOffset = 0,
11730
			title = this.title,
11731
			subtitle = this.subtitle,
11732
			options = this.options,
11733
			titleOptions = options.title,
11734
			subtitleOptions = options.subtitle,
11735
			requiresDirtyBox,
11736
			renderer = this.renderer,
11737
			autoWidth = this.spacingBox.width - 44; // 44 makes room for default context button
11738
 
11739
		if (title) {
11740
			title
11741
				.css({ width: (titleOptions.width || autoWidth) + PX })
11742
				.align(extend({
11743
					y: renderer.fontMetrics(titleOptions.style.fontSize, title).b - 3
11744
				}, titleOptions), false, 'spacingBox');
11745
 
11746
			if (!titleOptions.floating && !titleOptions.verticalAlign) {
11747
				titleOffset = title.getBBox().height;
11748
			}
11749
		}
11750
		if (subtitle) {
11751
			subtitle
11752
				.css({ width: (subtitleOptions.width || autoWidth) + PX })
11753
				.align(extend({
11754
					y: titleOffset + (titleOptions.margin - 13) + renderer.fontMetrics(titleOptions.style.fontSize, subtitle).b
11755
				}, subtitleOptions), false, 'spacingBox');
11756
 
11757
			if (!subtitleOptions.floating && !subtitleOptions.verticalAlign) {
11758
				titleOffset = mathCeil(titleOffset + subtitle.getBBox().height);
11759
			}
11760
		}
11761
 
11762
		requiresDirtyBox = this.titleOffset !== titleOffset;
11763
		this.titleOffset = titleOffset; // used in getMargins
11764
 
11765
		if (!this.isDirtyBox && requiresDirtyBox) {
11766
			this.isDirtyBox = requiresDirtyBox;
11767
			// Redraw if necessary (#2719, #2744)
11768
			if (this.hasRendered && pick(redraw, true) && this.isDirtyBox) {
11769
				this.redraw();
11770
			}
11771
		}
11772
	},
11773
 
11774
	/**
11775
	 * Get chart width and height according to options and container size
11776
	 */
11777
	getChartSize: function () {
11778
		var chart = this,
11779
			optionsChart = chart.options.chart,
11780
			widthOption = optionsChart.width,
11781
			heightOption = optionsChart.height,
11782
			renderTo = chart.renderToClone || chart.renderTo;
11783
 
11784
		// get inner width and height from jQuery (#824)
11785
		if (!defined(widthOption)) {
11786
			chart.containerWidth = adapterRun(renderTo, 'width');
11787
		}
11788
		if (!defined(heightOption)) {
11789
			chart.containerHeight = adapterRun(renderTo, 'height');
11790
		}
11791
 
11792
		chart.chartWidth = mathMax(0, widthOption || chart.containerWidth || 600); // #1393, 1460
11793
		chart.chartHeight = mathMax(0, pick(heightOption,
11794
			// the offsetHeight of an empty container is 0 in standard browsers, but 19 in IE7:
11795
			chart.containerHeight > 19 ? chart.containerHeight : 400));
11796
	},
11797
 
11798
	/**
11799
	 * Create a clone of the chart's renderTo div and place it outside the viewport to allow
11800
	 * size computation on chart.render and chart.redraw
11801
	 */
11802
	cloneRenderTo: function (revert) {
11803
		var clone = this.renderToClone,
11804
			container = this.container;
11805
 
11806
		// Destroy the clone and bring the container back to the real renderTo div
11807
		if (revert) {
11808
			if (clone) {
11809
				this.renderTo.appendChild(container);
11810
				discardElement(clone);
11811
				delete this.renderToClone;
11812
			}
11813
 
11814
		// Set up the clone
11815
		} else {
11816
			if (container && container.parentNode === this.renderTo) {
11817
				this.renderTo.removeChild(container); // do not clone this
11818
			}
11819
			this.renderToClone = clone = this.renderTo.cloneNode(0);
11820
			css(clone, {
11821
				position: ABSOLUTE,
11822
				top: '-9999px',
11823
				display: 'block' // #833
11824
			});
11825
			if (clone.style.setProperty) { // #2631
11826
				clone.style.setProperty('display', 'block', 'important');
11827
			}
11828
			doc.body.appendChild(clone);
11829
			if (container) {
11830
				clone.appendChild(container);
11831
			}
11832
		}
11833
	},
11834
 
11835
	/**
11836
	 * Get the containing element, determine the size and create the inner container
11837
	 * div to hold the chart
11838
	 */
11839
	getContainer: function () {
11840
		var chart = this,
11841
			container,
11842
			optionsChart = chart.options.chart,
11843
			chartWidth,
11844
			chartHeight,
11845
			renderTo,
11846
			indexAttrName = 'data-highcharts-chart',
11847
			oldChartIndex,
11848
			containerId;
11849
 
11850
		chart.renderTo = renderTo = optionsChart.renderTo;
11851
		containerId = PREFIX + idCounter++;
11852
 
11853
		if (isString(renderTo)) {
11854
			chart.renderTo = renderTo = doc.getElementById(renderTo);
11855
		}
11856
 
11857
		// Display an error if the renderTo is wrong
11858
		if (!renderTo) {
11859
			error(13, true);
11860
		}
11861
 
11862
		// If the container already holds a chart, destroy it. The check for hasRendered is there
11863
		// because web pages that are saved to disk from the browser, will preserve the data-highcharts-chart
11864
		// attribute and the SVG contents, but not an interactive chart. So in this case,
11865
		// charts[oldChartIndex] will point to the wrong chart if any (#2609).
11866
		oldChartIndex = pInt(attr(renderTo, indexAttrName));
11867
		if (!isNaN(oldChartIndex) && charts[oldChartIndex] && charts[oldChartIndex].hasRendered) {
11868
			charts[oldChartIndex].destroy();
11869
		}
11870
 
11871
		// Make a reference to the chart from the div
11872
		attr(renderTo, indexAttrName, chart.index);
11873
 
11874
		// remove previous chart
11875
		renderTo.innerHTML = '';
11876
 
11877
		// If the container doesn't have an offsetWidth, it has or is a child of a node
11878
		// that has display:none. We need to temporarily move it out to a visible
11879
		// state to determine the size, else the legend and tooltips won't render
11880
		// properly. The allowClone option is used in sparklines as a micro optimization,
11881
		// saving about 1-2 ms each chart.
11882
		if (!optionsChart.skipClone && !renderTo.offsetWidth) {
11883
			chart.cloneRenderTo();
11884
		}
11885
 
11886
		// get the width and height
11887
		chart.getChartSize();
11888
		chartWidth = chart.chartWidth;
11889
		chartHeight = chart.chartHeight;
11890
 
11891
		// create the inner container
11892
		chart.container = container = createElement(DIV, {
11893
				className: PREFIX + 'container' +
11894
					(optionsChart.className ? ' ' + optionsChart.className : ''),
11895
				id: containerId
11896
			}, extend({
11897
				position: RELATIVE,
11898
				overflow: HIDDEN, // needed for context menu (avoid scrollbars) and
11899
					// content overflow in IE
11900
				width: chartWidth + PX,
11901
				height: chartHeight + PX,
11902
				textAlign: 'left',
11903
				lineHeight: 'normal', // #427
11904
				zIndex: 0, // #1072
11905
				'-webkit-tap-highlight-color': 'rgba(0,0,0,0)'
11906
			}, optionsChart.style),
11907
			chart.renderToClone || renderTo
11908
		);
11909
 
11910
		// cache the cursor (#1650)
11911
		chart._cursor = container.style.cursor;
11912
 
11913
		// Initialize the renderer
11914
		chart.renderer =
11915
			optionsChart.forExport ? // force SVG, used for SVG export
11916
				new SVGRenderer(container, chartWidth, chartHeight, optionsChart.style, true) :
11917
				new Renderer(container, chartWidth, chartHeight, optionsChart.style);
11918
 
11919
		if (useCanVG) {
11920
			// If we need canvg library, extend and configure the renderer
11921
			// to get the tracker for translating mouse events
11922
			chart.renderer.create(chart, container, chartWidth, chartHeight);
11923
		}
11924
		// Add a reference to the charts index
11925
		chart.renderer.chartIndex = chart.index;
11926
	},
11927
 
11928
	/**
11929
	 * Calculate margins by rendering axis labels in a preliminary position. Title,
11930
	 * subtitle and legend have already been rendered at this stage, but will be
11931
	 * moved into their final positions
11932
	 */
11933
	getMargins: function (skipAxes) {
11934
		var chart = this,
11935
			spacing = chart.spacing,
11936
			margin = chart.margin,
11937
			titleOffset = chart.titleOffset;
11938
 
11939
		chart.resetMargins();
11940
 
11941
		// Adjust for title and subtitle
11942
		if (titleOffset && !defined(margin[0])) {
11943
			chart.plotTop = mathMax(chart.plotTop, titleOffset + chart.options.title.margin + spacing[0]);
11944
		}
11945
 
11946
		// Adjust for legend
11947
		chart.legend.adjustMargins(margin, spacing);
11948
 
11949
		// adjust for scroller
11950
		if (chart.extraBottomMargin) {
11951
			chart.marginBottom += chart.extraBottomMargin;
11952
		}
11953
		if (chart.extraTopMargin) {
11954
			chart.plotTop += chart.extraTopMargin;
11955
		}
11956
		if (!skipAxes) {
11957
			this.getAxisMargins();
11958
		}
11959
	},
11960
 
11961
	getAxisMargins: function () {
11962
 
11963
		var chart = this,
11964
			axisOffset = chart.axisOffset = [0, 0, 0, 0], // top, right, bottom, left
11965
			margin = chart.margin;
11966
 
11967
		// pre-render axes to get labels offset width
11968
		if (chart.hasCartesianSeries) {
11969
			each(chart.axes, function (axis) {
11970
				axis.getOffset();
11971
			});
11972
		}
11973
 
11974
		// Add the axis offsets
11975
		each(marginNames, function (m, side) {
11976
			if (!defined(margin[side])) {
11977
				chart[m] += axisOffset[side];
11978
			}
11979
		});
11980
 
11981
		chart.setChartSize();
11982
 
11983
	},
11984
 
11985
	/**
11986
	 * Resize the chart to its container if size is not explicitly set
11987
	 */
11988
	reflow: function (e) {
11989
		var chart = this,
11990
			optionsChart = chart.options.chart,
11991
			renderTo = chart.renderTo,
11992
			width = optionsChart.width || adapterRun(renderTo, 'width'),
11993
			height = optionsChart.height || adapterRun(renderTo, 'height'),
11994
			target = e ? e.target : win, // #805 - MooTools doesn't supply e
11995
			doReflow = function () {
11996
				if (chart.container) { // It may have been destroyed in the meantime (#1257)
11997
					chart.setSize(width, height, false);
11998
					chart.hasUserSize = null;
11999
				}
12000
			};
12001
 
12002
		// Width and height checks for display:none. Target is doc in IE8 and Opera,
12003
		// win in Firefox, Chrome and IE9.
12004
		if (!chart.hasUserSize && !chart.isPrinting && width && height && (target === win || target === doc)) { // #1093
12005
			if (width !== chart.containerWidth || height !== chart.containerHeight) {
12006
				clearTimeout(chart.reflowTimeout);
12007
				if (e) { // Called from window.resize
12008
					chart.reflowTimeout = setTimeout(doReflow, 100);
12009
				} else { // Called directly (#2224)
12010
					doReflow();
12011
				}
12012
			}
12013
			chart.containerWidth = width;
12014
			chart.containerHeight = height;
12015
		}
12016
	},
12017
 
12018
	/**
12019
	 * Add the event handlers necessary for auto resizing
12020
	 */
12021
	initReflow: function () {
12022
		var chart = this,
12023
			reflow = function (e) {
12024
				chart.reflow(e);
12025
			};
12026
 
12027
 
12028
		addEvent(win, 'resize', reflow);
12029
		addEvent(chart, 'destroy', function () {
12030
			removeEvent(win, 'resize', reflow);
12031
		});
12032
	},
12033
 
12034
	/**
12035
	 * Resize the chart to a given width and height
12036
	 * @param {Number} width
12037
	 * @param {Number} height
12038
	 * @param {Object|Boolean} animation
12039
	 */
12040
	setSize: function (width, height, animation) {
12041
		var chart = this,
12042
			chartWidth,
12043
			chartHeight,
12044
			fireEndResize,
12045
			renderer = chart.renderer,
12046
			globalAnimation = renderer.globalAnimation;
12047
 
12048
		// Handle the isResizing counter
12049
		chart.isResizing += 1;
12050
		fireEndResize = function () {
12051
			if (chart) {
12052
				fireEvent(chart, 'endResize', null, function () {
12053
					chart.isResizing -= 1;
12054
				});
12055
			}
12056
		};
12057
 
12058
		// set the animation for the current process
12059
		setAnimation(animation, chart);
12060
 
12061
		chart.oldChartHeight = chart.chartHeight;
12062
		chart.oldChartWidth = chart.chartWidth;
12063
		if (defined(width)) {
12064
			chart.chartWidth = chartWidth = mathMax(0, mathRound(width));
12065
			chart.hasUserSize = !!chartWidth;
12066
		}
12067
		if (defined(height)) {
12068
			chart.chartHeight = chartHeight = mathMax(0, mathRound(height));
12069
		}
12070
 
12071
		// Resize the container with the global animation applied if enabled (#2503)
12072
		(globalAnimation ? animate : css)(chart.container, {
12073
			width: chartWidth + PX,
12074
			height: chartHeight + PX
12075
		}, globalAnimation);
12076
 
12077
		chart.setChartSize(true);
12078
		renderer.setSize(chartWidth, chartHeight, animation);
12079
 
12080
		// handle axes
12081
		chart.maxTicks = null;
12082
		each(chart.axes, function (axis) {
12083
			axis.isDirty = true;
12084
			axis.setScale();
12085
		});
12086
 
12087
		// make sure non-cartesian series are also handled
12088
		each(chart.series, function (serie) {
12089
			serie.isDirty = true;
12090
		});
12091
 
12092
		chart.isDirtyLegend = true; // force legend redraw
12093
		chart.isDirtyBox = true; // force redraw of plot and chart border
12094
 
12095
		chart.layOutTitles(); // #2857
12096
		chart.getMargins();
12097
 
12098
		chart.redraw(animation);
12099
 
12100
 
12101
		chart.oldChartHeight = null;
12102
		fireEvent(chart, 'resize');
12103
 
12104
		// fire endResize and set isResizing back
12105
		// If animation is disabled, fire without delay
12106
		if (globalAnimation === false) {
12107
			fireEndResize();
12108
		} else { // else set a timeout with the animation duration
12109
			setTimeout(fireEndResize, (globalAnimation && globalAnimation.duration) || 500);
12110
		}
12111
	},
12112
 
12113
	/**
12114
	 * Set the public chart properties. This is done before and after the pre-render
12115
	 * to determine margin sizes
12116
	 */
12117
	setChartSize: function (skipAxes) {
12118
		var chart = this,
12119
			inverted = chart.inverted,
12120
			renderer = chart.renderer,
12121
			chartWidth = chart.chartWidth,
12122
			chartHeight = chart.chartHeight,
12123
			optionsChart = chart.options.chart,
12124
			spacing = chart.spacing,
12125
			clipOffset = chart.clipOffset,
12126
			clipX,
12127
			clipY,
12128
			plotLeft,
12129
			plotTop,
12130
			plotWidth,
12131
			plotHeight,
12132
			plotBorderWidth;
12133
 
12134
		chart.plotLeft = plotLeft = mathRound(chart.plotLeft);
12135
		chart.plotTop = plotTop = mathRound(chart.plotTop);
12136
		chart.plotWidth = plotWidth = mathMax(0, mathRound(chartWidth - plotLeft - chart.marginRight));
12137
		chart.plotHeight = plotHeight = mathMax(0, mathRound(chartHeight - plotTop - chart.marginBottom));
12138
 
12139
		chart.plotSizeX = inverted ? plotHeight : plotWidth;
12140
		chart.plotSizeY = inverted ? plotWidth : plotHeight;
12141
 
12142
		chart.plotBorderWidth = optionsChart.plotBorderWidth || 0;
12143
 
12144
		// Set boxes used for alignment
12145
		chart.spacingBox = renderer.spacingBox = {
12146
			x: spacing[3],
12147
			y: spacing[0],
12148
			width: chartWidth - spacing[3] - spacing[1],
12149
			height: chartHeight - spacing[0] - spacing[2]
12150
		};
12151
		chart.plotBox = renderer.plotBox = {
12152
			x: plotLeft,
12153
			y: plotTop,
12154
			width: plotWidth,
12155
			height: plotHeight
12156
		};
12157
 
12158
		plotBorderWidth = 2 * mathFloor(chart.plotBorderWidth / 2);
12159
		clipX = mathCeil(mathMax(plotBorderWidth, clipOffset[3]) / 2);
12160
		clipY = mathCeil(mathMax(plotBorderWidth, clipOffset[0]) / 2);
12161
		chart.clipBox = {
12162
			x: clipX,
12163
			y: clipY,
12164
			width: mathFloor(chart.plotSizeX - mathMax(plotBorderWidth, clipOffset[1]) / 2 - clipX),
12165
			height: mathMax(0, mathFloor(chart.plotSizeY - mathMax(plotBorderWidth, clipOffset[2]) / 2 - clipY))
12166
		};
12167
 
12168
		if (!skipAxes) {
12169
			each(chart.axes, function (axis) {
12170
				axis.setAxisSize();
12171
				axis.setAxisTranslation();
12172
			});
12173
		}
12174
	},
12175
 
12176
	/**
12177
	 * Initial margins before auto size margins are applied
12178
	 */
12179
	resetMargins: function () {
12180
		var chart = this;
12181
 
12182
		each(marginNames, function (m, side) {
12183
			chart[m] = pick(chart.margin[side], chart.spacing[side]);
12184
		});
12185
		chart.axisOffset = [0, 0, 0, 0]; // top, right, bottom, left
12186
		chart.clipOffset = [0, 0, 0, 0];
12187
	},
12188
 
12189
	/**
12190
	 * Draw the borders and backgrounds for chart and plot area
12191
	 */
12192
	drawChartBox: function () {
12193
		var chart = this,
12194
			optionsChart = chart.options.chart,
12195
			renderer = chart.renderer,
12196
			chartWidth = chart.chartWidth,
12197
			chartHeight = chart.chartHeight,
12198
			chartBackground = chart.chartBackground,
12199
			plotBackground = chart.plotBackground,
12200
			plotBorder = chart.plotBorder,
12201
			plotBGImage = chart.plotBGImage,
12202
			chartBorderWidth = optionsChart.borderWidth || 0,
12203
			chartBackgroundColor = optionsChart.backgroundColor,
12204
			plotBackgroundColor = optionsChart.plotBackgroundColor,
12205
			plotBackgroundImage = optionsChart.plotBackgroundImage,
12206
			plotBorderWidth = optionsChart.plotBorderWidth || 0,
12207
			mgn,
12208
			bgAttr,
12209
			plotLeft = chart.plotLeft,
12210
			plotTop = chart.plotTop,
12211
			plotWidth = chart.plotWidth,
12212
			plotHeight = chart.plotHeight,
12213
			plotBox = chart.plotBox,
12214
			clipRect = chart.clipRect,
12215
			clipBox = chart.clipBox;
12216
 
12217
		// Chart area
12218
		mgn = chartBorderWidth + (optionsChart.shadow ? 8 : 0);
12219
 
12220
		if (chartBorderWidth || chartBackgroundColor) {
12221
			if (!chartBackground) {
12222
 
12223
				bgAttr = {
12224
					fill: chartBackgroundColor || NONE
12225
				};
12226
				if (chartBorderWidth) { // #980
12227
					bgAttr.stroke = optionsChart.borderColor;
12228
					bgAttr['stroke-width'] = chartBorderWidth;
12229
				}
12230
				chart.chartBackground = renderer.rect(mgn / 2, mgn / 2, chartWidth - mgn, chartHeight - mgn,
12231
						optionsChart.borderRadius, chartBorderWidth)
12232
					.attr(bgAttr)
12233
					.addClass(PREFIX + 'background')
12234
					.add()
12235
					.shadow(optionsChart.shadow);
12236
 
12237
			} else { // resize
12238
				chartBackground.animate(
12239
					chartBackground.crisp({ width: chartWidth - mgn, height: chartHeight - mgn })
12240
				);
12241
			}
12242
		}
12243
 
12244
 
12245
		// Plot background
12246
		if (plotBackgroundColor) {
12247
			if (!plotBackground) {
12248
				chart.plotBackground = renderer.rect(plotLeft, plotTop, plotWidth, plotHeight, 0)
12249
					.attr({
12250
						fill: plotBackgroundColor
12251
					})
12252
					.add()
12253
					.shadow(optionsChart.plotShadow);
12254
			} else {
12255
				plotBackground.animate(plotBox);
12256
			}
12257
		}
12258
		if (plotBackgroundImage) {
12259
			if (!plotBGImage) {
12260
				chart.plotBGImage = renderer.image(plotBackgroundImage, plotLeft, plotTop, plotWidth, plotHeight)
12261
					.add();
12262
			} else {
12263
				plotBGImage.animate(plotBox);
12264
			}
12265
		}
12266
 
12267
		// Plot clip
12268
		if (!clipRect) {
12269
			chart.clipRect = renderer.clipRect(clipBox);
12270
		} else {
12271
			clipRect.animate({
12272
				width: clipBox.width,
12273
				height: clipBox.height
12274
			});
12275
		}
12276
 
12277
		// Plot area border
12278
		if (plotBorderWidth) {
12279
			if (!plotBorder) {
12280
				chart.plotBorder = renderer.rect(plotLeft, plotTop, plotWidth, plotHeight, 0, -plotBorderWidth)
12281
					.attr({
12282
						stroke: optionsChart.plotBorderColor,
12283
						'stroke-width': plotBorderWidth,
12284
						fill: NONE,
12285
						zIndex: 1
12286
					})
12287
					.add();
12288
			} else {
12289
				plotBorder.animate(
12290
					plotBorder.crisp({ x: plotLeft, y: plotTop, width: plotWidth, height: plotHeight, strokeWidth: -plotBorderWidth }) //#3282 plotBorder should be negative
12291
				);
12292
			}
12293
		}
12294
 
12295
		// reset
12296
		chart.isDirtyBox = false;
12297
	},
12298
 
12299
	/**
12300
	 * Detect whether a certain chart property is needed based on inspecting its options
12301
	 * and series. This mainly applies to the chart.invert property, and in extensions to
12302
	 * the chart.angular and chart.polar properties.
12303
	 */
12304
	propFromSeries: function () {
12305
		var chart = this,
12306
			optionsChart = chart.options.chart,
12307
			klass,
12308
			seriesOptions = chart.options.series,
12309
			i,
12310
			value;
12311
 
12312
 
12313
		each(['inverted', 'angular', 'polar'], function (key) {
12314
 
12315
			// The default series type's class
12316
			klass = seriesTypes[optionsChart.type || optionsChart.defaultSeriesType];
12317
 
12318
			// Get the value from available chart-wide properties
12319
			value = (
12320
				chart[key] || // 1. it is set before
12321
				optionsChart[key] || // 2. it is set in the options
12322
				(klass && klass.prototype[key]) // 3. it's default series class requires it
12323
			);
12324
 
12325
			// 4. Check if any the chart's series require it
12326
			i = seriesOptions && seriesOptions.length;
12327
			while (!value && i--) {
12328
				klass = seriesTypes[seriesOptions[i].type];
12329
				if (klass && klass.prototype[key]) {
12330
					value = true;
12331
				}
12332
			}
12333
 
12334
			// Set the chart property
12335
			chart[key] = value;
12336
		});
12337
 
12338
	},
12339
 
12340
	/**
12341
	 * Link two or more series together. This is done initially from Chart.render,
12342
	 * and after Chart.addSeries and Series.remove.
12343
	 */
12344
	linkSeries: function () {
12345
		var chart = this,
12346
			chartSeries = chart.series;
12347
 
12348
		// Reset links
12349
		each(chartSeries, function (series) {
12350
			series.linkedSeries.length = 0;
12351
		});
12352
 
12353
		// Apply new links
12354
		each(chartSeries, function (series) {
12355
			var linkedTo = series.options.linkedTo;
12356
			if (isString(linkedTo)) {
12357
				if (linkedTo === ':previous') {
12358
					linkedTo = chart.series[series.index - 1];
12359
				} else {
12360
					linkedTo = chart.get(linkedTo);
12361
				}
12362
				if (linkedTo) {
12363
					linkedTo.linkedSeries.push(series);
12364
					series.linkedParent = linkedTo;
12365
					series.visible = pick(series.options.visible, linkedTo.options.visible, series.visible); // #3879
12366
				}
12367
			}
12368
		});
12369
	},
12370
 
12371
	/**
12372
	 * Render series for the chart
12373
	 */
12374
	renderSeries: function () {
12375
		each(this.series, function (serie) {
12376
			serie.translate();
12377
			serie.render();
12378
		});
12379
	},
12380
 
12381
	/**
12382
	 * Render labels for the chart
12383
	 */
12384
	renderLabels: function () {
12385
		var chart = this,
12386
			labels = chart.options.labels;
12387
		if (labels.items) {
12388
			each(labels.items, function (label) {
12389
				var style = extend(labels.style, label.style),
12390
					x = pInt(style.left) + chart.plotLeft,
12391
					y = pInt(style.top) + chart.plotTop + 12;
12392
 
12393
				// delete to prevent rewriting in IE
12394
				delete style.left;
12395
				delete style.top;
12396
 
12397
				chart.renderer.text(
12398
					label.html,
12399
					x,
12400
					y
12401
				)
12402
				.attr({ zIndex: 2 })
12403
				.css(style)
12404
				.add();
12405
 
12406
			});
12407
		}
12408
	},
12409
 
12410
	/**
12411
	 * Render all graphics for the chart
12412
	 */
12413
	render: function () {
12414
		var chart = this,
12415
			axes = chart.axes,
12416
			renderer = chart.renderer,
12417
			options = chart.options,
12418
			tempWidth,
12419
			tempHeight,
12420
			redoHorizontal,
12421
			redoVertical;
12422
 
12423
		// Title
12424
		chart.setTitle();
12425
 
12426
 
12427
		// Legend
12428
		chart.legend = new Legend(chart, options.legend);
12429
 
12430
		// Get stacks
12431
		if (chart.getStacks) {
12432
			chart.getStacks();
12433
		}
12434
 
12435
		// Get chart margins
12436
		chart.getMargins(true);
12437
		chart.setChartSize();
12438
 
12439
		// Record preliminary dimensions for later comparison
12440
		tempWidth = chart.plotWidth;
12441
		tempHeight = chart.plotHeight = chart.plotHeight - 13; // 13 is the most common height of X axis labels
12442
 
12443
		// Get margins by pre-rendering axes
12444
		each(axes, function (axis) {
12445
			axis.setScale();
12446
		});
12447
		chart.getAxisMargins();
12448
 
12449
		// If the plot area size has changed significantly, calculate tick positions again
12450
		redoHorizontal = tempWidth / chart.plotWidth > 1.1;
12451
		redoVertical = tempHeight / chart.plotHeight > 1.1;
12452
 
12453
		if (redoHorizontal || redoVertical) {
12454
 
12455
			chart.maxTicks = null; // reset for second pass
12456
			each(axes, function (axis) {
12457
				if ((axis.horiz && redoHorizontal) || (!axis.horiz && redoVertical)) {
12458
					axis.setTickInterval(true); // update to reflect the new margins
12459
				}
12460
			});
12461
			chart.getMargins(); // second pass to check for new labels
12462
		}
12463
 
12464
		// Draw the borders and backgrounds
12465
		chart.drawChartBox();
12466
 
12467
 
12468
		// Axes
12469
		if (chart.hasCartesianSeries) {
12470
			each(axes, function (axis) {
12471
				axis.render();
12472
			});
12473
		}
12474
 
12475
		// The series
12476
		if (!chart.seriesGroup) {
12477
			chart.seriesGroup = renderer.g('series-group')
12478
				.attr({ zIndex: 3 })
12479
				.add();
12480
		}
12481
		chart.renderSeries();
12482
 
12483
		// Labels
12484
		chart.renderLabels();
12485
 
12486
		// Credits
12487
		chart.showCredits(options.credits);
12488
 
12489
		// Set flag
12490
		chart.hasRendered = true;
12491
 
12492
	},
12493
 
12494
	/**
12495
	 * Show chart credits based on config options
12496
	 */
12497
	showCredits: function (credits) {
12498
		if (credits.enabled && !this.credits) {
12499
			this.credits = this.renderer.text(
12500
				credits.text,
12501
				0,
12502
 
12503
			)
12504
			.on('click', function () {
12505
				if (credits.href) {
12506
					location.href = credits.href;
12507
				}
12508
			})
12509
			.attr({
12510
				align: credits.position.align,
12511
				zIndex: 8
12512
			})
12513
			.css(credits.style)
12514
			.add()
12515
			.align(credits.position);
12516
		}
12517
	},
12518
 
12519
	/**
12520
	 * Clean up memory usage
12521
	 */
12522
	destroy: function () {
12523
		var chart = this,
12524
			axes = chart.axes,
12525
			series = chart.series,
12526
			container = chart.container,
12527
			i,
12528
			parentNode = container && container.parentNode;
12529
 
12530
		// fire the chart.destoy event
12531
		fireEvent(chart, 'destroy');
12532
 
12533
		// Delete the chart from charts lookup array
12534
		charts[chart.index] = UNDEFINED;
12535
		chartCount--;
12536
		chart.renderTo.removeAttribute('data-highcharts-chart');
12537
 
12538
		// remove events
12539
		removeEvent(chart);
12540
 
12541
		// ==== Destroy collections:
12542
		// Destroy axes
12543
		i = axes.length;
12544
		while (i--) {
12545
			axes[i] = axes[i].destroy();
12546
		}
12547
 
12548
		// Destroy each series
12549
		i = series.length;
12550
		while (i--) {
12551
			series[i] = series[i].destroy();
12552
		}
12553
 
12554
		// ==== Destroy chart properties:
12555
		each(['title', 'subtitle', 'chartBackground', 'plotBackground', 'plotBGImage',
12556
				'plotBorder', 'seriesGroup', 'clipRect', 'credits', 'pointer', 'scroller',
12557
				'rangeSelector', 'legend', 'resetZoomButton', 'tooltip', 'renderer'], function (name) {
12558
			var prop = chart[name];
12559
 
12560
			if (prop && prop.destroy) {
12561
				chart[name] = prop.destroy();
12562
			}
12563
		});
12564
 
12565
		// remove container and all SVG
12566
		if (container) { // can break in IE when destroyed before finished loading
12567
			container.innerHTML = '';
12568
			removeEvent(container);
12569
			if (parentNode) {
12570
				discardElement(container);
12571
			}
12572
 
12573
		}
12574
 
12575
		// clean it all up
12576
		for (i in chart) {
12577
			delete chart[i];
12578
		}
12579
 
12580
	},
12581
 
12582
 
12583
	/**
12584
	 * VML namespaces can't be added until after complete. Listening
12585
	 * for Perini's doScroll hack is not enough.
12586
	 */
12587
	isReadyToRender: function () {
12588
		var chart = this;
12589
 
12590
		// Note: in spite of JSLint's complaints, win == win.top is required
12591
		/*jslint eqeq: true*/
12592
		if ((!hasSVG && (win == win.top && doc.readyState !== 'complete')) || (useCanVG && !win.canvg)) {
12593
		/*jslint eqeq: false*/
12594
			if (useCanVG) {
12595
				// Delay rendering until canvg library is downloaded and ready
12596
				CanVGController.push(function () { chart.firstRender(); }, chart.options.global.canvasToolsURL);
12597
			} else {
12598
				doc.attachEvent('onreadystatechange', function () {
12599
					doc.detachEvent('onreadystatechange', chart.firstRender);
12600
					if (doc.readyState === 'complete') {
12601
						chart.firstRender();
12602
					}
12603
				});
12604
			}
12605
			return false;
12606
		}
12607
		return true;
12608
	},
12609
 
12610
	/**
12611
	 * Prepare for first rendering after all data are loaded
12612
	 */
12613
	firstRender: function () {
12614
		var chart = this,
12615
			options = chart.options,
12616
			callback = chart.callback;
12617
 
12618
		// Check whether the chart is ready to render
12619
		if (!chart.isReadyToRender()) {
12620
			return;
12621
		}
12622
 
12623
		// Create the container
12624
		chart.getContainer();
12625
 
12626
		// Run an early event after the container and renderer are established
12627
		fireEvent(chart, 'init');
12628
 
12629
 
12630
		chart.resetMargins();
12631
		chart.setChartSize();
12632
 
12633
		// Set the common chart properties (mainly invert) from the given series
12634
		chart.propFromSeries();
12635
 
12636
		// get axes
12637
		chart.getAxes();
12638
 
12639
		// Initialize the series
12640
		each(options.series || [], function (serieOptions) {
12641
			chart.initSeries(serieOptions);
12642
		});
12643
 
12644
		chart.linkSeries();
12645
 
12646
		// Run an event after axes and series are initialized, but before render. At this stage,
12647
		// the series data is indexed and cached in the xData and yData arrays, so we can access
12648
		// those before rendering. Used in Highstock.
12649
		fireEvent(chart, 'beforeRender');
12650
 
12651
		// depends on inverted and on margins being set
12652
		if (Highcharts.Pointer) {
12653
			chart.pointer = new Pointer(chart, options);
12654
		}
12655
 
12656
		chart.render();
12657
 
12658
		// add canvas
12659
		chart.renderer.draw();
12660
		// run callbacks
12661
		if (callback) {
12662
			callback.apply(chart, [chart]);
12663
		}
12664
		each(chart.callbacks, function (fn) {
12665
			if (chart.index !== UNDEFINED) { // Chart destroyed in its own callback (#3600)
12666
				fn.apply(chart, [chart]);
12667
			}
12668
		});
12669
 
12670
		// Fire the load event
12671
		fireEvent(chart, 'load');
12672
 
12673
		// If the chart was rendered outside the top container, put it back in (#3679)
12674
		chart.cloneRenderTo(true);
12675
 
12676
	},
12677
 
12678
	/**
12679
	* Creates arrays for spacing and margin from given options.
12680
	*/
12681
	splashArray: function (target, options) {
12682
		var oVar = options[target],
12683
			tArray = isObject(oVar) ? oVar : [oVar, oVar, oVar, oVar];
12684
 
12685
		return [pick(options[target + 'Top'], tArray[0]),
12686
				pick(options[target + 'Right'], tArray[1]),
12687
				pick(options[target + 'Bottom'], tArray[2]),
12688
				pick(options[target + 'Left'], tArray[3])];
12689
	}
12690
}; // end Chart
12691
 
12692
var CenteredSeriesMixin = Highcharts.CenteredSeriesMixin = {
12693
	/**
12694
	 * Get the center of the pie based on the size and center options relative to the
12695
	 * plot area. Borrowed by the polar and gauge series types.
12696
	 */
12697
	getCenter: function () {
12698
 
12699
		var options = this.options,
12700
			chart = this.chart,
12701
			slicingRoom = 2 * (options.slicedOffset || 0),
12702
			handleSlicingRoom,
12703
			plotWidth = chart.plotWidth - 2 * slicingRoom,
12704
			plotHeight = chart.plotHeight - 2 * slicingRoom,
12705
			centerOption = options.center,
12706
			positions = [pick(centerOption[0], '50%'), pick(centerOption[1], '50%'), options.size || '100%', options.innerSize || 0],
12707
			smallestSize = mathMin(plotWidth, plotHeight),
12708
			i,
12709
			value;
12710
 
12711
		for (i = 0; i < 4; ++i) {
12712
			value = positions[i];
12713
			handleSlicingRoom = i < 2 || (i === 2 && /%$/.test(value));
12714
 
12715
			// i == 0: centerX, relative to width
12716
			// i == 1: centerY, relative to height
12717
			// i == 2: size, relative to smallestSize
12718
			// i == 3: innerSize, relative to size
12719
			positions[i] = relativeLength(value, [plotWidth, plotHeight, smallestSize, positions[2]][i]) +
12720
				(handleSlicingRoom ? slicingRoom : 0);
12721
 
12722
		}
12723
		return positions;
12724
	}
12725
};
12726
 
12727
/**
12728
 * The Point object and prototype. Inheritable and used as base for PiePoint
12729
 */
12730
var Point = function () {};
12731
Point.prototype = {
12732
 
12733
	/**
12734
	 * Initialize the point
12735
	 * @param {Object} series The series object containing this point
12736
	 * @param {Object} options The data in either number, array or object format
12737
	 */
12738
	init: function (series, options, x) {
12739
 
12740
		var point = this,
12741
			colors;
12742
		point.series = series;
12743
		point.color = series.color; // #3445
12744
		point.applyOptions(options, x);
12745
		point.pointAttr = {};
12746
 
12747
		if (series.options.colorByPoint) {
12748
			colors = series.options.colors || series.chart.options.colors;
12749
			point.color = point.color || colors[series.colorCounter++];
12750
			// loop back to zero
12751
			if (series.colorCounter === colors.length) {
12752
				series.colorCounter = 0;
12753
			}
12754
		}
12755
 
12756
		series.chart.pointCount++;
12757
		return point;
12758
	},
12759
	/**
12760
	 * Apply the options containing the x and y data and possible some extra properties.
12761
	 * This is called on point init or from point.update.
12762
	 *
12763
	 * @param {Object} options
12764
	 */
12765
	applyOptions: function (options, x) {
12766
		var point = this,
12767
			series = point.series,
12768
			pointValKey = series.options.pointValKey || series.pointValKey;
12769
 
12770
		options = Point.prototype.optionsToObject.call(this, options);
12771
 
12772
		// copy options directly to point
12773
		extend(point, options);
12774
		point.options = point.options ? extend(point.options, options) : options;
12775
 
12776
		// For higher dimension series types. For instance, for ranges, point.y is mapped to point.low.
12777
		if (pointValKey) {
12778
			point.y = point[pointValKey];
12779
		}
12780
 
12781
		// If no x is set by now, get auto incremented value. All points must have an
12782
		// x value, however the y value can be null to create a gap in the series
12783
		if (point.x === UNDEFINED && series) {
12784
			point.x = x === UNDEFINED ? series.autoIncrement() : x;
12785
		}
12786
 
12787
		return point;
12788
	},
12789
 
12790
	/**
12791
	 * Transform number or array configs into objects
12792
	 */
12793
	optionsToObject: function (options) {
12794
		var ret = {},
12795
			series = this.series,
12796
			keys = series.options.keys,
12797
			pointArrayMap = keys || series.pointArrayMap || ['y'],
12798
			valueCount = pointArrayMap.length,
12799
			firstItemType,
12800
			i = 0,
12801
			j = 0;
12802
 
12803
		if (typeof options === 'number' || options === null) {
12804
			ret[pointArrayMap[0]] = options;
12805
 
12806
		} else if (isArray(options)) {
12807
			// with leading x value
12808
			if (!keys && options.length > valueCount) {
12809
				firstItemType = typeof options[0];
12810
				if (firstItemType === 'string') {
12811
					ret.name = options[0];
12812
				} else if (firstItemType === 'number') {
12813
					ret.x = options[0];
12814
				}
12815
				i++;
12816
			}
12817
			while (j < valueCount) {
12818
				ret[pointArrayMap[j++]] = options[i++];
12819
			}
12820
		} else if (typeof options === 'object') {
12821
			ret = options;
12822
 
12823
			// This is the fastest way to detect if there are individual point dataLabels that need
12824
			// to be considered in drawDataLabels. These can only occur in object configs.
12825
			if (options.dataLabels) {
12826
				series._hasPointLabels = true;
12827
			}
12828
 
12829
			// Same approach as above for markers
12830
			if (options.marker) {
12831
				series._hasPointMarkers = true;
12832
			}
12833
		}
12834
		return ret;
12835
	},
12836
 
12837
	/**
12838
	 * Destroy a point to clear memory. Its reference still stays in series.data.
12839
	 */
12840
	destroy: function () {
12841
		var point = this,
12842
			series = point.series,
12843
			chart = series.chart,
12844
			hoverPoints = chart.hoverPoints,
12845
			prop;
12846
 
12847
		chart.pointCount--;
12848
 
12849
		if (hoverPoints) {
12850
			point.setState();
12851
			erase(hoverPoints, point);
12852
			if (!hoverPoints.length) {
12853
				chart.hoverPoints = null;
12854
			}
12855
 
12856
		}
12857
		if (point === chart.hoverPoint) {
12858
			point.onMouseOut();
12859
		}
12860
 
12861
		// remove all events
12862
		if (point.graphic || point.dataLabel) { // removeEvent and destroyElements are performance expensive
12863
			removeEvent(point);
12864
			point.destroyElements();
12865
		}
12866
 
12867
		if (point.legendItem) { // pies have legend items
12868
			chart.legend.destroyItem(point);
12869
		}
12870
 
12871
		for (prop in point) {
12872
			point[prop] = null;
12873
		}
12874
 
12875
 
12876
	},
12877
 
12878
	/**
12879
	 * Destroy SVG elements associated with the point
12880
	 */
12881
	destroyElements: function () {
12882
		var point = this,
12883
			props = ['graphic', 'dataLabel', 'dataLabelUpper', 'group', 'connector', 'shadowGroup'],
12884
			prop,
12885
			i = 6;
12886
		while (i--) {
12887
			prop = props[i];
12888
			if (point[prop]) {
12889
				point[prop] = point[prop].destroy();
12890
			}
12891
		}
12892
	},
12893
 
12894
	/**
12895
	 * Return the configuration hash needed for the data label and tooltip formatters
12896
	 */
12897
	getLabelConfig: function () {
12898
		return {
12899
			x: this.category,
12900
			y: this.y,
12901
			color: this.color,
12902
			key: this.name || this.category,
12903
			series: this.series,
12904
			point: this,
12905
			percentage: this.percentage,
12906
			total: this.total || this.stackTotal
12907
		};
12908
	},
12909
 
12910
	/**
12911
	 * Extendable method for formatting each point's tooltip line
12912
	 *
12913
	 * @return {String} A string to be concatenated in to the common tooltip text
12914
	 */
12915
	tooltipFormatter: function (pointFormat) {
12916
 
12917
		// Insert options for valueDecimals, valuePrefix, and valueSuffix
12918
		var series = this.series,
12919
			seriesTooltipOptions = series.tooltipOptions,
12920
			valueDecimals = pick(seriesTooltipOptions.valueDecimals, ''),
12921
			valuePrefix = seriesTooltipOptions.valuePrefix || '',
12922
			valueSuffix = seriesTooltipOptions.valueSuffix || '';
12923
 
12924
		// Loop over the point array map and replace unformatted values with sprintf formatting markup
12925
		each(series.pointArrayMap || ['y'], function (key) {
12926
			key = '{point.' + key; // without the closing bracket
12927
			if (valuePrefix || valueSuffix) {
12928
				pointFormat = pointFormat.replace(key + '}', valuePrefix + key + '}' + valueSuffix);
12929
			}
12930
			pointFormat = pointFormat.replace(key + '}', key + ':,.' + valueDecimals + 'f}');
12931
		});
12932
 
12933
		return format(pointFormat, {
12934
			point: this,
12935
			series: this.series
12936
		});
12937
	},
12938
 
12939
	/**
12940
	 * Fire an event on the Point object. Must not be renamed to fireEvent, as this
12941
	 * causes a name clash in MooTools
12942
	 * @param {String} eventType
12943
	 * @param {Object} eventArgs Additional event arguments
12944
	 * @param {Function} defaultFunction Default event handler
12945
	 */
12946
	firePointEvent: function (eventType, eventArgs, defaultFunction) {
12947
		var point = this,
12948
			series = this.series,
12949
			seriesOptions = series.options;
12950
 
12951
		// load event handlers on demand to save time on mouseover/out
12952
		if (seriesOptions.point.events[eventType] || (point.options && point.options.events && point.options.events[eventType])) {
12953
			this.importEvents();
12954
		}
12955
 
12956
		// add default handler if in selection mode
12957
		if (eventType === 'click' && seriesOptions.allowPointSelect) {
12958
			defaultFunction = function (event) {
12959
				// Control key is for Windows, meta (= Cmd key) for Mac, Shift for Opera
12960
				if (point.select) { // Could be destroyed by prior event handlers (#2911)
12961
					point.select(null, event.ctrlKey || event.metaKey || event.shiftKey);
12962
				}
12963
			};
12964
		}
12965
 
12966
		fireEvent(this, eventType, eventArgs, defaultFunction);
12967
	}
12968
};/**
12969
 * @classDescription The base function which all other series types inherit from. The data in the series is stored
12970
 * in various arrays.
12971
 *
12972
 * - First, series.options.data contains all the original config options for
12973
 * each point whether added by options or methods like series.addPoint.
12974
 * - Next, series.data contains those values converted to points, but in case the series data length
12975
 * exceeds the cropThreshold, or if the data is grouped, series.data doesn't contain all the points. It
12976
 * only contains the points that have been created on demand.
12977
 * - Then there's series.points that contains all currently visible point objects. In case of cropping,
12978
 * the cropped-away points are not part of this array. The series.points array starts at series.cropStart
12979
 * compared to series.data and series.options.data. If however the series data is grouped, these can't
12980
 * be correlated one to one.
12981
 * - series.xData and series.processedXData contain clean x values, equivalent to series.data and series.points.
12982
 * - series.yData and series.processedYData contain clean x values, equivalent to series.data and series.points.
12983
 *
12984
 * @param {Object} chart
12985
 * @param {Object} options
12986
 */
12987
var Series = Highcharts.Series = function () {};
12988
 
12989
Series.prototype = {
12990
 
12991
	isCartesian: true,
12992
	type: 'line',
12993
	pointClass: Point,
12994
	sorted: true, // requires the data to be sorted
12995
	requireSorting: true,
12996
	pointAttrToOptions: { // mapping between SVG attributes and the corresponding options
12997
		stroke: 'lineColor',
12998
		'stroke-width': 'lineWidth',
12999
		fill: 'fillColor',
13000
		r: 'radius'
13001
	},
13002
	axisTypes: ['xAxis', 'yAxis'],
13003
	colorCounter: 0,
13004
	parallelArrays: ['x', 'y'], // each point's x and y values are stored in this.xData and this.yData
13005
	init: function (chart, options) {
13006
		var series = this,
13007
			eventType,
13008
			events,
13009
			chartSeries = chart.series,
13010
			sortByIndex = function (a, b) {
13011
				return pick(a.options.index, a._i) - pick(b.options.index, b._i);
13012
			};
13013
 
13014
		series.chart = chart;
13015
		series.options = options = series.setOptions(options); // merge with plotOptions
13016
		series.linkedSeries = [];
13017
 
13018
		// bind the axes
13019
		series.bindAxes();
13020
 
13021
		// set some variables
13022
		extend(series, {
13023
			name: options.name,
13024
			state: NORMAL_STATE,
13025
			pointAttr: {},
13026
			visible: options.visible !== false, // true by default
13027
			selected: options.selected === true // false by default
13028
		});
13029
 
13030
		// special
13031
		if (useCanVG) {
13032
			options.animation = false;
13033
		}
13034
 
13035
		// register event listeners
13036
		events = options.events;
13037
		for (eventType in events) {
13038
			addEvent(series, eventType, events[eventType]);
13039
		}
13040
		if (
13041
			(events && events.click) ||
13042
			(options.point && options.point.events && options.point.events.click) ||
13043
			options.allowPointSelect
13044
		) {
13045
			chart.runTrackerClick = true;
13046
		}
13047
 
13048
		series.getColor();
13049
		series.getSymbol();
13050
 
13051
		// Set the data
13052
		each(series.parallelArrays, function (key) {
13053
			series[key + 'Data'] = [];
13054
		});
13055
		series.setData(options.data, false);
13056
 
13057
		// Mark cartesian
13058
		if (series.isCartesian) {
13059
			chart.hasCartesianSeries = true;
13060
		}
13061
 
13062
		// Register it in the chart
13063
		chartSeries.push(series);
13064
		series._i = chartSeries.length - 1;
13065
 
13066
		// Sort series according to index option (#248, #1123, #2456)
13067
		stableSort(chartSeries, sortByIndex);
13068
		if (this.yAxis) {
13069
			stableSort(this.yAxis.series, sortByIndex);
13070
		}
13071
 
13072
		each(chartSeries, function (series, i) {
13073
			series.index = i;
13074
			series.name = series.name || 'Series ' + (i + 1);
13075
		});
13076
 
13077
	},
13078
 
13079
	/**
13080
	 * Set the xAxis and yAxis properties of cartesian series, and register the series
13081
	 * in the axis.series array
13082
	 */
13083
	bindAxes: function () {
13084
		var series = this,
13085
			seriesOptions = series.options,
13086
			chart = series.chart,
13087
			axisOptions;
13088
 
13089
		each(series.axisTypes || [], function (AXIS) { // repeat for xAxis and yAxis
13090
 
13091
			each(chart[AXIS], function (axis) { // loop through the chart's axis objects
13092
				axisOptions = axis.options;
13093
 
13094
				// apply if the series xAxis or yAxis option mathches the number of the
13095
				// axis, or if undefined, use the first axis
13096
				if ((seriesOptions[AXIS] === axisOptions.index) ||
13097
						(seriesOptions[AXIS] !== UNDEFINED && seriesOptions[AXIS] === axisOptions.id) ||
13098
						(seriesOptions[AXIS] === UNDEFINED && axisOptions.index === 0)) {
13099
 
13100
					// register this series in the axis.series lookup
13101
					axis.series.push(series);
13102
 
13103
					// set this series.xAxis or series.yAxis reference
13104
					series[AXIS] = axis;
13105
 
13106
					// mark dirty for redraw
13107
					axis.isDirty = true;
13108
				}
13109
			});
13110
 
13111
			// The series needs an X and an Y axis
13112
			if (!series[AXIS] && series.optionalAxis !== AXIS) {
13113
				error(18, true);
13114
			}
13115
 
13116
		});
13117
	},
13118
 
13119
	/**
13120
	 * For simple series types like line and column, the data values are held in arrays like
13121
	 * xData and yData for quick lookup to find extremes and more. For multidimensional series
13122
	 * like bubble and map, this can be extended with arrays like zData and valueData by
13123
	 * adding to the series.parallelArrays array.
13124
	 */
13125
	updateParallelArrays: function (point, i) {
13126
		var series = point.series,
13127
			args = arguments,
13128
			fn = typeof i === 'number' ?
13129
				 // Insert the value in the given position
13130
				function (key) {
13131
					var val = key === 'y' && series.toYData ? series.toYData(point) : point[key];
13132
					series[key + 'Data'][i] = val;
13133
				} :
13134
				// Apply the method specified in i with the following arguments as arguments
13135
				function (key) {
13136
					Array.prototype[i].apply(series[key + 'Data'], Array.prototype.slice.call(args, 2));
13137
				};
13138
 
13139
		each(series.parallelArrays, fn);
13140
	},
13141
 
13142
	/**
13143
	 * Return an auto incremented x value based on the pointStart and pointInterval options.
13144
	 * This is only used if an x value is not given for the point that calls autoIncrement.
13145
	 */
13146
	autoIncrement: function () {
13147
 
13148
		var options = this.options,
13149
			xIncrement = this.xIncrement,
13150
			date,
13151
			pointInterval,
13152
			pointIntervalUnit = options.pointIntervalUnit;
13153
 
13154
		xIncrement = pick(xIncrement, options.pointStart, 0);
13155
 
13156
		this.pointInterval = pointInterval = pick(this.pointInterval, options.pointInterval, 1);
13157
 
13158
		// Added code for pointInterval strings
13159
		if (pointIntervalUnit === 'month' || pointIntervalUnit === 'year') {
13160
			date = new Date(xIncrement);
13161
			date = (pointIntervalUnit === 'month') ?
13162
				+date[setMonth](date[getMonth]() + pointInterval) :
13163
				+date[setFullYear](date[getFullYear]() + pointInterval);
13164
			pointInterval = date - xIncrement;
13165
		}
13166
 
13167
		this.xIncrement = xIncrement + pointInterval;
13168
		return xIncrement;
13169
	},
13170
 
13171
	/**
13172
	 * Divide the series data into segments divided by null values.
13173
	 */
13174
	getSegments: function () {
13175
		var series = this,
13176
			lastNull = -1,
13177
			segments = [],
13178
			i,
13179
			points = series.points,
13180
			pointsLength = points.length;
13181
 
13182
		if (pointsLength) { // no action required for []
13183
 
13184
			// if connect nulls, just remove null points
13185
			if (series.options.connectNulls) {
13186
				i = pointsLength;
13187
				while (i--) {
13188
					if (points[i].y === null) {
13189
						points.splice(i, 1);
13190
					}
13191
				}
13192
				if (points.length) {
13193
					segments = [points];
13194
				}
13195
 
13196
			// else, split on null points
13197
			} else {
13198
				each(points, function (point, i) {
13199
					if (point.y === null) {
13200
						if (i > lastNull + 1) {
13201
							segments.push(points.slice(lastNull + 1, i));
13202
						}
13203
						lastNull = i;
13204
					} else if (i === pointsLength - 1) { // last value
13205
						segments.push(points.slice(lastNull + 1, i + 1));
13206
					}
13207
				});
13208
			}
13209
		}
13210
 
13211
		// register it
13212
		series.segments = segments;
13213
	},
13214
 
13215
	/**
13216
	 * Set the series options by merging from the options tree
13217
	 * @param {Object} itemOptions
13218
	 */
13219
	setOptions: function (itemOptions) {
13220
		var chart = this.chart,
13221
			chartOptions = chart.options,
13222
			plotOptions = chartOptions.plotOptions,
13223
			userOptions = chart.userOptions || {},
13224
			userPlotOptions = userOptions.plotOptions || {},
13225
			typeOptions = plotOptions[this.type],
13226
			options,
13227
			zones;
13228
 
13229
		this.userOptions = itemOptions;
13230
 
13231
		// General series options take precedence over type options because otherwise, default
13232
		// type options like column.animation would be overwritten by the general option.
13233
		// But issues have been raised here (#3881), and the solution may be to distinguish
13234
		// between default option and userOptions like in the tooltip below.
13235
		options = merge(
13236
			typeOptions,
13237
			plotOptions.series,
13238
			itemOptions
13239
		);
13240
 
13241
		// The tooltip options are merged between global and series specific options
13242
		this.tooltipOptions = merge(
13243
			defaultOptions.tooltip,
13244
			defaultOptions.plotOptions[this.type].tooltip,
13245
			userOptions.tooltip,
13246
			userPlotOptions.series && userPlotOptions.series.tooltip,
13247
			userPlotOptions[this.type] && userPlotOptions[this.type].tooltip,
13248
			itemOptions.tooltip
13249
		);
13250
 
13251
		// Delete marker object if not allowed (#1125)
13252
		if (typeOptions.marker === null) {
13253
			delete options.marker;
13254
		}
13255
 
13256
		// Handle color zones
13257
		this.zoneAxis = options.zoneAxis;
13258
		zones = this.zones = (options.zones || []).slice();
13259
		if ((options.negativeColor || options.negativeFillColor) && !options.zones) {
13260
			zones.push({
13261
				value: options[this.zoneAxis + 'Threshold'] || options.threshold || 0,
13262
				color: options.negativeColor,
13263
				fillColor: options.negativeFillColor
13264
			});
13265
		}
13266
		if (zones.length) { // Push one extra zone for the rest
13267
			if (defined(zones[zones.length - 1].value)) {
13268
				zones.push({
13269
					color: this.color,
13270
					fillColor: this.fillColor
13271
				});
13272
			}
13273
		}
13274
		return options;
13275
	},
13276
 
13277
	getCyclic: function (prop, value, defaults) {
13278
		var i,
13279
			userOptions = this.userOptions,
13280
			indexName = '_' + prop + 'Index',
13281
			counterName = prop + 'Counter';
13282
 
13283
		if (!value) {
13284
			if (defined(userOptions[indexName])) { // after Series.update()
13285
				i = userOptions[indexName];
13286
			} else {
13287
				userOptions[indexName] = i = this.chart[counterName] % defaults.length;
13288
				this.chart[counterName] += 1;
13289
			}
13290
			value = defaults[i];
13291
		}
13292
		this[prop] = value;
13293
	},
13294
 
13295
	/**
13296
	 * Get the series' color
13297
	 */
13298
	getColor: function () {
13299
		if (this.options.colorByPoint) {
13300
			this.options.color = null; // #4359, selected slice got series.color even when colorByPoint was set.
13301
		} else {
13302
			this.getCyclic('color', this.options.color || defaultPlotOptions[this.type].color, this.chart.options.colors);
13303
		}
13304
	},
13305
	/**
13306
	 * Get the series' symbol
13307
	 */
13308
	getSymbol: function () {
13309
		var seriesMarkerOption = this.options.marker;
13310
 
13311
		this.getCyclic('symbol', seriesMarkerOption.symbol, this.chart.options.symbols);
13312
 
13313
		// don't substract radius in image symbols (#604)
13314
		if (/^url/.test(this.symbol)) {
13315
			seriesMarkerOption.radius = 0;
13316
		}
13317
	},
13318
 
13319
	drawLegendSymbol: LegendSymbolMixin.drawLineMarker,
13320
 
13321
	/**
13322
	 * Replace the series data with a new set of data
13323
	 * @param {Object} data
13324
	 * @param {Object} redraw
13325
	 */
13326
	setData: function (data, redraw, animation, updatePoints) {
13327
		var series = this,
13328
			oldData = series.points,
13329
			oldDataLength = (oldData && oldData.length) || 0,
13330
			dataLength,
13331
			options = series.options,
13332
			chart = series.chart,
13333
			firstPoint = null,
13334
			xAxis = series.xAxis,
13335
			hasCategories = xAxis && !!xAxis.categories,
13336
			i,
13337
			turboThreshold = options.turboThreshold,
13338
			pt,
13339
			xData = this.xData,
13340
			yData = this.yData,
13341
			pointArrayMap = series.pointArrayMap,
13342
			valueCount = pointArrayMap && pointArrayMap.length;
13343
 
13344
		data = data || [];
13345
		dataLength = data.length;
13346
		redraw = pick(redraw, true);
13347
 
13348
		// If the point count is the same as is was, just run Point.update which is
13349
		// cheaper, allows animation, and keeps references to points.
13350
		if (updatePoints !== false && dataLength && oldDataLength === dataLength && !series.cropped && !series.hasGroupedData && series.visible) {
13351
			each(data, function (point, i) {
13352
				if (oldData[i].update) { // Linked, previously hidden series (#3709)
13353
					oldData[i].update(point, false, null, false);
13354
				}
13355
			});
13356
 
13357
		} else {
13358
 
13359
			// Reset properties
13360
			series.xIncrement = null;
13361
			series.pointRange = hasCategories ? 1 : options.pointRange;
13362
 
13363
			series.colorCounter = 0; // for series with colorByPoint (#1547)
13364
 
13365
			// Update parallel arrays
13366
			each(this.parallelArrays, function (key) {
13367
				series[key + 'Data'].length = 0;
13368
			});
13369
 
13370
			// In turbo mode, only one- or twodimensional arrays of numbers are allowed. The
13371
			// first value is tested, and we assume that all the rest are defined the same
13372
			// way. Although the 'for' loops are similar, they are repeated inside each
13373
			// if-else conditional for max performance.
13374
			if (turboThreshold && dataLength > turboThreshold) {
13375
 
13376
				// find the first non-null point
13377
				i = 0;
13378
				while (firstPoint === null && i < dataLength) {
13379
					firstPoint = data[i];
13380
					i++;
13381
				}
13382
 
13383
 
13384
				if (isNumber(firstPoint)) { // assume all points are numbers
13385
					var x = pick(options.pointStart, 0),
13386
						pointInterval = pick(options.pointInterval, 1);
13387
 
13388
					for (i = 0; i < dataLength; i++) {
13389
						xData[i] = x;
13390
						yData[i] = data[i];
13391
						x += pointInterval;
13392
					}
13393
					series.xIncrement = x;
13394
				} else if (isArray(firstPoint)) { // assume all points are arrays
13395
					if (valueCount) { // [x, low, high] or [x, o, h, l, c]
13396
						for (i = 0; i < dataLength; i++) {
13397
							pt = data[i];
13398
							xData[i] = pt[0];
13399
							yData[i] = pt.slice(1, valueCount + 1);
13400
						}
13401
					} else { // [x, y]
13402
						for (i = 0; i < dataLength; i++) {
13403
							pt = data[i];
13404
							xData[i] = pt[0];
13405
							yData[i] = pt[1];
13406
						}
13407
					}
13408
				} else {
13409
					error(12); // Highcharts expects configs to be numbers or arrays in turbo mode
13410
				}
13411
			} else {
13412
				for (i = 0; i < dataLength; i++) {
13413
					if (data[i] !== UNDEFINED) { // stray commas in oldIE
13414
						pt = { series: series };
13415
						series.pointClass.prototype.applyOptions.apply(pt, [data[i]]);
13416
						series.updateParallelArrays(pt, i);
13417
						if (hasCategories && defined(pt.name)) { // #4401
13418
							xAxis.names[pt.x] = pt.name; // #2046
13419
						}
13420
					}
13421
				}
13422
			}
13423
 
13424
			// Forgetting to cast strings to numbers is a common caveat when handling CSV or JSON
13425
			if (isString(yData[0])) {
13426
				error(14, true);
13427
			}
13428
 
13429
			series.data = [];
13430
			series.options.data = data;
13431
			//series.zData = zData;
13432
 
13433
			// destroy old points
13434
			i = oldDataLength;
13435
			while (i--) {
13436
				if (oldData[i] && oldData[i].destroy) {
13437
					oldData[i].destroy();
13438
				}
13439
			}
13440
 
13441
			// reset minRange (#878)
13442
			if (xAxis) {
13443
				xAxis.minRange = xAxis.userMinRange;
13444
			}
13445
 
13446
			// redraw
13447
			series.isDirty = series.isDirtyData = chart.isDirtyBox = true;
13448
			animation = false;
13449
		}
13450
 
13451
		if (redraw) {
13452
			chart.redraw(animation);
13453
		}
13454
	},
13455
 
13456
	/**
13457
	 * Process the data by cropping away unused data points if the series is longer
13458
	 * than the crop threshold. This saves computing time for lage series.
13459
	 */
13460
	processData: function (force) {
13461
		var series = this,
13462
			processedXData = series.xData, // copied during slice operation below
13463
			processedYData = series.yData,
13464
			dataLength = processedXData.length,
13465
			croppedData,
13466
			cropStart = 0,
13467
			cropped,
13468
			distance,
13469
			closestPointRange,
13470
			xAxis = series.xAxis,
13471
			i, // loop variable
13472
			options = series.options,
13473
			cropThreshold = options.cropThreshold,
13474
			isCartesian = series.isCartesian,
13475
			xExtremes,
13476
			min,
13477
			max;
13478
 
13479
		// If the series data or axes haven't changed, don't go through this. Return false to pass
13480
		// the message on to override methods like in data grouping.
13481
		if (isCartesian && !series.isDirty && !xAxis.isDirty && !series.yAxis.isDirty && !force) {
13482
			return false;
13483
		}
13484
 
13485
		if (xAxis) {
13486
			xExtremes = xAxis.getExtremes(); // corrected for log axis (#3053)
13487
			min = xExtremes.min;
13488
			max = xExtremes.max;
13489
		}
13490
 
13491
		// optionally filter out points outside the plot area
13492
		if (isCartesian && series.sorted && (!cropThreshold || dataLength > cropThreshold || series.forceCrop)) {
13493
 
13494
			// it's outside current extremes
13495
			if (processedXData[dataLength - 1] < min || processedXData[0] > max) {
13496
				processedXData = [];
13497
				processedYData = [];
13498
 
13499
			// only crop if it's actually spilling out
13500
			} else if (processedXData[0] < min || processedXData[dataLength - 1] > max) {
13501
				croppedData = this.cropData(series.xData, series.yData, min, max);
13502
				processedXData = croppedData.xData;
13503
				processedYData = croppedData.yData;
13504
				cropStart = croppedData.start;
13505
				cropped = true;
13506
			}
13507
		}
13508
 
13509
 
13510
		// Find the closest distance between processed points
13511
		for (i = processedXData.length - 1; i >= 0; i--) {
13512
			distance = processedXData[i] - processedXData[i - 1];
13513
 
13514
			if (distance > 0 && (closestPointRange === UNDEFINED || distance < closestPointRange)) {
13515
				closestPointRange = distance;
13516
 
13517
			// Unsorted data is not supported by the line tooltip, as well as data grouping and
13518
			// navigation in Stock charts (#725) and width calculation of columns (#1900)
13519
			} else if (distance < 0 && series.requireSorting) {
13520
				error(15);
13521
			}
13522
		}
13523
 
13524
		// Record the properties
13525
		series.cropped = cropped; // undefined or true
13526
		series.cropStart = cropStart;
13527
		series.processedXData = processedXData;
13528
		series.processedYData = processedYData;
13529
 
13530
		if (options.pointRange === null) { // null means auto, as for columns, candlesticks and OHLC
13531
			series.pointRange = closestPointRange || 1;
13532
		}
13533
		series.closestPointRange = closestPointRange;
13534
 
13535
	},
13536
 
13537
	/**
13538
	 * Iterate over xData and crop values between min and max. Returns object containing crop start/end
13539
	 * cropped xData with corresponding part of yData, dataMin and dataMax within the cropped range
13540
	 */
13541
	cropData: function (xData, yData, min, max) {
13542
		var dataLength = xData.length,
13543
			cropStart = 0,
13544
			cropEnd = dataLength,
13545
			cropShoulder = pick(this.cropShoulder, 1), // line-type series need one point outside
13546
			i;
13547
 
13548
		// iterate up to find slice start
13549
		for (i = 0; i < dataLength; i++) {
13550
			if (xData[i] >= min) {
13551
				cropStart = mathMax(0, i - cropShoulder);
13552
				break;
13553
			}
13554
		}
13555
 
13556
		// proceed to find slice end
13557
		for (; i < dataLength; i++) {
13558
			if (xData[i] > max) {
13559
				cropEnd = i + cropShoulder;
13560
				break;
13561
			}
13562
		}
13563
 
13564
		return {
13565
			xData: xData.slice(cropStart, cropEnd),
13566
			yData: yData.slice(cropStart, cropEnd),
13567
			start: cropStart,
13568
			end: cropEnd
13569
		};
13570
	},
13571
 
13572
 
13573
	/**
13574
	 * Generate the data point after the data has been processed by cropping away
13575
	 * unused points and optionally grouped in Highcharts Stock.
13576
	 */
13577
	generatePoints: function () {
13578
		var series = this,
13579
			options = series.options,
13580
			dataOptions = options.data,
13581
			data = series.data,
13582
			dataLength,
13583
			processedXData = series.processedXData,
13584
			processedYData = series.processedYData,
13585
			pointClass = series.pointClass,
13586
			processedDataLength = processedXData.length,
13587
			cropStart = series.cropStart || 0,
13588
			cursor,
13589
			hasGroupedData = series.hasGroupedData,
13590
			point,
13591
			points = [],
13592
			i;
13593
 
13594
		if (!data && !hasGroupedData) {
13595
			var arr = [];
13596
			arr.length = dataOptions.length;
13597
			data = series.data = arr;
13598
		}
13599
 
13600
		for (i = 0; i < processedDataLength; i++) {
13601
			cursor = cropStart + i;
13602
			if (!hasGroupedData) {
13603
				if (data[cursor]) {
13604
					point = data[cursor];
13605
				} else if (dataOptions[cursor] !== UNDEFINED) { // #970
13606
					data[cursor] = point = (new pointClass()).init(series, dataOptions[cursor], processedXData[i]);
13607
				}
13608
				points[i] = point;
13609
			} else {
13610
				// splat the y data in case of ohlc data array
13611
				points[i] = (new pointClass()).init(series, [processedXData[i]].concat(splat(processedYData[i])));
13612
			}
13613
			points[i].index = cursor; // For faster access in Point.update
13614
		}
13615
 
13616
		// Hide cropped-away points - this only runs when the number of points is above cropThreshold, or when
13617
		// swithching view from non-grouped data to grouped data (#637)
13618
		if (data && (processedDataLength !== (dataLength = data.length) || hasGroupedData)) {
13619
			for (i = 0; i < dataLength; i++) {
13620
				if (i === cropStart && !hasGroupedData) { // when has grouped data, clear all points
13621
					i += processedDataLength;
13622
				}
13623
				if (data[i]) {
13624
					data[i].destroyElements();
13625
					data[i].plotX = UNDEFINED; // #1003
13626
				}
13627
			}
13628
		}
13629
 
13630
		series.data = data;
13631
		series.points = points;
13632
	},
13633
 
13634
	/**
13635
	 * Calculate Y extremes for visible data
13636
	 */
13637
	getExtremes: function (yData) {
13638
		var xAxis = this.xAxis,
13639
			yAxis = this.yAxis,
13640
			xData = this.processedXData,
13641
			yDataLength,
13642
			activeYData = [],
13643
			activeCounter = 0,
13644
			xExtremes = xAxis.getExtremes(), // #2117, need to compensate for log X axis
13645
			xMin = xExtremes.min,
13646
			xMax = xExtremes.max,
13647
			validValue,
13648
			withinRange,
13649
			x,
13650
			y,
13651
			i,
13652
			j;
13653
 
13654
		yData = yData || this.stackedYData || this.processedYData;
13655
		yDataLength = yData.length;
13656
 
13657
		for (i = 0; i < yDataLength; i++) {
13658
 
13659
			x = xData[i];
13660
			y = yData[i];
13661
 
13662
			// For points within the visible range, including the first point outside the
13663
			// visible range, consider y extremes
13664
			validValue = y !== null && y !== UNDEFINED && (!yAxis.isLog || (y.length || y > 0));
13665
			withinRange = this.getExtremesFromAll || this.options.getExtremesFromAll || this.cropped ||
13666
				((xData[i + 1] || x) >= xMin &&	(xData[i - 1] || x) <= xMax);
13667
 
13668
			if (validValue && withinRange) {
13669
 
13670
				j = y.length;
13671
				if (j) { // array, like ohlc or range data
13672
					while (j--) {
13673
						if (y[j] !== null) {
13674
							activeYData[activeCounter++] = y[j];
13675
						}
13676
					}
13677
				} else {
13678
					activeYData[activeCounter++] = y;
13679
				}
13680
			}
13681
		}
13682
		this.dataMin = arrayMin(activeYData);
13683
		this.dataMax = arrayMax(activeYData);
13684
	},
13685
 
13686
	/**
13687
	 * Translate data points from raw data values to chart specific positioning data
13688
	 * needed later in drawPoints, drawGraph and drawTracker.
13689
	 */
13690
	translate: function () {
13691
		if (!this.processedXData) { // hidden series
13692
			this.processData();
13693
		}
13694
		this.generatePoints();
13695
		var series = this,
13696
			options = series.options,
13697
			stacking = options.stacking,
13698
			xAxis = series.xAxis,
13699
			categories = xAxis.categories,
13700
			yAxis = series.yAxis,
13701
			points = series.points,
13702
			dataLength = points.length,
13703
			hasModifyValue = !!series.modifyValue,
13704
			i,
13705
			pointPlacement = options.pointPlacement,
13706
			dynamicallyPlaced = pointPlacement === 'between' || isNumber(pointPlacement),
13707
			threshold = options.threshold,
13708
			stackThreshold = options.startFromThreshold ? threshold : 0,
13709
			plotX,
13710
			plotY,
13711
			lastPlotX,
13712
			closestPointRangePx = Number.MAX_VALUE;
13713
 
13714
		// Translate each point
13715
		for (i = 0; i < dataLength; i++) {
13716
			var point = points[i],
13717
				xValue = point.x,
13718
				yValue = point.y,
13719
				yBottom = point.low,
13720
				stack = stacking && yAxis.stacks[(series.negStacks && yValue < (stackThreshold ? 0 : threshold) ? '-' : '') + series.stackKey],
13721
				pointStack,
13722
				stackValues;
13723
 
13724
			// Discard disallowed y values for log axes (#3434)
13725
			if (yAxis.isLog && yValue !== null && yValue <= 0) {
13726
				point.y = yValue = null;
13727
				error(10);
13728
			}
13729
 
13730
			// Get the plotX translation
13731
			point.plotX = plotX = mathMin(mathMax(-1e5, xAxis.translate(xValue, 0, 0, 0, 1, pointPlacement, this.type === 'flags')), 1e5); // #3923
13732
 
13733
 
13734
			// Calculate the bottom y value for stacked series
13735
			if (stacking && series.visible && stack && stack[xValue]) {
13736
 
13737
				pointStack = stack[xValue];
13738
				stackValues = pointStack.points[series.index + ',' + i];
13739
				yBottom = stackValues[0];
13740
				yValue = stackValues[1];
13741
 
13742
				if (yBottom === stackThreshold) {
13743
					yBottom = pick(threshold, yAxis.min);
13744
				}
13745
				if (yAxis.isLog && yBottom <= 0) { // #1200, #1232
13746
					yBottom = null;
13747
				}
13748
 
13749
				point.total = point.stackTotal = pointStack.total;
13750
				point.percentage = pointStack.total && (point.y / pointStack.total * 100);
13751
				point.stackY = yValue;
13752
 
13753
				// Place the stack label
13754
				pointStack.setOffset(series.pointXOffset || 0, series.barW || 0);
13755
 
13756
			}
13757
 
13758
			// Set translated yBottom or remove it
13759
			point.yBottom = defined(yBottom) ?
13760
				yAxis.translate(yBottom, 0, 1, 0, 1) :
13761
				null;
13762
 
13763
			// general hook, used for Highstock compare mode
13764
			if (hasModifyValue) {
13765
				yValue = series.modifyValue(yValue, point);
13766
			}
13767
 
13768
			// Set the the plotY value, reset it for redraws
13769
			point.plotY = plotY = (typeof yValue === 'number' && yValue !== Infinity) ?
13770
				mathMin(mathMax(-1e5, yAxis.translate(yValue, 0, 1, 0, 1)), 1e5) : // #3201
13771
				UNDEFINED;
13772
			point.isInside = plotY !== UNDEFINED && plotY >= 0 && plotY <= yAxis.len && // #3519
13773
				plotX >= 0 && plotX <= xAxis.len;
13774
 
13775
 
13776
			// Set client related positions for mouse tracking
13777
			point.clientX = dynamicallyPlaced ? xAxis.translate(xValue, 0, 0, 0, 1) : plotX; // #1514
13778
 
13779
			point.negative = point.y < (threshold || 0);
13780
 
13781
			// some API data
13782
			point.category = categories && categories[point.x] !== UNDEFINED ?
13783
				categories[point.x] : point.x;
13784
 
13785
			// Determine auto enabling of markers (#3635)
13786
			if (i) {
13787
				closestPointRangePx = mathMin(closestPointRangePx, mathAbs(plotX - lastPlotX));
13788
			}
13789
			lastPlotX = plotX;
13790
 
13791
		}
13792
 
13793
		series.closestPointRangePx = closestPointRangePx;
13794
 
13795
		// now that we have the cropped data, build the segments
13796
		series.getSegments();
13797
	},
13798
 
13799
	/**
13800
	 * Set the clipping for the series. For animated series it is called twice, first to initiate
13801
	 * animating the clip then the second time without the animation to set the final clip.
13802
	 */
13803
	setClip: function (animation) {
13804
		var chart = this.chart,
13805
			renderer = chart.renderer,
13806
			inverted = chart.inverted,
13807
			seriesClipBox = this.clipBox,
13808
			clipBox = seriesClipBox || chart.clipBox,
13809
			sharedClipKey = this.sharedClipKey || ['_sharedClip', animation && animation.duration, animation && animation.easing, clipBox.height].join(','),
13810
			clipRect = chart[sharedClipKey],
13811
			markerClipRect = chart[sharedClipKey + 'm'];
13812
 
13813
		// If a clipping rectangle with the same properties is currently present in the chart, use that.
13814
		if (!clipRect) {
13815
 
13816
			// When animation is set, prepare the initial positions
13817
			if (animation) {
13818
				clipBox.width = 0;
13819
 
13820
				chart[sharedClipKey + 'm'] = markerClipRect = renderer.clipRect(
13821
					-99, // include the width of the first marker
13822
					inverted ? -chart.plotLeft : -chart.plotTop,
13823
					99,
13824
					inverted ? chart.chartWidth : chart.chartHeight
13825
				);
13826
			}
13827
			chart[sharedClipKey] = clipRect = renderer.clipRect(clipBox);
13828
 
13829
		}
13830
		if (animation) {
13831
			clipRect.count += 1;
13832
		}
13833
 
13834
		if (this.options.clip !== false) {
13835
			this.group.clip(animation || seriesClipBox ? clipRect : chart.clipRect);
13836
			this.markerGroup.clip(markerClipRect);
13837
			this.sharedClipKey = sharedClipKey;
13838
		}
13839
 
13840
		// Remove the shared clipping rectangle when all series are shown
13841
		if (!animation) {
13842
			clipRect.count -= 1;
13843
			if (clipRect.count <= 0 && sharedClipKey && chart[sharedClipKey]) {
13844
				if (!seriesClipBox) {
13845
					chart[sharedClipKey] = chart[sharedClipKey].destroy();
13846
				}
13847
				if (chart[sharedClipKey + 'm']) {
13848
					chart[sharedClipKey + 'm'] = chart[sharedClipKey + 'm'].destroy();
13849
				}
13850
			}
13851
		}
13852
	},
13853
 
13854
	/**
13855
	 * Animate in the series
13856
	 */
13857
	animate: function (init) {
13858
		var series = this,
13859
			chart = series.chart,
13860
			clipRect,
13861
			animation = series.options.animation,
13862
			sharedClipKey;
13863
 
13864
		// Animation option is set to true
13865
		if (animation && !isObject(animation)) {
13866
			animation = defaultPlotOptions[series.type].animation;
13867
		}
13868
 
13869
		// Initialize the animation. Set up the clipping rectangle.
13870
		if (init) {
13871
 
13872
			series.setClip(animation);
13873
 
13874
		// Run the animation
13875
		} else {
13876
			sharedClipKey = this.sharedClipKey;
13877
			clipRect = chart[sharedClipKey];
13878
			if (clipRect) {
13879
				clipRect.animate({
13880
					width: chart.plotSizeX
13881
				}, animation);
13882
			}
13883
			if (chart[sharedClipKey + 'm']) {
13884
				chart[sharedClipKey + 'm'].animate({
13885
					width: chart.plotSizeX + 99
13886
				}, animation);
13887
			}
13888
 
13889
			// Delete this function to allow it only once
13890
			series.animate = null;
13891
 
13892
		}
13893
	},
13894
 
13895
	/**
13896
	 * This runs after animation to land on the final plot clipping
13897
	 */
13898
	afterAnimate: function () {
13899
		this.setClip();
13900
		fireEvent(this, 'afterAnimate');
13901
	},
13902
 
13903
	/**
13904
	 * Draw the markers
13905
	 */
13906
	drawPoints: function () {
13907
		var series = this,
13908
			pointAttr,
13909
			points = series.points,
13910
			chart = series.chart,
13911
			plotX,
13912
			plotY,
13913
			i,
13914
			point,
13915
			radius,
13916
			symbol,
13917
			isImage,
13918
			graphic,
13919
			options = series.options,
13920
			seriesMarkerOptions = options.marker,
13921
			seriesPointAttr = series.pointAttr[''],
13922
			pointMarkerOptions,
13923
			hasPointMarker,
13924
			enabled,
13925
			isInside,
13926
			markerGroup = series.markerGroup,
13927
			xAxis = series.xAxis,
13928
			globallyEnabled = pick(
13929
				seriesMarkerOptions.enabled,
13930
				xAxis.isRadial,
13931
				series.closestPointRangePx > 2 * seriesMarkerOptions.radius
13932
			);
13933
 
13934
		if (seriesMarkerOptions.enabled !== false || series._hasPointMarkers) {
13935
 
13936
			i = points.length;
13937
			while (i--) {
13938
				point = points[i];
13939
				plotX = mathFloor(point.plotX); // #1843
13940
				plotY = point.plotY;
13941
				graphic = point.graphic;
13942
				pointMarkerOptions = point.marker || {};
13943
				hasPointMarker = !!point.marker;
13944
				enabled = (globallyEnabled && pointMarkerOptions.enabled === UNDEFINED) || pointMarkerOptions.enabled;
13945
				isInside = point.isInside;
13946
 
13947
				// only draw the point if y is defined
13948
				if (enabled && plotY !== UNDEFINED && !isNaN(plotY) && point.y !== null) {
13949
 
13950
					// shortcuts
13951
					pointAttr = point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE] || seriesPointAttr;
13952
					radius = pointAttr.r;
13953
					symbol = pick(pointMarkerOptions.symbol, series.symbol);
13954
					isImage = symbol.indexOf('url') === 0;
13955
 
13956
					if (graphic) { // update
13957
						graphic[isInside ? 'show' : 'hide'](true) // Since the marker group isn't clipped, each individual marker must be toggled
13958
							.animate(extend({
13959
								x: plotX - radius,
13960
								y: plotY - radius
13961
							}, graphic.symbolName ? { // don't apply to image symbols #507
13962
								width: 2 * radius,
13963
								height: 2 * radius
13964
							} : {}));
13965
					} else if (isInside && (radius > 0 || isImage)) {
13966
						point.graphic = graphic = chart.renderer.symbol(
13967
							symbol,
13968
							plotX - radius,
13969
							plotY - radius,
13970
							2 * radius,
13971
							2 * radius,
13972
							hasPointMarker ? pointMarkerOptions : seriesMarkerOptions
13973
						)
13974
						.attr(pointAttr)
13975
						.add(markerGroup);
13976
					}
13977
 
13978
				} else if (graphic) {
13979
					point.graphic = graphic.destroy(); // #1269
13980
				}
13981
			}
13982
		}
13983
 
13984
	},
13985
 
13986
	/**
13987
	 * Convert state properties from API naming conventions to SVG attributes
13988
	 *
13989
	 * @param {Object} options API options object
13990
	 * @param {Object} base1 SVG attribute object to inherit from
13991
	 * @param {Object} base2 Second level SVG attribute object to inherit from
13992
	 */
13993
	convertAttribs: function (options, base1, base2, base3) {
13994
		var conversion = this.pointAttrToOptions,
13995
			attr,
13996
			option,
13997
			obj = {};
13998
 
13999
		options = options || {};
14000
		base1 = base1 || {};
14001
		base2 = base2 || {};
14002
		base3 = base3 || {};
14003
 
14004
		for (attr in conversion) {
14005
			option = conversion[attr];
14006
			obj[attr] = pick(options[option], base1[attr], base2[attr], base3[attr]);
14007
		}
14008
		return obj;
14009
	},
14010
 
14011
	/**
14012
	 * Get the state attributes. Each series type has its own set of attributes
14013
	 * that are allowed to change on a point's state change. Series wide attributes are stored for
14014
	 * all series, and additionally point specific attributes are stored for all
14015
	 * points with individual marker options. If such options are not defined for the point,
14016
	 * a reference to the series wide attributes is stored in point.pointAttr.
14017
	 */
14018
	getAttribs: function () {
14019
		var series = this,
14020
			seriesOptions = series.options,
14021
			normalOptions = defaultPlotOptions[series.type].marker ? seriesOptions.marker : seriesOptions,
14022
			stateOptions = normalOptions.states,
14023
			stateOptionsHover = stateOptions[HOVER_STATE],
14024
			pointStateOptionsHover,
14025
			seriesColor = series.color,
14026
			seriesNegativeColor = series.options.negativeColor,
14027
			normalDefaults = {
14028
				stroke: seriesColor,
14029
				fill: seriesColor
14030
			},
14031
			points = series.points || [], // #927
14032
			i,
14033
			j,
14034
			threshold,
14035
			point,
14036
			seriesPointAttr = [],
14037
			pointAttr,
14038
			pointAttrToOptions = series.pointAttrToOptions,
14039
			hasPointSpecificOptions = series.hasPointSpecificOptions,
14040
			defaultLineColor = normalOptions.lineColor,
14041
			defaultFillColor = normalOptions.fillColor,
14042
			turboThreshold = seriesOptions.turboThreshold,
14043
			zones = series.zones,
14044
			zoneAxis = series.zoneAxis || 'y',
14045
			attr,
14046
			key;
14047
 
14048
		// series type specific modifications
14049
		if (seriesOptions.marker) { // line, spline, area, areaspline, scatter
14050
 
14051
			// if no hover radius is given, default to normal radius + 2
14052
			stateOptionsHover.radius = stateOptionsHover.radius || normalOptions.radius + stateOptionsHover.radiusPlus;
14053
			stateOptionsHover.lineWidth = stateOptionsHover.lineWidth || normalOptions.lineWidth + stateOptionsHover.lineWidthPlus;
14054
 
14055
		} else { // column, bar, pie
14056
 
14057
			// if no hover color is given, brighten the normal color
14058
			stateOptionsHover.color = stateOptionsHover.color ||
14059
				Color(stateOptionsHover.color || seriesColor)
14060
					.brighten(stateOptionsHover.brightness).get();
14061
 
14062
			// if no hover negativeColor is given, brighten the normal negativeColor
14063
			stateOptionsHover.negativeColor = stateOptionsHover.negativeColor ||
14064
				Color(stateOptionsHover.negativeColor || seriesNegativeColor)
14065
					.brighten(stateOptionsHover.brightness).get();
14066
		}
14067
 
14068
		// general point attributes for the series normal state
14069
		seriesPointAttr[NORMAL_STATE] = series.convertAttribs(normalOptions, normalDefaults);
14070
 
14071
		// HOVER_STATE and SELECT_STATE states inherit from normal state except the default radius
14072
		each([HOVER_STATE, SELECT_STATE], function (state) {
14073
			seriesPointAttr[state] =
14074
					series.convertAttribs(stateOptions[state], seriesPointAttr[NORMAL_STATE]);
14075
		});
14076
 
14077
		// set it
14078
		series.pointAttr = seriesPointAttr;
14079
 
14080
 
14081
		// Generate the point-specific attribute collections if specific point
14082
		// options are given. If not, create a referance to the series wide point
14083
		// attributes
14084
		i = points.length;
14085
		if (!turboThreshold || i < turboThreshold || hasPointSpecificOptions) {
14086
			while (i--) {
14087
				point = points[i];
14088
				normalOptions = (point.options && point.options.marker) || point.options;
14089
				if (normalOptions && normalOptions.enabled === false) {
14090
					normalOptions.radius = 0;
14091
				}
14092
 
14093
				if (zones.length) {
14094
					j = 0;
14095
					threshold = zones[j];
14096
					while (point[zoneAxis] >= threshold.value) {
14097
						threshold = zones[++j];
14098
					}
14099
 
14100
					if (threshold.color) {
14101
						point.color = point.fillColor = threshold.color;
14102
					}
14103
				}
14104
 
14105
				hasPointSpecificOptions = seriesOptions.colorByPoint || point.color; // #868
14106
 
14107
				// check if the point has specific visual options
14108
				if (point.options) {
14109
					for (key in pointAttrToOptions) {
14110
						if (defined(normalOptions[pointAttrToOptions[key]])) {
14111
							hasPointSpecificOptions = true;
14112
						}
14113
					}
14114
				}
14115
 
14116
				// a specific marker config object is defined for the individual point:
14117
				// create it's own attribute collection
14118
				if (hasPointSpecificOptions) {
14119
					normalOptions = normalOptions || {};
14120
					pointAttr = [];
14121
					stateOptions = normalOptions.states || {}; // reassign for individual point
14122
					pointStateOptionsHover = stateOptions[HOVER_STATE] = stateOptions[HOVER_STATE] || {};
14123
 
14124
					// Handle colors for column and pies
14125
					if (!seriesOptions.marker) { // column, bar, point
14126
						// If no hover color is given, brighten the normal color. #1619, #2579
14127
						pointStateOptionsHover.color = pointStateOptionsHover.color || (!point.options.color && stateOptionsHover[(point.negative && seriesNegativeColor ? 'negativeColor' : 'color')]) ||
14128
							Color(point.color)
14129
								.brighten(pointStateOptionsHover.brightness || stateOptionsHover.brightness)
14130
								.get();
14131
					}
14132
 
14133
					// normal point state inherits series wide normal state
14134
					attr = { color: point.color }; // #868
14135
					if (!defaultFillColor) { // Individual point color or negative color markers (#2219)
14136
						attr.fillColor = point.color;
14137
					}
14138
					if (!defaultLineColor) {
14139
						attr.lineColor = point.color; // Bubbles take point color, line markers use white
14140
					}
14141
					// Color is explicitly set to null or undefined (#1288, #4068)
14142
					if (normalOptions.hasOwnProperty('color') && !normalOptions.color) {
14143
						delete normalOptions.color;
14144
					}
14145
					pointAttr[NORMAL_STATE] = series.convertAttribs(extend(attr, normalOptions), seriesPointAttr[NORMAL_STATE]);
14146
 
14147
					// inherit from point normal and series hover
14148
					pointAttr[HOVER_STATE] = series.convertAttribs(
14149
						stateOptions[HOVER_STATE],
14150
						seriesPointAttr[HOVER_STATE],
14151
						pointAttr[NORMAL_STATE]
14152
					);
14153
 
14154
					// inherit from point normal and series hover
14155
					pointAttr[SELECT_STATE] = series.convertAttribs(
14156
						stateOptions[SELECT_STATE],
14157
						seriesPointAttr[SELECT_STATE],
14158
						pointAttr[NORMAL_STATE]
14159
					);
14160
 
14161
 
14162
				// no marker config object is created: copy a reference to the series-wide
14163
				// attribute collection
14164
				} else {
14165
					pointAttr = seriesPointAttr;
14166
				}
14167
 
14168
				point.pointAttr = pointAttr;
14169
			}
14170
		}
14171
	},
14172
 
14173
	/**
14174
	 * Clear DOM objects and free up memory
14175
	 */
14176
	destroy: function () {
14177
		var series = this,
14178
			chart = series.chart,
14179
			issue134 = /AppleWebKit\/533/.test(userAgent),
14180
			destroy,
14181
			i,
14182
			data = series.data || [],
14183
			point,
14184
			prop,
14185
			axis;
14186
 
14187
		// add event hook
14188
		fireEvent(series, 'destroy');
14189
 
14190
		// remove all events
14191
		removeEvent(series);
14192
 
14193
		// erase from axes
14194
		each(series.axisTypes || [], function (AXIS) {
14195
			axis = series[AXIS];
14196
			if (axis) {
14197
				erase(axis.series, series);
14198
				axis.isDirty = axis.forceRedraw = true;
14199
			}
14200
		});
14201
 
14202
		// remove legend items
14203
		if (series.legendItem) {
14204
			series.chart.legend.destroyItem(series);
14205
		}
14206
 
14207
		// destroy all points with their elements
14208
		i = data.length;
14209
		while (i--) {
14210
			point = data[i];
14211
			if (point && point.destroy) {
14212
				point.destroy();
14213
			}
14214
		}
14215
		series.points = null;
14216
 
14217
		// Clear the animation timeout if we are destroying the series during initial animation
14218
		clearTimeout(series.animationTimeout);
14219
 
14220
		// Destroy all SVGElements associated to the series
14221
		for (prop in series) {
14222
			if (series[prop] instanceof SVGElement && !series[prop].survive) { // Survive provides a hook for not destroying
14223
 
14224
				// issue 134 workaround
14225
				destroy = issue134 && prop === 'group' ?
14226
					'hide' :
14227
					'destroy';
14228
 
14229
				series[prop][destroy]();
14230
			}
14231
		}
14232
 
14233
		// remove from hoverSeries
14234
		if (chart.hoverSeries === series) {
14235
			chart.hoverSeries = null;
14236
		}
14237
		erase(chart.series, series);
14238
 
14239
		// clear all members
14240
		for (prop in series) {
14241
			delete series[prop];
14242
		}
14243
	},
14244
 
14245
	/**
14246
	 * Return the graph path of a segment
14247
	 */
14248
	getSegmentPath: function (segment) {
14249
		var series = this,
14250
			segmentPath = [],
14251
			step = series.options.step;
14252
 
14253
		// build the segment line
14254
		each(segment, function (point, i) {
14255
 
14256
			var plotX = point.plotX,
14257
				plotY = point.plotY,
14258
				lastPoint;
14259
 
14260
			if (series.getPointSpline) { // generate the spline as defined in the SplineSeries object
14261
				segmentPath.push.apply(segmentPath, series.getPointSpline(segment, point, i));
14262
 
14263
			} else {
14264
 
14265
				// moveTo or lineTo
14266
				segmentPath.push(i ? L : M);
14267
 
14268
				// step line?
14269
				if (step && i) {
14270
					lastPoint = segment[i - 1];
14271
					if (step === 'right') {
14272
						segmentPath.push(
14273
							lastPoint.plotX,
14274
							plotY
14275
						);
14276
 
14277
					} else if (step === 'center') {
14278
						segmentPath.push(
14279
							(lastPoint.plotX + plotX) / 2,
14280
							lastPoint.plotY,
14281
							(lastPoint.plotX + plotX) / 2,
14282
							plotY
14283
						);
14284
 
14285
					} else {
14286
						segmentPath.push(
14287
							plotX,
14288
							lastPoint.plotY
14289
						);
14290
					}
14291
				}
14292
 
14293
				// normal line to next point
14294
				segmentPath.push(
14295
					point.plotX,
14296
					point.plotY
14297
				);
14298
			}
14299
		});
14300
 
14301
		return segmentPath;
14302
	},
14303
 
14304
	/**
14305
	 * Get the graph path
14306
	 */
14307
	getGraphPath: function () {
14308
		var series = this,
14309
			graphPath = [],
14310
			segmentPath,
14311
			singlePoints = []; // used in drawTracker
14312
 
14313
		// Divide into segments and build graph and area paths
14314
		each(series.segments, function (segment) {
14315
 
14316
			segmentPath = series.getSegmentPath(segment);
14317
 
14318
			// add the segment to the graph, or a single point for tracking
14319
			if (segment.length > 1) {
14320
				graphPath = graphPath.concat(segmentPath);
14321
			} else {
14322
				singlePoints.push(segment[0]);
14323
			}
14324
		});
14325
 
14326
		// Record it for use in drawGraph and drawTracker, and return graphPath
14327
		series.singlePoints = singlePoints;
14328
		series.graphPath = graphPath;
14329
 
14330
		return graphPath;
14331
 
14332
	},
14333
 
14334
	/**
14335
	 * Draw the actual graph
14336
	 */
14337
	drawGraph: function () {
14338
		var series = this,
14339
			options = this.options,
14340
			props = [['graph', options.lineColor || this.color, options.dashStyle]],
14341
			lineWidth = options.lineWidth,
14342
			roundCap = options.linecap !== 'square',
14343
			graphPath = this.getGraphPath(),
14344
			fillColor = (this.fillGraph && this.color) || NONE, // polygon series use filled graph
14345
			zones = this.zones;
14346
 
14347
		each(zones, function (threshold, i) {
14348
			props.push(['zoneGraph' + i, threshold.color || series.color, threshold.dashStyle || options.dashStyle]);
14349
		});
14350
 
14351
		// Draw the graph
14352
		each(props, function (prop, i) {
14353
			var graphKey = prop[0],
14354
				graph = series[graphKey],
14355
				attribs;
14356
 
14357
			if (graph) {
14358
				graph.animate({ d: graphPath });
14359
 
14360
			} else if ((lineWidth || fillColor) && graphPath.length) { // #1487
14361
				attribs = {
14362
					stroke: prop[1],
14363
					'stroke-width': lineWidth,
14364
					fill: fillColor,
14365
					zIndex: 1 // #1069
14366
				};
14367
				if (prop[2]) {
14368
					attribs.dashstyle = prop[2];
14369
				} else if (roundCap) {
14370
					attribs['stroke-linecap'] = attribs['stroke-linejoin'] = 'round';
14371
				}
14372
 
14373
				series[graphKey] = series.chart.renderer.path(graphPath)
14374
					.attr(attribs)
14375
					.add(series.group)
14376
					.shadow((i < 2) && options.shadow); // add shadow to normal series (0) or to first zone (1) #3932
14377
			}
14378
		});
14379
	},
14380
 
14381
	/**
14382
	 * Clip the graphs into the positive and negative coloured graphs
14383
	 */
14384
	applyZones: function () {
14385
		var series = this,
14386
			chart = this.chart,
14387
			renderer = chart.renderer,
14388
			zones = this.zones,
14389
			translatedFrom,
14390
			translatedTo,
14391
			clips = this.clips || [],
14392
			clipAttr,
14393
			graph = this.graph,
14394
			area = this.area,
14395
			chartSizeMax = mathMax(chart.chartWidth, chart.chartHeight),
14396
			axis = this[(this.zoneAxis || 'y') + 'Axis'],
14397
			extremes,
14398
			reversed = axis.reversed,
14399
			inverted = chart.inverted,
14400
			horiz = axis.horiz,
14401
			pxRange,
14402
			pxPosMin,
14403
			pxPosMax,
14404
			ignoreZones = false;
14405
 
14406
		if (zones.length && (graph || area) && axis.min !== UNDEFINED) {
14407
			// The use of the Color Threshold assumes there are no gaps
14408
			// so it is safe to hide the original graph and area
14409
			if (graph) {
14410
				graph.hide();
14411
			}
14412
			if (area) {
14413
				area.hide();
14414
			}
14415
 
14416
			// Create the clips
14417
			extremes = axis.getExtremes();
14418
			each(zones, function (threshold, i) {
14419
 
14420
				translatedFrom = reversed ?
14421
					(horiz ? chart.plotWidth : 0) :
14422
					(horiz ? 0 : axis.toPixels(extremes.min));
14423
				translatedFrom = mathMin(mathMax(pick(translatedTo, translatedFrom), 0), chartSizeMax);
14424
				translatedTo = mathMin(mathMax(mathRound(axis.toPixels(pick(threshold.value, extremes.max), true)), 0), chartSizeMax);
14425
 
14426
				if (ignoreZones) {
14427
					translatedFrom = translatedTo = axis.toPixels(extremes.max);
14428
				}
14429
 
14430
				pxRange = Math.abs(translatedFrom - translatedTo);
14431
				pxPosMin = mathMin(translatedFrom, translatedTo);
14432
				pxPosMax = mathMax(translatedFrom, translatedTo);
14433
				if (axis.isXAxis) {
14434
					clipAttr = {
14435
						x: inverted ? pxPosMax : pxPosMin,
14436
						y: 0,
14437
						width: pxRange,
14438
						height: chartSizeMax
14439
					};
14440
					if (!horiz) {
14441
						clipAttr.x = chart.plotHeight - clipAttr.x;
14442
					}
14443
				} else {
14444
					clipAttr = {
14445
						x: 0,
14446
						y: inverted ? pxPosMax : pxPosMin,
14447
						width: chartSizeMax,
14448
						height: pxRange
14449
					};
14450
					if (horiz) {
14451
						clipAttr.y = chart.plotWidth - clipAttr.y;
14452
					}
14453
				}
14454
 
14455
				/// VML SUPPPORT
14456
				if (chart.inverted && renderer.isVML) {
14457
					if (axis.isXAxis) {
14458
						clipAttr = {
14459
							x: 0,
14460
							y: reversed ? pxPosMin : pxPosMax,
14461
							height: clipAttr.width,
14462
							width: chart.chartWidth
14463
						};
14464
					} else {
14465
						clipAttr = {
14466
							x: clipAttr.y - chart.plotLeft - chart.spacingBox.x,
14467
							y: 0,
14468
							width: clipAttr.height,
14469
							height: chart.chartHeight
14470
						};
14471
					}
14472
				}
14473
				/// END OF VML SUPPORT
14474
 
14475
				if (clips[i]) {
14476
					clips[i].animate(clipAttr);
14477
				} else {
14478
					clips[i] = renderer.clipRect(clipAttr);
14479
 
14480
					if (graph) {
14481
						series['zoneGraph' + i].clip(clips[i]);
14482
					}
14483
 
14484
					if (area) {
14485
						series['zoneArea' + i].clip(clips[i]);
14486
					}
14487
				}
14488
				// if this zone extends out of the axis, ignore the others
14489
				ignoreZones = threshold.value > extremes.max;
14490
			});
14491
			this.clips = clips;
14492
		}
14493
	},
14494
 
14495
	/**
14496
	 * Initialize and perform group inversion on series.group and series.markerGroup
14497
	 */
14498
	invertGroups: function () {
14499
		var series = this,
14500
			chart = series.chart;
14501
 
14502
		// Pie, go away (#1736)
14503
		if (!series.xAxis) {
14504
			return;
14505
		}
14506
 
14507
		// A fixed size is needed for inversion to work
14508
		function setInvert() {
14509
			var size = {
14510
				width: series.yAxis.len,
14511
				height: series.xAxis.len
14512
			};
14513
 
14514
			each(['group', 'markerGroup'], function (groupName) {
14515
				if (series[groupName]) {
14516
					series[groupName].attr(size).invert();
14517
				}
14518
			});
14519
		}
14520
 
14521
		addEvent(chart, 'resize', setInvert); // do it on resize
14522
		addEvent(series, 'destroy', function () {
14523
			removeEvent(chart, 'resize', setInvert);
14524
		});
14525
 
14526
		// Do it now
14527
		setInvert(); // do it now
14528
 
14529
		// On subsequent render and redraw, just do setInvert without setting up events again
14530
		series.invertGroups = setInvert;
14531
	},
14532
 
14533
	/**
14534
	 * General abstraction for creating plot groups like series.group, series.dataLabelsGroup and
14535
	 * series.markerGroup. On subsequent calls, the group will only be adjusted to the updated plot size.
14536
	 */
14537
	plotGroup: function (prop, name, visibility, zIndex, parent) {
14538
		var group = this[prop],
14539
			isNew = !group;
14540
 
14541
		// Generate it on first call
14542
		if (isNew) {
14543
			this[prop] = group = this.chart.renderer.g(name)
14544
				.attr({
14545
					visibility: visibility,
14546
					zIndex: zIndex || 0.1 // IE8 needs this
14547
				})
14548
				.add(parent);
14549
		}
14550
		// Place it on first and subsequent (redraw) calls
14551
		group[isNew ? 'attr' : 'animate'](this.getPlotBox());
14552
		return group;
14553
	},
14554
 
14555
	/**
14556
	 * Get the translation and scale for the plot area of this series
14557
	 */
14558
	getPlotBox: function () {
14559
		var chart = this.chart,
14560
			xAxis = this.xAxis,
14561
			yAxis = this.yAxis;
14562
 
14563
		// Swap axes for inverted (#2339)
14564
		if (chart.inverted) {
14565
			xAxis = yAxis;
14566
			yAxis = this.xAxis;
14567
		}
14568
		return {
14569
			translateX: xAxis ? xAxis.left : chart.plotLeft,
14570
			translateY: yAxis ? yAxis.top : chart.plotTop,
14571
			scaleX: 1, // #1623
14572
			scaleY: 1
14573
		};
14574
	},
14575
 
14576
	/**
14577
	 * Render the graph and markers
14578
	 */
14579
	render: function () {
14580
		var series = this,
14581
			chart = series.chart,
14582
			group,
14583
			options = series.options,
14584
			animation = options.animation,
14585
			// Animation doesn't work in IE8 quirks when the group div is hidden,
14586
			// and looks bad in other oldIE
14587
			animDuration = (animation && !!series.animate && chart.renderer.isSVG && pick(animation.duration, 500)) || 0,
14588
			visibility = series.visible ? VISIBLE : HIDDEN,
14589
			zIndex = options.zIndex,
14590
			hasRendered = series.hasRendered,
14591
			chartSeriesGroup = chart.seriesGroup;
14592
 
14593
		// the group
14594
		group = series.plotGroup(
14595
			'group',
14596
			'series',
14597
			visibility,
14598
			zIndex,
14599
			chartSeriesGroup
14600
		);
14601
 
14602
		series.markerGroup = series.plotGroup(
14603
			'markerGroup',
14604
			'markers',
14605
			visibility,
14606
			zIndex,
14607
			chartSeriesGroup
14608
		);
14609
 
14610
		// initiate the animation
14611
		if (animDuration) {
14612
			series.animate(true);
14613
		}
14614
 
14615
		// cache attributes for shapes
14616
		series.getAttribs();
14617
 
14618
		// SVGRenderer needs to know this before drawing elements (#1089, #1795)
14619
		group.inverted = series.isCartesian ? chart.inverted : false;
14620
 
14621
		// draw the graph if any
14622
		if (series.drawGraph) {
14623
			series.drawGraph();
14624
			series.applyZones();
14625
		}
14626
 
14627
		each(series.points, function (point) {
14628
			if (point.redraw) {
14629
				point.redraw();
14630
			}
14631
		});
14632
 
14633
		// draw the data labels (inn pies they go before the points)
14634
		if (series.drawDataLabels) {
14635
			series.drawDataLabels();
14636
		}
14637
 
14638
		// draw the points
14639
		if (series.visible) {
14640
			series.drawPoints();
14641
		}
14642
 
14643
 
14644
		// draw the mouse tracking area
14645
		if (series.drawTracker && series.options.enableMouseTracking !== false) {
14646
			series.drawTracker();
14647
		}
14648
 
14649
		// Handle inverted series and tracker groups
14650
		if (chart.inverted) {
14651
			series.invertGroups();
14652
		}
14653
 
14654
		// Initial clipping, must be defined after inverting groups for VML. Applies to columns etc. (#3839).
14655
		if (options.clip !== false && !series.sharedClipKey && !hasRendered) {
14656
			group.clip(chart.clipRect);
14657
		}
14658
 
14659
		// Run the animation
14660
		if (animDuration) {
14661
			series.animate();
14662
		}
14663
 
14664
		// Call the afterAnimate function on animation complete (but don't overwrite the animation.complete option
14665
		// which should be available to the user).
14666
		if (!hasRendered) {
14667
			if (animDuration) {
14668
				series.animationTimeout = setTimeout(function () {
14669
					series.afterAnimate();
14670
				}, animDuration);
14671
			} else {
14672
				series.afterAnimate();
14673
			}
14674
		}
14675
 
14676
		series.isDirty = series.isDirtyData = false; // means data is in accordance with what you see
14677
		// (See #322) series.isDirty = series.isDirtyData = false; // means data is in accordance with what you see
14678
		series.hasRendered = true;
14679
	},
14680
 
14681
	/**
14682
	 * Redraw the series after an update in the axes.
14683
	 */
14684
	redraw: function () {
14685
		var series = this,
14686
			chart = series.chart,
14687
			wasDirtyData = series.isDirtyData, // cache it here as it is set to false in render, but used after
14688
			wasDirty = series.isDirty,
14689
			group = series.group,
14690
			xAxis = series.xAxis,
14691
			yAxis = series.yAxis;
14692
 
14693
		// reposition on resize
14694
		if (group) {
14695
			if (chart.inverted) {
14696
				group.attr({
14697
					width: chart.plotWidth,
14698
					height: chart.plotHeight
14699
				});
14700
			}
14701
 
14702
			group.animate({
14703
				translateX: pick(xAxis && xAxis.left, chart.plotLeft),
14704
				translateY: pick(yAxis && yAxis.top, chart.plotTop)
14705
			});
14706
		}
14707
 
14708
		series.translate();
14709
		series.render();
14710
		if (wasDirtyData) {
14711
			fireEvent(series, 'updatedData');
14712
		}
14713
		if (wasDirty || wasDirtyData) {			// #3945 recalculate the kdtree when dirty
14714
			delete this.kdTree; // #3868 recalculate the kdtree with dirty data
14715
		}
14716
	},
14717
 
14718
	/**
14719
	 * KD Tree && PointSearching Implementation
14720
	 */
14721
 
14722
	kdDimensions: 1,
14723
	kdAxisArray: ['clientX', 'plotY'],
14724
 
14725
	searchPoint: function (e, compareX) {
14726
		var series = this,
14727
			xAxis = series.xAxis,
14728
			yAxis = series.yAxis,
14729
			inverted = series.chart.inverted;
14730
 
14731
		return this.searchKDTree({
14732
			clientX: inverted ? xAxis.len - e.chartY + xAxis.pos : e.chartX - xAxis.pos,
14733
			plotY: inverted ? yAxis.len - e.chartX + yAxis.pos : e.chartY - yAxis.pos
14734
		}, compareX);
14735
	},
14736
 
14737
	buildKDTree: function () {
14738
		var series = this,
14739
			dimensions = series.kdDimensions;
14740
 
14741
		// Internal function
14742
		function _kdtree(points, depth, dimensions) {
14743
			var axis, median, length = points && points.length;
14744
 
14745
			if (length) {
14746
 
14747
				// alternate between the axis
14748
				axis = series.kdAxisArray[depth % dimensions];
14749
 
14750
				// sort point array
14751
				points.sort(function(a, b) {
14752
					return a[axis] - b[axis];
14753
				});
14754
 
14755
				median = Math.floor(length / 2);
14756
 
14757
				// build and return nod
14758
				return {
14759
					point: points[median],
14760
					left: _kdtree(points.slice(0, median), depth + 1, dimensions),
14761
					right: _kdtree(points.slice(median + 1), depth + 1, dimensions)
14762
				};
14763
 
14764
			}
14765
		}
14766
 
14767
		// Start the recursive build process with a clone of the points array and null points filtered out (#3873)
14768
		function startRecursive() {
14769
			var points = grep(series.points || [], function (point) { // #4390
14770
				return point.y !== null;
14771
			});
14772
 
14773
			series.kdTree = _kdtree(points, dimensions, dimensions);
14774
		}
14775
		delete series.kdTree;
14776
 
14777
		if (series.options.kdSync) {  // For testing tooltips, don't build async
14778
			startRecursive();
14779
		} else {
14780
			setTimeout(startRecursive);
14781
		}
14782
	},
14783
 
14784
	searchKDTree: function (point, compareX) {
14785
		var series = this,
14786
			kdX = this.kdAxisArray[0],
14787
			kdY = this.kdAxisArray[1],
14788
			kdComparer = compareX ? 'distX' : 'dist';
14789
 
14790
		// Set the one and two dimensional distance on the point object
14791
		function setDistance(p1, p2) {
14792
			var x = (defined(p1[kdX]) && defined(p2[kdX])) ? Math.pow(p1[kdX] - p2[kdX], 2) : null,
14793
				y = (defined(p1[kdY]) && defined(p2[kdY])) ? Math.pow(p1[kdY] - p2[kdY], 2) : null,
14794
				r = (x || 0) + (y || 0);
14795
 
14796
			p2.dist = defined(r) ? Math.sqrt(r) : Number.MAX_VALUE;
14797
			p2.distX = defined(x) ? Math.sqrt(x) : Number.MAX_VALUE;
14798
		}
14799
		function _search(search, tree, depth, dimensions) {
14800
			var point = tree.point,
14801
				axis = series.kdAxisArray[depth % dimensions],
14802
				tdist,
14803
				sideA,
14804
				sideB,
14805
				ret = point,
14806
				nPoint1,
14807
				nPoint2;
14808
 
14809
			setDistance(search, point);
14810
 
14811
			// Pick side based on distance to splitting point
14812
			tdist = search[axis] - point[axis];
14813
			sideA = tdist < 0 ? 'left' : 'right';
14814
			sideB = tdist < 0 ? 'right' : 'left';
14815
 
14816
			// End of tree
14817
			if (tree[sideA]) {
14818
				nPoint1 =_search(search, tree[sideA], depth + 1, dimensions);
14819
 
14820
				ret = (nPoint1[kdComparer] < ret[kdComparer] ? nPoint1 : point);
14821
			}
14822
			if (tree[sideB]) {
14823
				// compare distance to current best to splitting point to decide wether to check side B or not
14824
				if (Math.sqrt(tdist * tdist) < ret[kdComparer]) {
14825
					nPoint2 = _search(search, tree[sideB], depth + 1, dimensions);
14826
					ret = (nPoint2[kdComparer] < ret[kdComparer] ? nPoint2 : ret);
14827
				}
14828
			}
14829
 
14830
			return ret;
14831
		}
14832
 
14833
		if (!this.kdTree) {
14834
			this.buildKDTree();
14835
		}
14836
 
14837
		if (this.kdTree) {
14838
			return _search(point,
14839
				this.kdTree, this.kdDimensions, this.kdDimensions);
14840
		}
14841
	}
14842
 
14843
}; // end Series prototype
14844
 
14845
/**
14846
 * The class for stack items
14847
 */
14848
function StackItem(axis, options, isNegative, x, stackOption) {
14849
 
14850
	var inverted = axis.chart.inverted;
14851
 
14852
	this.axis = axis;
14853
 
14854
	// Tells if the stack is negative
14855
	this.isNegative = isNegative;
14856
 
14857
	// Save the options to be able to style the label
14858
	this.options = options;
14859
 
14860
	// Save the x value to be able to position the label later
14861
	this.x = x;
14862
 
14863
	// Initialize total value
14864
	this.total = null;
14865
 
14866
	// This will keep each points' extremes stored by series.index and point index
14867
	this.points = {};
14868
 
14869
	// Save the stack option on the series configuration object, and whether to treat it as percent
14870
	this.stack = stackOption;
14871
 
14872
	// The align options and text align varies on whether the stack is negative and
14873
	// if the chart is inverted or not.
14874
	// First test the user supplied value, then use the dynamic.
14875
	this.alignOptions = {
14876
		align: options.align || (inverted ? (isNegative ? 'left' : 'right') : 'center'),
14877
		verticalAlign: options.verticalAlign || (inverted ? 'middle' : (isNegative ? 'bottom' : 'top')),
14878
		y: pick(options.y, inverted ? 4 : (isNegative ? 14 : -6)),
14879
		x: pick(options.x, inverted ? (isNegative ? -6 : 6) : 0)
14880
	};
14881
 
14882
	this.textAlign = options.textAlign || (inverted ? (isNegative ? 'right' : 'left') : 'center');
14883
}
14884
 
14885
StackItem.prototype = {
14886
	destroy: function () {
14887
		destroyObjectProperties(this, this.axis);
14888
	},
14889
 
14890
	/**
14891
	 * Renders the stack total label and adds it to the stack label group.
14892
	 */
14893
	render: function (group) {
14894
		var options = this.options,
14895
			formatOption = options.format,
14896
			str = formatOption ?
14897
				format(formatOption, this) :
14898
				options.formatter.call(this);  // format the text in the label
14899
 
14900
		// Change the text to reflect the new total and set visibility to hidden in case the serie is hidden
14901
		if (this.label) {
14902
			this.label.attr({text: str, visibility: HIDDEN});
14903
		// Create new label
14904
		} else {
14905
			this.label =
14906
				this.axis.chart.renderer.text(str, null, null, options.useHTML)		// dummy positions, actual position updated with setOffset method in columnseries
14907
					.css(options.style)				// apply style
14908
					.attr({
14909
						align: this.textAlign,				// fix the text-anchor
14910
						rotation: options.rotation,	// rotation
14911
						visibility: HIDDEN					// hidden until setOffset is called
14912
					})
14913
					.add(group);							// add to the labels-group
14914
		}
14915
	},
14916
 
14917
	/**
14918
	 * Sets the offset that the stack has from the x value and repositions the label.
14919
	 */
14920
	setOffset: function (xOffset, xWidth) {
14921
		var stackItem = this,
14922
			axis = stackItem.axis,
14923
			chart = axis.chart,
14924
			inverted = chart.inverted,
14925
			reversed = axis.reversed,
14926
			neg = (this.isNegative && !reversed) || (!this.isNegative && reversed), // #4056
14927
			y = axis.translate(axis.usePercentage ? 100 : this.total, 0, 0, 0, 1), // stack value translated mapped to chart coordinates
14928
			yZero = axis.translate(0),						// stack origin
14929
			h = mathAbs(y - yZero),							// stack height
14930
			x = chart.xAxis[0].translate(this.x) + xOffset,	// stack x position
14931
			plotHeight = chart.plotHeight,
14932
			stackBox = {	// this is the box for the complete stack
14933
				x: inverted ? (neg ? y : y - h) : x,
14934
				y: inverted ? plotHeight - x - xWidth : (neg ? (plotHeight - y - h) : plotHeight - y),
14935
				width: inverted ? h : xWidth,
14936
				height: inverted ? xWidth : h
14937
			},
14938
			label = this.label,
14939
			alignAttr;
14940
 
14941
		if (label) {
14942
			label.align(this.alignOptions, null, stackBox);	// align the label to the box
14943
 
14944
			// Set visibility (#678)
14945
			alignAttr = label.alignAttr;
14946
			label[this.options.crop === false || chart.isInsidePlot(alignAttr.x, alignAttr.y) ? 'show' : 'hide'](true);
14947
		}
14948
	}
14949
};
14950
 
14951
/**
14952
 * Generate stacks for each series and calculate stacks total values
14953
 */
14954
Chart.prototype.getStacks = function () {
14955
	var chart = this;
14956
 
14957
	// reset stacks for each yAxis
14958
	each(chart.yAxis, function (axis) {
14959
		if (axis.stacks && axis.hasVisibleSeries) {
14960
			axis.oldStacks = axis.stacks;
14961
		}
14962
	});
14963
 
14964
	each(chart.series, function (series) {
14965
		if (series.options.stacking && (series.visible === true || chart.options.chart.ignoreHiddenSeries === false)) {
14966
			series.stackKey = series.type + pick(series.options.stack, '');
14967
		}
14968
	});
14969
};
14970
 
14971
 
14972
// Stacking methods defined on the Axis prototype
14973
 
14974
/**
14975
 * Build the stacks from top down
14976
 */
14977
Axis.prototype.buildStacks = function () {
14978
	var series = this.series,
14979
		reversedStacks = pick(this.options.reversedStacks, true),
14980
		i = series.length;
14981
	if (!this.isXAxis) {
14982
		this.usePercentage = false;
14983
		while (i--) {
14984
			series[reversedStacks ? i : series.length - i - 1].setStackedPoints();
14985
		}
14986
		// Loop up again to compute percent stack
14987
		if (this.usePercentage) {
14988
			for (i = 0; i < series.length; i++) {
14989
				series[i].setPercentStacks();
14990
			}
14991
		}
14992
	}
14993
};
14994
 
14995
Axis.prototype.renderStackTotals = function () {
14996
	var axis = this,
14997
		chart = axis.chart,
14998
		renderer = chart.renderer,
14999
		stacks = axis.stacks,
15000
		stackKey,
15001
		oneStack,
15002
		stackCategory,
15003
		stackTotalGroup = axis.stackTotalGroup;
15004
 
15005
	// Create a separate group for the stack total labels
15006
	if (!stackTotalGroup) {
15007
		axis.stackTotalGroup = stackTotalGroup =
15008
			renderer.g('stack-labels')
15009
				.attr({
15010
					visibility: VISIBLE,
15011
					zIndex: 6
15012
				})
15013
				.add();
15014
	}
15015
 
15016
	// plotLeft/Top will change when y axis gets wider so we need to translate the
15017
	// stackTotalGroup at every render call. See bug #506 and #516
15018
	stackTotalGroup.translate(chart.plotLeft, chart.plotTop);
15019
 
15020
	// Render each stack total
15021
	for (stackKey in stacks) {
15022
		oneStack = stacks[stackKey];
15023
		for (stackCategory in oneStack) {
15024
			oneStack[stackCategory].render(stackTotalGroup);
15025
		}
15026
	}
15027
};
15028
 
15029
/**
15030
 * Set all the stacks to initial states and destroy unused ones.
15031
 */
15032
Axis.prototype.resetStacks = function () {
15033
	var stacks = this.stacks,
15034
		type,
15035
		i;
15036
	if (!this.isXAxis) {
15037
		for (type in stacks) {
15038
			for (i in stacks[type]) {
15039
 
15040
				// Clean up memory after point deletion (#1044, #4320)
15041
				if (stacks[type][i].touched < this.stacksTouched) {
15042
					stacks[type][i].destroy();
15043
					delete stacks[type][i];
15044
 
15045
				// Reset stacks
15046
				} else {
15047
					stacks[type][i].total = null;
15048
					stacks[type][i].cum = 0;
15049
				}
15050
			}
15051
		}
15052
	}
15053
};
15054
 
15055
Axis.prototype.cleanStacks = function () {
15056
	var stacks, type, i;
15057
 
15058
	if (!this.isXAxis) {
15059
		if (this.oldStacks) {
15060
			stacks = this.stacks = this.oldStacks;
15061
		}
15062
 
15063
		// reset stacks
15064
		for (type in stacks) {
15065
			for (i in stacks[type]) {
15066
				stacks[type][i].cum = stacks[type][i].total;
15067
			}
15068
		}
15069
	}
15070
};
15071
 
15072
 
15073
// Stacking methods defnied for Series prototype
15074
 
15075
/**
15076
 * Adds series' points value to corresponding stack
15077
 */
15078
Series.prototype.setStackedPoints = function () {
15079
	if (!this.options.stacking || (this.visible !== true && this.chart.options.chart.ignoreHiddenSeries !== false)) {
15080
		return;
15081
	}
15082
 
15083
	var series = this,
15084
		xData = series.processedXData,
15085
		yData = series.processedYData,
15086
		stackedYData = [],
15087
		yDataLength = yData.length,
15088
		seriesOptions = series.options,
15089
		threshold = seriesOptions.threshold,
15090
		stackThreshold = seriesOptions.startFromThreshold ? threshold : 0,
15091
		stackOption = seriesOptions.stack,
15092
		stacking = seriesOptions.stacking,
15093
		stackKey = series.stackKey,
15094
		negKey = '-' + stackKey,
15095
		negStacks = series.negStacks,
15096
		yAxis = series.yAxis,
15097
		stacks = yAxis.stacks,
15098
		oldStacks = yAxis.oldStacks,
15099
		isNegative,
15100
		stack,
15101
		other,
15102
		key,
15103
		pointKey,
15104
		i,
15105
		x,
15106
		y;
15107
 
15108
 
15109
	yAxis.stacksTouched += 1;
15110
 
15111
	// loop over the non-null y values and read them into a local array
15112
	for (i = 0; i < yDataLength; i++) {
15113
		x = xData[i];
15114
		y = yData[i];
15115
		pointKey = series.index + ',' + i;
15116
 
15117
		// Read stacked values into a stack based on the x value,
15118
		// the sign of y and the stack key. Stacking is also handled for null values (#739)
15119
		isNegative = negStacks && y < (stackThreshold ? 0 : threshold);
15120
		key = isNegative ? negKey : stackKey;
15121
 
15122
		// Create empty object for this stack if it doesn't exist yet
15123
		if (!stacks[key]) {
15124
			stacks[key] = {};
15125
		}
15126
 
15127
		// Initialize StackItem for this x
15128
		if (!stacks[key][x]) {
15129
			if (oldStacks[key] && oldStacks[key][x]) {
15130
				stacks[key][x] = oldStacks[key][x];
15131
				stacks[key][x].total = null;
15132
			} else {
15133
				stacks[key][x] = new StackItem(yAxis, yAxis.options.stackLabels, isNegative, x, stackOption);
15134
			}
15135
		}
15136
 
15137
		// If the StackItem doesn't exist, create it first
15138
		stack = stacks[key][x];
15139
		//stack.points[pointKey] = [stack.cum || stackThreshold];
15140
		stack.points[pointKey] = [pick(stack.cum, stackThreshold)];
15141
		stack.touched = yAxis.stacksTouched;
15142
 
15143
 
15144
		// Add value to the stack total
15145
		if (stacking === 'percent') {
15146
 
15147
			// Percent stacked column, totals are the same for the positive and negative stacks
15148
			other = isNegative ? stackKey : negKey;
15149
			if (negStacks && stacks[other] && stacks[other][x]) {
15150
				other = stacks[other][x];
15151
				stack.total = other.total = mathMax(other.total, stack.total) + mathAbs(y) || 0;
15152
 
15153
			// Percent stacked areas
15154
			} else {
15155
				stack.total = correctFloat(stack.total + (mathAbs(y) || 0));
15156
			}
15157
		} else {
15158
			stack.total = correctFloat(stack.total + (y || 0));
15159
		}
15160
 
15161
		stack.cum = pick(stack.cum, stackThreshold) + (y || 0);
15162
 
15163
		stack.points[pointKey].push(stack.cum);
15164
		stackedYData[i] = stack.cum;
15165
 
15166
	}
15167
 
15168
	if (stacking === 'percent') {
15169
		yAxis.usePercentage = true;
15170
	}
15171
 
15172
	this.stackedYData = stackedYData; // To be used in getExtremes
15173
 
15174
	// Reset old stacks
15175
	yAxis.oldStacks = {};
15176
};
15177
 
15178
/**
15179
 * Iterate over all stacks and compute the absolute values to percent
15180
 */
15181
Series.prototype.setPercentStacks = function () {
15182
	var series = this,
15183
		stackKey = series.stackKey,
15184
		stacks = series.yAxis.stacks,
15185
		processedXData = series.processedXData;
15186
 
15187
	each([stackKey, '-' + stackKey], function (key) {
15188
		var i = processedXData.length,
15189
			x,
15190
			stack,
15191
			pointExtremes,
15192
			totalFactor;
15193
 
15194
		while (i--) {
15195
			x = processedXData[i];
15196
			stack = stacks[key] && stacks[key][x];
15197
			pointExtremes = stack && stack.points[series.index + ',' + i];
15198
			if (pointExtremes) {
15199
				totalFactor = stack.total ? 100 / stack.total : 0;
15200
				pointExtremes[0] = correctFloat(pointExtremes[0] * totalFactor); // Y bottom value
15201
				pointExtremes[1] = correctFloat(pointExtremes[1] * totalFactor); // Y value
15202
				series.stackedYData[i] = pointExtremes[1];
15203
			}
15204
		}
15205
	});
15206
};
15207
 
15208
// Extend the Chart prototype for dynamic methods
15209
extend(Chart.prototype, {
15210
 
15211
	/**
15212
	 * Add a series dynamically after  time
15213
	 *
15214
	 * @param {Object} options The config options
15215
	 * @param {Boolean} redraw Whether to redraw the chart after adding. Defaults to true.
15216
	 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
15217
	 *    configuration
15218
	 *
15219
	 * @return {Object} series The newly created series object
15220
	 */
15221
	addSeries: function (options, redraw, animation) {
15222
		var series,
15223
			chart = this;
15224
 
15225
		if (options) {
15226
			redraw = pick(redraw, true); // defaults to true
15227
 
15228
			fireEvent(chart, 'addSeries', { options: options }, function () {
15229
				series = chart.initSeries(options);
15230
 
15231
				chart.isDirtyLegend = true; // the series array is out of sync with the display
15232
				chart.linkSeries();
15233
				if (redraw) {
15234
					chart.redraw(animation);
15235
				}
15236
			});
15237
		}
15238
 
15239
		return series;
15240
	},
15241
 
15242
	/**
15243
     * Add an axis to the chart
15244
     * @param {Object} options The axis option
15245
     * @param {Boolean} isX Whether it is an X axis or a value axis
15246
     */
15247
	addAxis: function (options, isX, redraw, animation) {
15248
		var key = isX ? 'xAxis' : 'yAxis',
15249
			chartOptions = this.options,
15250
			axis;
15251
 
15252
		/*jslint unused: false*/
15253
		axis = new Axis(this, merge(options, {
15254
			index: this[key].length,
15255
			isX: isX
15256
		}));
15257
		/*jslint unused: true*/
15258
 
15259
		// Push the new axis options to the chart options
15260
		chartOptions[key] = splat(chartOptions[key] || {});
15261
		chartOptions[key].push(options);
15262
 
15263
		if (pick(redraw, true)) {
15264
			this.redraw(animation);
15265
		}
15266
	},
15267
 
15268
	/**
15269
	 * Dim the chart and show a loading text or symbol
15270
	 * @param {String} str An optional text to show in the loading label instead of the default one
15271
	 */
15272
	showLoading: function (str) {
15273
		var chart = this,
15274
			options = chart.options,
15275
			loadingDiv = chart.loadingDiv,
15276
			loadingOptions = options.loading,
15277
			setLoadingSize = function () {
15278
				if (loadingDiv) {
15279
					css(loadingDiv, {
15280
						left: chart.plotLeft + PX,
15281
						top: chart.plotTop + PX,
15282
						width: chart.plotWidth + PX,
15283
						height: chart.plotHeight + PX
15284
					});
15285
				}
15286
			};
15287
 
15288
		// create the layer at the first call
15289
		if (!loadingDiv) {
15290
			chart.loadingDiv = loadingDiv = createElement(DIV, {
15291
				className: PREFIX + 'loading'
15292
			}, extend(loadingOptions.style, {
15293
				zIndex: 10,
15294
				display: NONE
15295
			}), chart.container);
15296
 
15297
			chart.loadingSpan = createElement(
15298
				'span',
15299
				null,
15300
				loadingOptions.labelStyle,
15301
				loadingDiv
15302
			);
15303
			addEvent(chart, 'redraw', setLoadingSize); // #1080
15304
		}
15305
 
15306
		// update text
15307
		chart.loadingSpan.innerHTML = str || options.lang.loading;
15308
 
15309
		// show it
15310
		if (!chart.loadingShown) {
15311
			css(loadingDiv, {
15312
				opacity: 0,
15313
				display: ''
15314
			});
15315
			animate(loadingDiv, {
15316
				opacity: loadingOptions.style.opacity
15317
			}, {
15318
				duration: loadingOptions.showDuration || 0
15319
			});
15320
			chart.loadingShown = true;
15321
		}
15322
		setLoadingSize();
15323
	},
15324
 
15325
	/**
15326
	 * Hide the loading layer
15327
	 */
15328
	hideLoading: function () {
15329
		var options = this.options,
15330
			loadingDiv = this.loadingDiv;
15331
 
15332
		if (loadingDiv) {
15333
			animate(loadingDiv, {
15334
				opacity: 0
15335
			}, {
15336
				duration: options.loading.hideDuration || 100,
15337
				complete: function () {
15338
					css(loadingDiv, { display: NONE });
15339
				}
15340
			});
15341
		}
15342
		this.loadingShown = false;
15343
	}
15344
});
15345
 
15346
// extend the Point prototype for dynamic methods
15347
extend(Point.prototype, {
15348
	/**
15349
	 * Update the point with new options (typically x/y data) and optionally redraw the series.
15350
	 *
15351
	 * @param {Object} options Point options as defined in the series.data array
15352
	 * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call
15353
	 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
15354
	 *    configuration
15355
	 *
15356
	 */
15357
	update: function (options, redraw, animation, runEvent) {
15358
		var point = this,
15359
			series = point.series,
15360
			graphic = point.graphic,
15361
			i,
15362
			chart = series.chart,
15363
			seriesOptions = series.options,
15364
			names = series.xAxis && series.xAxis.names;
15365
 
15366
		redraw = pick(redraw, true);
15367
 
15368
		function update() {
15369
 
15370
			point.applyOptions(options);
15371
 
15372
			// Update visuals
15373
			if (point.y === null && graphic) { // #4146
15374
				point.graphic = graphic.destroy();
15375
			}
15376
			if (isObject(options) && !isArray(options)) {
15377
				// Defer the actual redraw until getAttribs has been called (#3260)
15378
				point.redraw = function () {
15379
					if (graphic && graphic.element) {
15380
						if (options && options.marker && options.marker.symbol) {
15381
							point.graphic = graphic.destroy();
15382
						}
15383
					}
15384
					if (options && options.dataLabels && point.dataLabel) { // #2468
15385
						point.dataLabel = point.dataLabel.destroy();
15386
					}
15387
					point.redraw = null;
15388
				};
15389
			}
15390
 
15391
			// record changes in the parallel arrays
15392
			i = point.index;
15393
			series.updateParallelArrays(point, i);
15394
			if (names && point.name) {
15395
				names[point.x] = point.name;
15396
			}
15397
 
15398
			seriesOptions.data[i] = point.options;
15399
 
15400
			// redraw
15401
			series.isDirty = series.isDirtyData = true;
15402
			if (!series.fixedBox && series.hasCartesianSeries) { // #1906, #2320
15403
				chart.isDirtyBox = true;
15404
			}
15405
 
15406
			if (seriesOptions.legendType === 'point') { // #1831, #1885
15407
				chart.isDirtyLegend = true;
15408
			}
15409
			if (redraw) {
15410
				chart.redraw(animation);
15411
			}
15412
		}
15413
 
15414
		// Fire the event with a default handler of doing the update
15415
		if (runEvent === false) { // When called from setData
15416
			update();
15417
		} else {
15418
			point.firePointEvent('update', { options: options }, update);
15419
		}
15420
	},
15421
 
15422
	/**
15423
	 * Remove a point and optionally redraw the series and if necessary the axes
15424
	 * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call
15425
	 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
15426
	 *    configuration
15427
	 */
15428
	remove: function (redraw, animation) {
15429
		this.series.removePoint(inArray(this, this.series.data), redraw, animation);
15430
	}
15431
});
15432
 
15433
// Extend the series prototype for dynamic methods
15434
extend(Series.prototype, {
15435
	/**
15436
	 * Add a point dynamically after chart load time
15437
	 * @param {Object} options Point options as given in series.data
15438
	 * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call
15439
	 * @param {Boolean} shift If shift is true, a point is shifted off the start
15440
	 *    of the series as one is appended to the end.
15441
	 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
15442
	 *    configuration
15443
	 */
15444
	addPoint: function (options, redraw, shift, animation) {
15445
		var series = this,
15446
			seriesOptions = series.options,
15447
			data = series.data,
15448
			graph = series.graph,
15449
			area = series.area,
15450
			chart = series.chart,
15451
			names = series.xAxis && series.xAxis.names,
15452
			currentShift = (graph && graph.shift) || 0,
15453
			shiftShapes = ['graph', 'area'],
15454
			dataOptions = seriesOptions.data,
15455
			point,
15456
			isInTheMiddle,
15457
			xData = series.xData,
15458
			i,
15459
			x;
15460
 
15461
		setAnimation(animation, chart);
15462
 
15463
		// Make graph animate sideways
15464
		if (shift) {
15465
			i = series.zones.length;
15466
			while (i--) {
15467
				shiftShapes.push('zoneGraph' + i, 'zoneArea' + i);
15468
			}
15469
			each(shiftShapes, function (shape) {
15470
				if (series[shape]) {
15471
					series[shape].shift = currentShift + 1;
15472
				}
15473
			});
15474
		}
15475
		if (area) {
15476
			area.isArea = true; // needed in animation, both with and without shift
15477
		}
15478
 
15479
		// Optional redraw, defaults to true
15480
		redraw = pick(redraw, true);
15481
 
15482
		// Get options and push the point to xData, yData and series.options. In series.generatePoints
15483
		// the Point instance will be created on demand and pushed to the series.data array.
15484
		point = { series: series };
15485
		series.pointClass.prototype.applyOptions.apply(point, [options]);
15486
		x = point.x;
15487
 
15488
		// Get the insertion point
15489
		i = xData.length;
15490
		if (series.requireSorting && x < xData[i - 1]) {
15491
			isInTheMiddle = true;
15492
			while (i && xData[i - 1] > x) {
15493
				i--;
15494
			}
15495
		}
15496
 
15497
		series.updateParallelArrays(point, 'splice', i, 0, 0); // insert undefined item
15498
		series.updateParallelArrays(point, i); // update it
15499
 
15500
		if (names && point.name) {
15501
			names[x] = point.name;
15502
		}
15503
		dataOptions.splice(i, 0, options);
15504
 
15505
		if (isInTheMiddle) {
15506
			series.data.splice(i, 0, null);
15507
			series.processData();
15508
		}
15509
 
15510
		// Generate points to be added to the legend (#1329)
15511
		if (seriesOptions.legendType === 'point') {
15512
			series.generatePoints();
15513
		}
15514
 
15515
		// Shift the first point off the parallel arrays
15516
		// todo: consider series.removePoint(i) method
15517
		if (shift) {
15518
			if (data[0] && data[0].remove) {
15519
				data[0].remove(false);
15520
			} else {
15521
				data.shift();
15522
				series.updateParallelArrays(point, 'shift');
15523
 
15524
				dataOptions.shift();
15525
			}
15526
		}
15527
 
15528
		// redraw
15529
		series.isDirty = true;
15530
		series.isDirtyData = true;
15531
		if (redraw) {
15532
			series.getAttribs(); // #1937
15533
			chart.redraw();
15534
		}
15535
	},
15536
 
15537
	/**
15538
	 * Remove a point (rendered or not), by index
15539
	 */
15540
	removePoint: function (i, redraw, animation) {
15541
 
15542
		var series = this,
15543
			data = series.data,
15544
			point = data[i],
15545
			points = series.points,
15546
			chart = series.chart,
15547
			remove = function () {
15548
 
15549
				if (data.length === points.length) {
15550
					points.splice(i, 1);
15551
				}
15552
				data.splice(i, 1);
15553
				series.options.data.splice(i, 1);
15554
				series.updateParallelArrays(point || { series: series }, 'splice', i, 1);
15555
 
15556
				if (point) {
15557
					point.destroy();
15558
				}
15559
 
15560
				// redraw
15561
				series.isDirty = true;
15562
				series.isDirtyData = true;
15563
				if (redraw) {
15564
					chart.redraw();
15565
				}
15566
			};
15567
 
15568
		setAnimation(animation, chart);
15569
		redraw = pick(redraw, true);
15570
 
15571
		// Fire the event with a default handler of removing the point
15572
		if (point) {
15573
			point.firePointEvent('remove', null, remove);
15574
		} else {
15575
			remove();
15576
		}
15577
	},
15578
 
15579
	/**
15580
	 * Remove a series and optionally redraw the chart
15581
	 *
15582
	 * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call
15583
	 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
15584
	 *    configuration
15585
	 */
15586
 
15587
	remove: function (redraw, animation) {
15588
		var series = this,
15589
			chart = series.chart;
15590
		redraw = pick(redraw, true);
15591
 
15592
		if (!series.isRemoving) {  /* prevent triggering native event in jQuery
15593
				(calling the remove function from the remove event) */
15594
			series.isRemoving = true;
15595
 
15596
			// fire the event with a default handler of removing the point
15597
			fireEvent(series, 'remove', null, function () {
15598
 
15599
 
15600
				// destroy elements
15601
				series.destroy();
15602
 
15603
 
15604
				// redraw
15605
				chart.isDirtyLegend = chart.isDirtyBox = true;
15606
				chart.linkSeries();
15607
 
15608
				if (redraw) {
15609
					chart.redraw(animation);
15610
				}
15611
			});
15612
 
15613
		}
15614
		series.isRemoving = false;
15615
	},
15616
 
15617
	/**
15618
	 * Update the series with a new set of options
15619
	 */
15620
	update: function (newOptions, redraw) {
15621
		var series = this,
15622
			chart = this.chart,
15623
			// must use user options when changing type because this.options is merged
15624
			// in with type specific plotOptions
15625
			oldOptions = this.userOptions,
15626
			oldType = this.type,
15627
			proto = seriesTypes[oldType].prototype,
15628
			preserve = ['group', 'markerGroup', 'dataLabelsGroup'],
15629
			n;
15630
 
15631
		// If we're changing type or zIndex, create new groups (#3380, #3404)
15632
		if ((newOptions.type && newOptions.type !== oldType) || newOptions.zIndex !== undefined) {
15633
			preserve.length = 0;
15634
		}
15635
 
15636
		// Make sure groups are not destroyed (#3094)
15637
		each(preserve, function (prop) {
15638
			preserve[prop] = series[prop];
15639
			delete series[prop];
15640
		});
15641
 
15642
		// Do the merge, with some forced options
15643
		newOptions = merge(oldOptions, {
15644
			animation: false,
15645
			index: this.index,
15646
			pointStart: this.xData[0] // when updating after addPoint
15647
		}, { data: this.options.data }, newOptions);
15648
 
15649
		// Destroy the series and delete all properties. Reinsert all methods
15650
		// and properties from the new type prototype (#2270, #3719)
15651
		this.remove(false);
15652
		for (n in proto) {
15653
			this[n] = UNDEFINED;
15654
		}
15655
		extend(this, seriesTypes[newOptions.type || oldType].prototype);
15656
 
15657
		// Re-register groups (#3094)
15658
		each(preserve, function (prop) {
15659
			series[prop] = preserve[prop];
15660
		});
15661
 
15662
		this.init(chart, newOptions);
15663
		chart.linkSeries(); // Links are lost in this.remove (#3028)
15664
		if (pick(redraw, true)) {
15665
			chart.redraw(false);
15666
		}
15667
	}
15668
});
15669
 
15670
// Extend the Axis.prototype for dynamic methods
15671
extend(Axis.prototype, {
15672
 
15673
	/**
15674
	 * Update the axis with a new options structure
15675
	 */
15676
	update: function (newOptions, redraw) {
15677
		var chart = this.chart;
15678
 
15679
		newOptions = chart.options[this.coll][this.options.index] = merge(this.userOptions, newOptions);
15680
 
15681
		this.destroy(true);
15682
		this._addedPlotLB = this.chart._labelPanes = UNDEFINED; // #1611, #2887, #4314
15683
 
15684
		this.init(chart, extend(newOptions, { events: UNDEFINED }));
15685
 
15686
		chart.isDirtyBox = true;
15687
		if (pick(redraw, true)) {
15688
			chart.redraw();
15689
		}
15690
	},
15691
 
15692
	/**
15693
     * Remove the axis from the chart
15694
     */
15695
	remove: function (redraw) {
15696
		var chart = this.chart,
15697
			key = this.coll, // xAxis or yAxis
15698
			axisSeries = this.series,
15699
			i = axisSeries.length;
15700
 
15701
		// Remove associated series (#2687)
15702
		while (i--) {
15703
			if (axisSeries[i]) {
15704
				axisSeries[i].remove(false);
15705
			}
15706
		}
15707
 
15708
		// Remove the axis
15709
		erase(chart.axes, this);
15710
		erase(chart[key], this);
15711
		chart.options[key].splice(this.options.index, 1);
15712
		each(chart[key], function (axis, i) { // Re-index, #1706
15713
			axis.options.index = i;
15714
		});
15715
		this.destroy();
15716
		chart.isDirtyBox = true;
15717
 
15718
		if (pick(redraw, true)) {
15719
			chart.redraw();
15720
		}
15721
	},
15722
 
15723
	/**
15724
	 * Update the axis title by options
15725
	 */
15726
	setTitle: function (newTitleOptions, redraw) {
15727
		this.update({ title: newTitleOptions }, redraw);
15728
	},
15729
 
15730
	/**
15731
	 * Set new axis categories and optionally redraw
15732
	 * @param {Array} categories
15733
	 * @param {Boolean} redraw
15734
	 */
15735
	setCategories: function (categories, redraw) {
15736
		this.update({ categories: categories }, redraw);
15737
	}
15738
 
15739
});
15740
 
15741
 
15742
/**
15743
 * LineSeries object
15744
 */
15745
var LineSeries = extendClass(Series);
15746
seriesTypes.line = LineSeries;
15747
 
15748
/**
15749
 * Set the default options for area
15750
 */
15751
defaultPlotOptions.area = merge(defaultSeriesOptions, {
15752
	threshold: 0
15753
	// trackByArea: false,
15754
	// lineColor: null, // overrides color, but lets fillColor be unaltered
15755
	// fillOpacity: 0.75,
15756
	// fillColor: null
15757
});
15758
 
15759
/**
15760
 * AreaSeries object
15761
 */
15762
var AreaSeries = extendClass(Series, {
15763
	type: 'area',
15764
	/**
15765
	 * For stacks, don't split segments on null values. Instead, draw null values with
15766
	 * no marker. Also insert dummy points for any X position that exists in other series
15767
	 * in the stack.
15768
	 */
15769
	getSegments: function () {
15770
		var series = this,
15771
			segments = [],
15772
			segment = [],
15773
			keys = [],
15774
			xAxis = this.xAxis,
15775
			yAxis = this.yAxis,
15776
			stack = yAxis.stacks[this.stackKey],
15777
			pointMap = {},
15778
			plotX,
15779
			plotY,
15780
			points = this.points,
15781
			connectNulls = this.options.connectNulls,
15782
			i,
15783
			x;
15784
 
15785
		if (this.options.stacking && !this.cropped) { // cropped causes artefacts in Stock, and perf issue
15786
			// Create a map where we can quickly look up the points by their X value.
15787
			for (i = 0; i < points.length; i++) {
15788
				pointMap[points[i].x] = points[i];
15789
			}
15790
 
15791
			// Sort the keys (#1651)
15792
			for (x in stack) {
15793
				if (stack[x].total !== null) { // nulled after switching between grouping and not (#1651, #2336)
15794
					keys.push(+x);
15795
				}
15796
			}
15797
			keys.sort(function (a, b) {
15798
				return a - b;
15799
			});
15800
 
15801
			each(keys, function (x) {
15802
				var y = 0,
15803
					stackPoint;
15804
 
15805
				if (connectNulls && (!pointMap[x] || pointMap[x].y === null)) { // #1836
15806
					return;
15807
 
15808
				// The point exists, push it to the segment
15809
				} else if (pointMap[x]) {
15810
					segment.push(pointMap[x]);
15811
 
15812
				// There is no point for this X value in this series, so we
15813
				// insert a dummy point in order for the areas to be drawn
15814
				// correctly.
15815
				} else {
15816
 
15817
					// Loop down the stack to find the series below this one that has
15818
					// a value (#1991)
15819
					for (i = series.index; i <= yAxis.series.length; i++) {
15820
						stackPoint = stack[x].points[i + ',' + x];
15821
						if (stackPoint) {
15822
							y = stackPoint[1];
15823
							break;
15824
						}
15825
					}
15826
 
15827
					plotX = xAxis.translate(x);
15828
					plotY = yAxis.toPixels(y, true);
15829
					segment.push({
15830
						y: null,
15831
						plotX: plotX,
15832
						clientX: plotX,
15833
						plotY: plotY,
15834
						yBottom: plotY,
15835
						onMouseOver: noop
15836
					});
15837
				}
15838
			});
15839
 
15840
			if (segment.length) {
15841
				segments.push(segment);
15842
			}
15843
 
15844
		} else {
15845
			Series.prototype.getSegments.call(this);
15846
			segments = this.segments;
15847
		}
15848
 
15849
		this.segments = segments;
15850
	},
15851
 
15852
	/**
15853
	 * Extend the base Series getSegmentPath method by adding the path for the area.
15854
	 * This path is pushed to the series.areaPath property.
15855
	 */
15856
	getSegmentPath: function (segment) {
15857
 
15858
		var segmentPath = Series.prototype.getSegmentPath.call(this, segment), // call base method
15859
			areaSegmentPath = [].concat(segmentPath), // work on a copy for the area path
15860
			i,
15861
			options = this.options,
15862
			segLength = segmentPath.length,
15863
			translatedThreshold = this.yAxis.getThreshold(options.threshold), // #2181
15864
			yBottom;
15865
 
15866
		if (segLength === 3) { // for animation from 1 to two points
15867
			areaSegmentPath.push(L, segmentPath[1], segmentPath[2]);
15868
		}
15869
		if (options.stacking && !this.closedStacks) {
15870
 
15871
			// Follow stack back. Todo: implement areaspline. A general solution could be to
15872
			// reverse the entire graphPath of the previous series, though may be hard with
15873
			// splines and with series with different extremes
15874
			for (i = segment.length - 1; i >= 0; i--) {
15875
 
15876
				yBottom = pick(segment[i].yBottom, translatedThreshold);
15877
 
15878
				// step line?
15879
				if (i < segment.length - 1 && options.step) {
15880
					areaSegmentPath.push(segment[i + 1].plotX, yBottom);
15881
				}
15882
 
15883
				areaSegmentPath.push(segment[i].plotX, yBottom);
15884
			}
15885
 
15886
		} else { // follow zero line back
15887
			this.closeSegment(areaSegmentPath, segment, translatedThreshold);
15888
		}
15889
		this.areaPath = this.areaPath.concat(areaSegmentPath);
15890
		return segmentPath;
15891
	},
15892
 
15893
	/**
15894
	 * Extendable method to close the segment path of an area. This is overridden in polar
15895
	 * charts.
15896
	 */
15897
	closeSegment: function (path, segment, translatedThreshold) {
15898
		path.push(
15899
			L,
15900
			segment[segment.length - 1].plotX,
15901
			translatedThreshold,
15902
			L,
15903
			segment[0].plotX,
15904
			translatedThreshold
15905
		);
15906
	},
15907
 
15908
	/**
15909
	 * Draw the graph and the underlying area. This method calls the Series base
15910
	 * function and adds the area. The areaPath is calculated in the getSegmentPath
15911
	 * method called from Series.prototype.drawGraph.
15912
	 */
15913
	drawGraph: function () {
15914
 
15915
		// Define or reset areaPath
15916
		this.areaPath = [];
15917
 
15918
		// Call the base method
15919
		Series.prototype.drawGraph.apply(this);
15920
 
15921
		// Define local variables
15922
		var series = this,
15923
			areaPath = this.areaPath,
15924
			options = this.options,
15925
			zones = this.zones,
15926
			props = [['area', this.color, options.fillColor]]; // area name, main color, fill color
15927
 
15928
		each(zones, function (threshold, i) {
15929
			props.push(['zoneArea' + i, threshold.color || series.color, threshold.fillColor || options.fillColor]);
15930
		});
15931
		each(props, function (prop) {
15932
			var areaKey = prop[0],
15933
				area = series[areaKey];
15934
 
15935
			// Create or update the area
15936
			if (area) { // update
15937
				area.animate({ d: areaPath });
15938
 
15939
			} else { // create
15940
				series[areaKey] = series.chart.renderer.path(areaPath)
15941
					.attr({
15942
						fill: pick(
15943
							prop[2],
15944
							Color(prop[1]).setOpacity(pick(options.fillOpacity, 0.75)).get()
15945
						),
15946
						zIndex: 0 // #1069
15947
					}).add(series.group);
15948
			}
15949
		});
15950
	},
15951
 
15952
	drawLegendSymbol: LegendSymbolMixin.drawRectangle
15953
});
15954
 
15955
seriesTypes.area = AreaSeries;
15956
/**
15957
 * Set the default options for spline
15958
 */
15959
defaultPlotOptions.spline = merge(defaultSeriesOptions);
15960
 
15961
/**
15962
 * SplineSeries object
15963
 */
15964
var SplineSeries = extendClass(Series, {
15965
	type: 'spline',
15966
 
15967
	/**
15968
	 * Get the spline segment from a given point's previous neighbour to the given point
15969
	 */
15970
	getPointSpline: function (segment, point, i) {
15971
		var smoothing = 1.5, // 1 means control points midway between points, 2 means 1/3 from the point, 3 is 1/4 etc
15972
			denom = smoothing + 1,
15973
			plotX = point.plotX,
15974
			plotY = point.plotY,
15975
			lastPoint = segment[i - 1],
15976
			nextPoint = segment[i + 1],
15977
			leftContX,
15978
			leftContY,
15979
			rightContX,
15980
			rightContY,
15981
			ret;
15982
 
15983
		// find control points
15984
		if (lastPoint && nextPoint) {
15985
 
15986
			var lastX = lastPoint.plotX,
15987
				lastY = lastPoint.plotY,
15988
				nextX = nextPoint.plotX,
15989
				nextY = nextPoint.plotY,
15990
				correction;
15991
 
15992
			leftContX = (smoothing * plotX + lastX) / denom;
15993
			leftContY = (smoothing * plotY + lastY) / denom;
15994
			rightContX = (smoothing * plotX + nextX) / denom;
15995
			rightContY = (smoothing * plotY + nextY) / denom;
15996
 
15997
			// have the two control points make a straight line through main point
15998
			correction = ((rightContY - leftContY) * (rightContX - plotX)) /
15999
				(rightContX - leftContX) + plotY - rightContY;
16000
 
16001
			leftContY += correction;
16002
			rightContY += correction;
16003
 
16004
			// to prevent false extremes, check that control points are between
16005
			// neighbouring points' y values
16006
			if (leftContY > lastY && leftContY > plotY) {
16007
				leftContY = mathMax(lastY, plotY);
16008
				rightContY = 2 * plotY - leftContY; // mirror of left control point
16009
			} else if (leftContY < lastY && leftContY < plotY) {
16010
				leftContY = mathMin(lastY, plotY);
16011
				rightContY = 2 * plotY - leftContY;
16012
			}
16013
			if (rightContY > nextY && rightContY > plotY) {
16014
				rightContY = mathMax(nextY, plotY);
16015
				leftContY = 2 * plotY - rightContY;
16016
			} else if (rightContY < nextY && rightContY < plotY) {
16017
				rightContY = mathMin(nextY, plotY);
16018
				leftContY = 2 * plotY - rightContY;
16019
			}
16020
 
16021
			// record for drawing in next point
16022
			point.rightContX = rightContX;
16023
			point.rightContY = rightContY;
16024
 
16025
		}
16026
 
16027
		// Visualize control points for debugging
16028
		/*
16029
		if (leftContX) {
16030
			this.chart.renderer.circle(leftContX + this.chart.plotLeft, leftContY + this.chart.plotTop, 2)
16031
				.attr({
16032
					stroke: 'red',
16033
					'stroke-width': 1,
16034
					fill: 'none'
16035
				})
16036
				.add();
16037
			this.chart.renderer.path(['M', leftContX + this.chart.plotLeft, leftContY + this.chart.plotTop,
16038
				'L', plotX + this.chart.plotLeft, plotY + this.chart.plotTop])
16039
				.attr({
16040
					stroke: 'red',
16041
					'stroke-width': 1
16042
				})
16043
				.add();
16044
			this.chart.renderer.circle(rightContX + this.chart.plotLeft, rightContY + this.chart.plotTop, 2)
16045
				.attr({
16046
					stroke: 'green',
16047
					'stroke-width': 1,
16048
					fill: 'none'
16049
				})
16050
				.add();
16051
			this.chart.renderer.path(['M', rightContX + this.chart.plotLeft, rightContY + this.chart.plotTop,
16052
				'L', plotX + this.chart.plotLeft, plotY + this.chart.plotTop])
16053
				.attr({
16054
					stroke: 'green',
16055
					'stroke-width': 1
16056
				})
16057
				.add();
16058
		}
16059
		*/
16060
 
16061
		// moveTo or lineTo
16062
		if (!i) {
16063
			ret = [M, plotX, plotY];
16064
		} else { // curve from last point to this
16065
			ret = [
16066
				'C',
16067
				lastPoint.rightContX || lastPoint.plotX,
16068
				lastPoint.rightContY || lastPoint.plotY,
16069
				leftContX || plotX,
16070
				leftContY || plotY,
16071
				plotX,
16072
				plotY
16073
			];
16074
			lastPoint.rightContX = lastPoint.rightContY = null; // reset for updating series later
16075
		}
16076
		return ret;
16077
	}
16078
});
16079
seriesTypes.spline = SplineSeries;
16080
 
16081
/**
16082
 * Set the default options for areaspline
16083
 */
16084
defaultPlotOptions.areaspline = merge(defaultPlotOptions.area);
16085
 
16086
/**
16087
 * AreaSplineSeries object
16088
 */
16089
var areaProto = AreaSeries.prototype,
16090
	AreaSplineSeries = extendClass(SplineSeries, {
16091
		type: 'areaspline',
16092
		closedStacks: true, // instead of following the previous graph back, follow the threshold back
16093
 
16094
		// Mix in methods from the area series
16095
		getSegmentPath: areaProto.getSegmentPath,
16096
		closeSegment: areaProto.closeSegment,
16097
		drawGraph: areaProto.drawGraph,
16098
		drawLegendSymbol: LegendSymbolMixin.drawRectangle
16099
	});
16100
 
16101
seriesTypes.areaspline = AreaSplineSeries;
16102
 
16103
/**
16104
 * Set the default options for column
16105
 */
16106
defaultPlotOptions.column = merge(defaultSeriesOptions, {
16107
	borderColor: '#FFFFFF',
16108
	//borderWidth: 1,
16109
	borderRadius: 0,
16110
	//colorByPoint: undefined,
16111
	groupPadding: 0.2,
16112
	//grouping: true,
16113
	marker: null, // point options are specified in the base options
16114
	pointPadding: 0.1,
16115
	//pointWidth: null,
16116
	minPointLength: 0,
16117
	cropThreshold: 50, // when there are more points, they will not animate out of the chart on xAxis.setExtremes
16118
	pointRange: null, // null means auto, meaning 1 in a categorized axis and least distance between points if not categories
16119
	states: {
16120
		hover: {
16121
			brightness: 0.1,
16122
			shadow: false,
16123
			halo: false
16124
		},
16125
		select: {
16126
			color: '#C0C0C0',
16127
			borderColor: '#000000',
16128
			shadow: false
16129
		}
16130
	},
16131
	dataLabels: {
16132
		align: null, // auto
16133
		verticalAlign: null, // auto
16134
		y: null
16135
	},
16136
	startFromThreshold: true, // docs: http://jsfiddle.net/highcharts/hz8fopan/14/
16137
	stickyTracking: false,
16138
	tooltip: {
16139
		distance: 6
16140
	},
16141
	threshold: 0
16142
});
16143
 
16144
/**
16145
 * ColumnSeries object
16146
 */
16147
var ColumnSeries = extendClass(Series, {
16148
	type: 'column',
16149
	pointAttrToOptions: { // mapping between SVG attributes and the corresponding options
16150
		stroke: 'borderColor',
16151
		fill: 'color',
16152
		r: 'borderRadius'
16153
	},
16154
	cropShoulder: 0,
16155
	directTouch: true, // When tooltip is not shared, this series (and derivatives) requires direct touch/hover. KD-tree does not apply.
16156
	trackerGroups: ['group', 'dataLabelsGroup'],
16157
	negStacks: true, // use separate negative stacks, unlike area stacks where a negative
16158
		// point is substracted from previous (#1910)
16159
 
16160
	/**
16161
	 * Initialize the series
16162
	 */
16163
	init: function () {
16164
		Series.prototype.init.apply(this, arguments);
16165
 
16166
		var series = this,
16167
			chart = series.chart;
16168
 
16169
		// if the series is added dynamically, force redraw of other
16170
		// series affected by a new column
16171
		if (chart.hasRendered) {
16172
			each(chart.series, function (otherSeries) {
16173
				if (otherSeries.type === series.type) {
16174
					otherSeries.isDirty = true;
16175
				}
16176
			});
16177
		}
16178
	},
16179
 
16180
	/**
16181
	 * Return the width and x offset of the columns adjusted for grouping, groupPadding, pointPadding,
16182
	 * pointWidth etc.
16183
	 */
16184
	getColumnMetrics: function () {
16185
 
16186
		var series = this,
16187
			options = series.options,
16188
			xAxis = series.xAxis,
16189
			yAxis = series.yAxis,
16190
			reversedXAxis = xAxis.reversed,
16191
			stackKey,
16192
			stackGroups = {},
16193
			columnIndex,
16194
			columnCount = 0;
16195
 
16196
		// Get the total number of column type series.
16197
		// This is called on every series. Consider moving this logic to a
16198
		// chart.orderStacks() function and call it on init, addSeries and removeSeries
16199
		if (options.grouping === false) {
16200
			columnCount = 1;
16201
		} else {
16202
			each(series.chart.series, function (otherSeries) {
16203
				var otherOptions = otherSeries.options,
16204
					otherYAxis = otherSeries.yAxis;
16205
				if (otherSeries.type === series.type && otherSeries.visible &&
16206
						yAxis.len === otherYAxis.len && yAxis.pos === otherYAxis.pos) {  // #642, #2086
16207
					if (otherOptions.stacking) {
16208
						stackKey = otherSeries.stackKey;
16209
						if (stackGroups[stackKey] === UNDEFINED) {
16210
							stackGroups[stackKey] = columnCount++;
16211
						}
16212
						columnIndex = stackGroups[stackKey];
16213
					} else if (otherOptions.grouping !== false) { // #1162
16214
						columnIndex = columnCount++;
16215
					}
16216
					otherSeries.columnIndex = columnIndex;
16217
				}
16218
			});
16219
		}
16220
 
16221
		var categoryWidth = mathMin(
16222
				mathAbs(xAxis.transA) * (xAxis.ordinalSlope || options.pointRange || xAxis.closestPointRange || xAxis.tickInterval || 1), // #2610
16223
				xAxis.len // #1535
16224
			),
16225
			groupPadding = categoryWidth * options.groupPadding,
16226
			groupWidth = categoryWidth - 2 * groupPadding,
16227
			pointOffsetWidth = groupWidth / columnCount,
16228
			pointWidth = mathMin(
16229
				options.maxPointWidth || xAxis.len,
16230
				pick(options.pointWidth, pointOffsetWidth * (1 - 2 * options.pointPadding))
16231
			),
16232
			pointPadding = (pointOffsetWidth - pointWidth) / 2,
16233
			colIndex = (reversedXAxis ?
16234
				columnCount - (series.columnIndex || 0) : // #1251
16235
				series.columnIndex) || 0,
16236
			pointXOffset = pointPadding + (groupPadding + colIndex *
16237
				pointOffsetWidth - (categoryWidth / 2)) *
16238
				(reversedXAxis ? -1 : 1);
16239
 
16240
		// Save it for reading in linked series (Error bars particularly)
16241
		return (series.columnMetrics = {
16242
			width: pointWidth,
16243
			offset: pointXOffset
16244
		});
16245
 
16246
	},
16247
 
16248
	/**
16249
	 * Translate each point to the plot area coordinate system and find shape positions
16250
	 */
16251
	translate: function () {
16252
		var series = this,
16253
			chart = series.chart,
16254
			options = series.options,
16255
			borderWidth = series.borderWidth = pick(
16256
				options.borderWidth,
16257
				series.closestPointRange * series.xAxis.transA < 2 ? 0 : 1 // #3635
16258
			),
16259
			yAxis = series.yAxis,
16260
			threshold = options.threshold,
16261
			translatedThreshold = series.translatedThreshold = yAxis.getThreshold(threshold),
16262
			minPointLength = pick(options.minPointLength, 5),
16263
			metrics = series.getColumnMetrics(),
16264
			pointWidth = metrics.width,
16265
			seriesBarW = series.barW = mathMax(pointWidth, 1 + 2 * borderWidth), // postprocessed for border width
16266
			pointXOffset = series.pointXOffset = metrics.offset,
16267
			xCrisp = -(borderWidth % 2 ? 0.5 : 0),
16268
			yCrisp = borderWidth % 2 ? 0.5 : 1;
16269
 
16270
		if (chart.inverted) {
16271
			translatedThreshold -= 0.5; // #3355
16272
			if (chart.renderer.isVML) {
16273
				yCrisp += 1;
16274
			}
16275
		}
16276
 
16277
		// When the pointPadding is 0, we want the columns to be packed tightly, so we allow individual
16278
		// columns to have individual sizes. When pointPadding is greater, we strive for equal-width
16279
		// columns (#2694).
16280
		if (options.pointPadding) {
16281
			seriesBarW = mathCeil(seriesBarW);
16282
		}
16283
 
16284
		Series.prototype.translate.apply(series);
16285
 
16286
		// Record the new values
16287
		each(series.points, function (point) {
16288
			var yBottom = pick(point.yBottom, translatedThreshold),
16289
				safeDistance = 999 + mathAbs(yBottom),
16290
				plotY = mathMin(mathMax(-safeDistance, point.plotY), yAxis.len + safeDistance), // Don't draw too far outside plot area (#1303, #2241, #4264)
16291
				barX = point.plotX + pointXOffset,
16292
				barW = seriesBarW,
16293
				barY = mathMin(plotY, yBottom),
16294
				right,
16295
				bottom,
16296
				fromTop,
16297
				up,
16298
				barH = mathMax(plotY, yBottom) - barY;
16299
 
16300
			// Handle options.minPointLength
16301
			if (mathAbs(barH) < minPointLength) {
16302
				if (minPointLength) {
16303
					barH = minPointLength;
16304
					up = (!yAxis.reversed && !point.negative) || (yAxis.reversed && point.negative);
16305
					barY =
16306
						mathRound(mathAbs(barY - translatedThreshold) > minPointLength ? // stacked
16307
							yBottom - minPointLength : // keep position
16308
							translatedThreshold - (up ? minPointLength : 0)); // #1485, #4051
16309
				}
16310
			}
16311
 
16312
			// Cache for access in polar
16313
			point.barX = barX;
16314
			point.pointWidth = pointWidth;
16315
 
16316
			// Round off to obtain crisp edges and avoid overlapping with neighbours (#2694)
16317
			right = mathRound(barX + barW) + xCrisp;
16318
			barX = mathRound(barX) + xCrisp;
16319
			barW = right - barX;
16320
 
16321
			fromTop = mathAbs(barY) < 0.5;
16322
			bottom = mathMin(mathRound(barY + barH) + yCrisp, 9e4); // #3575
16323
			barY = mathRound(barY) + yCrisp;
16324
			barH = bottom - barY;
16325
 
16326
			// Top edges are exceptions
16327
			if (fromTop) {
16328
				barY -= 1;
16329
				barH += 1;
16330
			}
16331
 
16332
			// Fix the tooltip on center of grouped columns (#1216, #424, #3648)
16333
			point.tooltipPos = chart.inverted ?
16334
				[yAxis.len + yAxis.pos - chart.plotLeft - plotY, series.xAxis.len - barX - barW / 2, barH] :
16335
				[barX + barW / 2, plotY + yAxis.pos - chart.plotTop, barH];
16336
 
16337
			// Register shape type and arguments to be used in drawPoints
16338
			point.shapeType = 'rect';
16339
			point.shapeArgs = {
16340
				x: barX,
16341
				y: barY,
16342
				width: barW,
16343
				height: barH
16344
			};
16345
		});
16346
 
16347
	},
16348
 
16349
	getSymbol: noop,
16350
 
16351
	/**
16352
	 * Use a solid rectangle like the area series types
16353
	 */
16354
	drawLegendSymbol: LegendSymbolMixin.drawRectangle,
16355
 
16356
 
16357
	/**
16358
	 * Columns have no graph
16359
	 */
16360
	drawGraph: noop,
16361
 
16362
	/**
16363
	 * Draw the columns. For bars, the series.group is rotated, so the same coordinates
16364
	 * apply for columns and bars. This method is inherited by scatter series.
16365
	 *
16366
	 */
16367
	drawPoints: function () {
16368
		var series = this,
16369
			chart = this.chart,
16370
			options = series.options,
16371
			renderer = chart.renderer,
16372
			animationLimit = options.animationLimit || 250,
16373
			shapeArgs,
16374
			pointAttr;
16375
 
16376
		// draw the columns
16377
		each(series.points, function (point) {
16378
			var plotY = point.plotY,
16379
				graphic = point.graphic,
16380
				borderAttr;
16381
 
16382
			if (plotY !== UNDEFINED && !isNaN(plotY) && point.y !== null) {
16383
				shapeArgs = point.shapeArgs;
16384
 
16385
				borderAttr = defined(series.borderWidth) ? {
16386
					'stroke-width': series.borderWidth
16387
				} : {};
16388
 
16389
				pointAttr = point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE] || series.pointAttr[NORMAL_STATE];
16390
 
16391
				if (graphic) { // update
16392
					stop(graphic);
16393
					graphic.attr(borderAttr)[chart.pointCount < animationLimit ? 'animate' : 'attr'](merge(shapeArgs));
16394
 
16395
				} else {
16396
					point.graphic = graphic = renderer[point.shapeType](shapeArgs)
16397
						.attr(borderAttr)
16398
						.attr(pointAttr)
16399
						.add(series.group)
16400
						.shadow(options.shadow, null, options.stacking && !options.borderRadius);
16401
				}
16402
 
16403
			} else if (graphic) {
16404
				point.graphic = graphic.destroy(); // #1269
16405
			}
16406
		});
16407
	},
16408
 
16409
	/**
16410
	 * Animate the column heights one by one from zero
16411
	 * @param {Boolean} init Whether to initialize the animation or run it
16412
	 */
16413
	animate: function (init) {
16414
		var series = this,
16415
			yAxis = this.yAxis,
16416
			options = series.options,
16417
			inverted = this.chart.inverted,
16418
			attr = {},
16419
			translatedThreshold;
16420
 
16421
		if (hasSVG) { // VML is too slow anyway
16422
			if (init) {
16423
				attr.scaleY = 0.001;
16424
				translatedThreshold = mathMin(yAxis.pos + yAxis.len, mathMax(yAxis.pos, yAxis.toPixels(options.threshold)));
16425
				if (inverted) {
16426
					attr.translateX = translatedThreshold - yAxis.len;
16427
				} else {
16428
					attr.translateY = translatedThreshold;
16429
				}
16430
				series.group.attr(attr);
16431
 
16432
			} else { // run the animation
16433
 
16434
				attr.scaleY = 1;
16435
				attr[inverted ? 'translateX' : 'translateY'] = yAxis.pos;
16436
				series.group.animate(attr, series.options.animation);
16437
 
16438
				// delete this function to allow it only once
16439
				series.animate = null;
16440
			}
16441
		}
16442
	},
16443
 
16444
	/**
16445
	 * Remove this series from the chart
16446
	 */
16447
	remove: function () {
16448
		var series = this,
16449
			chart = series.chart;
16450
 
16451
		// column and bar series affects other series of the same type
16452
		// as they are either stacked or grouped
16453
		if (chart.hasRendered) {
16454
			each(chart.series, function (otherSeries) {
16455
				if (otherSeries.type === series.type) {
16456
					otherSeries.isDirty = true;
16457
				}
16458
			});
16459
		}
16460
 
16461
		Series.prototype.remove.apply(series, arguments);
16462
	}
16463
});
16464
seriesTypes.column = ColumnSeries;
16465
/**
16466
 * Set the default options for bar
16467
 */
16468
defaultPlotOptions.bar = merge(defaultPlotOptions.column);
16469
/**
16470
 * The Bar series class
16471
 */
16472
var BarSeries = extendClass(ColumnSeries, {
16473
	type: 'bar',
16474
	inverted: true
16475
});
16476
seriesTypes.bar = BarSeries;
16477
 
16478
/**
16479
 * Set the default options for scatter
16480
 */
16481
defaultPlotOptions.scatter = merge(defaultSeriesOptions, {
16482
	lineWidth: 0,
16483
	marker: {
16484
		enabled: true // Overrides auto-enabling in line series (#3647)
16485
	},
16486
	tooltip: {
16487
		headerFormat: '<span style="color:{point.color}">\u25CF</span> <span style="font-size: 10px;"> {series.name}</span><br/>',
16488
		pointFormat: 'x: <b>{point.x}</b><br/>y: <b>{point.y}</b><br/>'
16489
	}
16490
});
16491
 
16492
/**
16493
 * The scatter series class
16494
 */
16495
var ScatterSeries = extendClass(Series, {
16496
	type: 'scatter',
16497
	sorted: false,
16498
	requireSorting: false,
16499
	noSharedTooltip: true,
16500
	trackerGroups: ['group', 'markerGroup', 'dataLabelsGroup'],
16501
	takeOrdinalPosition: false, // #2342
16502
	kdDimensions: 2,
16503
	drawGraph: function () {
16504
		if (this.options.lineWidth) {
16505
			Series.prototype.drawGraph.call(this);
16506
		}
16507
	}
16508
});
16509
 
16510
seriesTypes.scatter = ScatterSeries;
16511
 
16512
/**
16513
 * Set the default options for pie
16514
 */
16515
defaultPlotOptions.pie = merge(defaultSeriesOptions, {
16516
	borderColor: '#FFFFFF',
16517
	borderWidth: 1,
16518
	center: [null, null],
16519
	clip: false,
16520
	colorByPoint: true, // always true for pies
16521
	dataLabels: {
16522
		// align: null,
16523
		// connectorWidth: 1,
16524
		// connectorColor: point.color,
16525
		// connectorPadding: 5,
16526
		distance: 30,
16527
		enabled: true,
16528
		formatter: function () { // #2945
16529
			return this.y === null ? undefined : this.point.name;
16530
		},
16531
		// softConnector: true,
16532
		x: 0
16533
		// y: 0
16534
	},
16535
	ignoreHiddenPoint: true,
16536
	//innerSize: 0,
16537
	legendType: 'point',
16538
	marker: null, // point options are specified in the base options
16539
	size: null,
16540
	showInLegend: false,
16541
	slicedOffset: 10,
16542
	states: {
16543
		hover: {
16544
			brightness: 0.1,
16545
			shadow: false
16546
		}
16547
	},
16548
	stickyTracking: false,
16549
	tooltip: {
16550
		followPointer: true
16551
	}
16552
});
16553
 
16554
/**
16555
 * Extended point object for pies
16556
 */
16557
var PiePoint = extendClass(Point, {
16558
	/**
16559
	 * Initiate the pie slice
16560
	 */
16561
	init: function () {
16562
 
16563
		Point.prototype.init.apply(this, arguments);
16564
 
16565
		var point = this,
16566
			toggleSlice;
16567
 
16568
		extend(point, {
16569
			visible: point.visible !== false,
16570
			name: pick(point.name, 'Slice')
16571
		});
16572
 
16573
		// add event listener for select
16574
		toggleSlice = function (e) {
16575
			point.slice(e.type === 'select');
16576
		};
16577
		addEvent(point, 'select', toggleSlice);
16578
		addEvent(point, 'unselect', toggleSlice);
16579
 
16580
		return point;
16581
	},
16582
 
16583
	/**
16584
	 * Toggle the visibility of the pie slice
16585
	 * @param {Boolean} vis Whether to show the slice or not. If undefined, the
16586
	 *    visibility is toggled
16587
	 */
16588
	setVisible: function (vis, redraw) {
16589
		var point = this,
16590
			series = point.series,
16591
			chart = series.chart,
16592
			ignoreHiddenPoint = series.options.ignoreHiddenPoint;
16593
 
16594
		redraw = pick(redraw, ignoreHiddenPoint);
16595
 
16596
		if (vis !== point.visible) {
16597
 
16598
			// If called without an argument, toggle visibility
16599
			point.visible = point.options.visible = vis = vis === UNDEFINED ? !point.visible : vis;
16600
			series.options.data[inArray(point, series.data)] = point.options; // update userOptions.data
16601
 
16602
			// Show and hide associated elements. This is performed regardless of redraw or not,
16603
			// because chart.redraw only handles full series.
16604
			each(['graphic', 'dataLabel', 'connector', 'shadowGroup'], function (key) {
16605
				if (point[key]) {
16606
					point[key][vis ? 'show' : 'hide'](true);
16607
				}
16608
			});
16609
 
16610
			if (point.legendItem) {
16611
				chart.legend.colorizeItem(point, vis);
16612
			}
16613
 
16614
			// #4170, hide halo after hiding point
16615
			if (!vis && point.state === 'hover') {
16616
				point.setState('');
16617
			}
16618
 
16619
			// Handle ignore hidden slices
16620
			if (ignoreHiddenPoint) {
16621
				series.isDirty = true;
16622
			}
16623
 
16624
			if (redraw) {
16625
				chart.redraw();
16626
			}
16627
		}
16628
	},
16629
 
16630
	/**
16631
	 * Set or toggle whether the slice is cut out from the pie
16632
	 * @param {Boolean} sliced When undefined, the slice state is toggled
16633
	 * @param {Boolean} redraw Whether to redraw the chart. True by default.
16634
	 */
16635
	slice: function (sliced, redraw, animation) {
16636
		var point = this,
16637
			series = point.series,
16638
			chart = series.chart,
16639
			translation;
16640
 
16641
		setAnimation(animation, chart);
16642
 
16643
		// redraw is true by default
16644
		redraw = pick(redraw, true);
16645
 
16646
		// if called without an argument, toggle
16647
		point.sliced = point.options.sliced = sliced = defined(sliced) ? sliced : !point.sliced;
16648
		series.options.data[inArray(point, series.data)] = point.options; // update userOptions.data
16649
 
16650
		translation = sliced ? point.slicedTranslation : {
16651
			translateX: 0,
16652
			translateY: 0
16653
		};
16654
 
16655
		point.graphic.animate(translation);
16656
 
16657
		if (point.shadowGroup) {
16658
			point.shadowGroup.animate(translation);
16659
		}
16660
 
16661
	},
16662
 
16663
	haloPath: function (size) {
16664
		var shapeArgs = this.shapeArgs,
16665
			chart = this.series.chart;
16666
 
16667
		return this.sliced || !this.visible ? [] : this.series.chart.renderer.symbols.arc(chart.plotLeft + shapeArgs.x, chart.plotTop + shapeArgs.y, shapeArgs.r + size, shapeArgs.r + size, {
16668
			innerR: this.shapeArgs.r,
16669
			start: shapeArgs.start,
16670
			end: shapeArgs.end
16671
		});
16672
	}
16673
});
16674
 
16675
/**
16676
 * The Pie series class
16677
 */
16678
var PieSeries = {
16679
	type: 'pie',
16680
	isCartesian: false,
16681
	pointClass: PiePoint,
16682
	requireSorting: false,
16683
	directTouch: true,
16684
	noSharedTooltip: true,
16685
	trackerGroups: ['group', 'dataLabelsGroup'],
16686
	axisTypes: [],
16687
	pointAttrToOptions: { // mapping between SVG attributes and the corresponding options
16688
		stroke: 'borderColor',
16689
		'stroke-width': 'borderWidth',
16690
		fill: 'color'
16691
	},
16692
 
16693
	/**
16694
	 * Animate the pies in
16695
	 */
16696
	animate: function (init) {
16697
		var series = this,
16698
			points = series.points,
16699
			startAngleRad = series.startAngleRad;
16700
 
16701
		if (!init) {
16702
			each(points, function (point) {
16703
				var graphic = point.graphic,
16704
					args = point.shapeArgs;
16705
 
16706
				if (graphic) {
16707
					// start values
16708
					graphic.attr({
16709
						r: point.startR || (series.center[3] / 2), // animate from inner radius (#779)
16710
						start: startAngleRad,
16711
						end: startAngleRad
16712
					});
16713
 
16714
					// animate
16715
					graphic.animate({
16716
						r: args.r,
16717
						start: args.start,
16718
						end: args.end
16719
					}, series.options.animation);
16720
				}
16721
			});
16722
 
16723
			// delete this function to allow it only once
16724
			series.animate = null;
16725
		}
16726
	},
16727
 
16728
	/**
16729
	 * Extend the basic setData method by running processData and generatePoints immediately,
16730
	 * in order to access the points from the legend.
16731
	 */
16732
	setData: function (data, redraw, animation, updatePoints) {
16733
		Series.prototype.setData.call(this, data, false, animation, updatePoints);
16734
		this.processData();
16735
		this.generatePoints();
16736
		if (pick(redraw, true)) {
16737
			this.chart.redraw(animation);
16738
		}
16739
	},
16740
 
16741
	/**
16742
	 * Recompute total chart sum and update percentages of points.
16743
	 */
16744
	updateTotals: function () {
16745
		var i,
16746
			total = 0,
16747
			points = this.points,
16748
			len = points.length,
16749
			point,
16750
			ignoreHiddenPoint = this.options.ignoreHiddenPoint;
16751
 
16752
		// Get the total sum
16753
		for (i = 0; i < len; i++) {
16754
			point = points[i];
16755
			total += (ignoreHiddenPoint && !point.visible) ? 0 : point.y;
16756
		}
16757
		this.total = total;
16758
 
16759
		// Set each point's properties
16760
		for (i = 0; i < len; i++) {
16761
			point = points[i];
16762
			point.percentage = (total > 0 && (point.visible || !ignoreHiddenPoint)) ? point.y / total * 100 : 0;
16763
			point.total = total;
16764
		}
16765
	},
16766
 
16767
	/**
16768
	 * Extend the generatePoints method by adding total and percentage properties to each point
16769
	 */
16770
	generatePoints: function () {
16771
		Series.prototype.generatePoints.call(this);
16772
		this.updateTotals();
16773
	},
16774
 
16775
	/**
16776
	 * Do translation for pie slices
16777
	 */
16778
	translate: function (positions) {
16779
		this.generatePoints();
16780
 
16781
		var series = this,
16782
			cumulative = 0,
16783
			precision = 1000, // issue #172
16784
			options = series.options,
16785
			slicedOffset = options.slicedOffset,
16786
			connectorOffset = slicedOffset + options.borderWidth,
16787
			start,
16788
			end,
16789
			angle,
16790
			startAngle = options.startAngle || 0,
16791
			startAngleRad = series.startAngleRad = mathPI / 180 * (startAngle - 90),
16792
			endAngleRad = series.endAngleRad = mathPI / 180 * ((pick(options.endAngle, startAngle + 360)) - 90),
16793
			circ = endAngleRad - startAngleRad, //2 * mathPI,
16794
			points = series.points,
16795
			radiusX, // the x component of the radius vector for a given point
16796
			radiusY,
16797
			labelDistance = options.dataLabels.distance,
16798
			ignoreHiddenPoint = options.ignoreHiddenPoint,
16799
			i,
16800
			len = points.length,
16801
			point;
16802
 
16803
		// Get positions - either an integer or a percentage string must be given.
16804
		// If positions are passed as a parameter, we're in a recursive loop for adjusting
16805
		// space for data labels.
16806
		if (!positions) {
16807
			series.center = positions = series.getCenter();
16808
		}
16809
 
16810
		// utility for getting the x value from a given y, used for anticollision logic in data labels
16811
		series.getX = function (y, left) {
16812
 
16813
			angle = math.asin(mathMin((y - positions[1]) / (positions[2] / 2 + labelDistance), 1));
16814
 
16815
			return positions[0] +
16816
				(left ? -1 : 1) *
16817
				(mathCos(angle) * (positions[2] / 2 + labelDistance));
16818
		};
16819
 
16820
		// Calculate the geometry for each point
16821
		for (i = 0; i < len; i++) {
16822
 
16823
			point = points[i];
16824
 
16825
			// set start and end angle
16826
			start = startAngleRad + (cumulative * circ);
16827
			if (!ignoreHiddenPoint || point.visible) {
16828
				cumulative += point.percentage / 100;
16829
			}
16830
			end = startAngleRad + (cumulative * circ);
16831
 
16832
			// set the shape
16833
			point.shapeType = 'arc';
16834
			point.shapeArgs = {
16835
				x: positions[0],
16836
				y: positions[1],
16837
				r: positions[2] / 2,
16838
				innerR: positions[3] / 2,
16839
				start: mathRound(start * precision) / precision,
16840
				end: mathRound(end * precision) / precision
16841
			};
16842
 
16843
			// The angle must stay within -90 and 270 (#2645)
16844
			angle = (end + start) / 2;
16845
			if (angle > 1.5 * mathPI) {
16846
				angle -= 2 * mathPI;
16847
			} else if (angle < -mathPI / 2) {
16848
				angle += 2 * mathPI;
16849
			}
16850
 
16851
			// Center for the sliced out slice
16852
			point.slicedTranslation = {
16853
				translateX: mathRound(mathCos(angle) * slicedOffset),
16854
				translateY: mathRound(mathSin(angle) * slicedOffset)
16855
			};
16856
 
16857
			// set the anchor point for tooltips
16858
			radiusX = mathCos(angle) * positions[2] / 2;
16859
			radiusY = mathSin(angle) * positions[2] / 2;
16860
			point.tooltipPos = [
16861
				positions[0] + radiusX * 0.7,
16862
				positions[1] + radiusY * 0.7
16863
			];
16864
 
16865
			point.half = angle < -mathPI / 2 || angle > mathPI / 2 ? 1 : 0;
16866
			point.angle = angle;
16867
 
16868
			// set the anchor point for data labels
16869
			connectorOffset = mathMin(connectorOffset, labelDistance / 2); // #1678
16870
			point.labelPos = [
16871
				positions[0] + radiusX + mathCos(angle) * labelDistance, // first break of connector
16872
				positions[1] + radiusY + mathSin(angle) * labelDistance, // a/a
16873
				positions[0] + radiusX + mathCos(angle) * connectorOffset, // second break, right outside pie
16874
				positions[1] + radiusY + mathSin(angle) * connectorOffset, // a/a
16875
				positions[0] + radiusX, // landing point for connector
16876
				positions[1] + radiusY, // a/a
16877
				labelDistance < 0 ? // alignment
16878
					'center' :
16879
					point.half ? 'right' : 'left', // alignment
16880
				angle // center angle
16881
			];
16882
 
16883
		}
16884
	},
16885
 
16886
	drawGraph: null,
16887
 
16888
	/**
16889
	 * Draw the data points
16890
	 */
16891
	drawPoints: function () {
16892
		var series = this,
16893
			chart = series.chart,
16894
			renderer = chart.renderer,
16895
			groupTranslation,
16896
			//center,
16897
			graphic,
16898
			//group,
16899
			shadow = series.options.shadow,
16900
			shadowGroup,
16901
			shapeArgs,
16902
			attr;
16903
 
16904
		if (shadow && !series.shadowGroup) {
16905
			series.shadowGroup = renderer.g('shadow')
16906
				.add(series.group);
16907
		}
16908
 
16909
		// draw the slices
16910
		each(series.points, function (point) {
16911
			if (point.y !== null) {
16912
				graphic = point.graphic;
16913
				shapeArgs = point.shapeArgs;
16914
				shadowGroup = point.shadowGroup;
16915
 
16916
				// put the shadow behind all points
16917
				if (shadow && !shadowGroup) {
16918
					shadowGroup = point.shadowGroup = renderer.g('shadow')
16919
						.add(series.shadowGroup);
16920
				}
16921
 
16922
				// if the point is sliced, use special translation, else use plot area traslation
16923
				groupTranslation = point.sliced ? point.slicedTranslation : {
16924
					translateX: 0,
16925
					translateY: 0
16926
				};
16927
 
16928
				//group.translate(groupTranslation[0], groupTranslation[1]);
16929
				if (shadowGroup) {
16930
					shadowGroup.attr(groupTranslation);
16931
				}
16932
 
16933
				// draw the slice
16934
				if (graphic) {
16935
					graphic.animate(extend(shapeArgs, groupTranslation));
16936
				} else {
16937
					attr = { 'stroke-linejoin': 'round' };
16938
					if (!point.visible) {
16939
						attr.visibility = 'hidden';
16940
					}
16941
 
16942
					point.graphic = graphic = renderer[point.shapeType](shapeArgs)
16943
						.setRadialReference(series.center)
16944
						.attr(
16945
							point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE]
16946
						)
16947
						.attr(attr)
16948
						.attr(groupTranslation)
16949
						.add(series.group)
16950
						.shadow(shadow, shadowGroup);
16951
				}
16952
			}
16953
		});
16954
 
16955
	},
16956
 
16957
 
16958
	searchPoint: noop,
16959
 
16960
	/**
16961
	 * Utility for sorting data labels
16962
	 */
16963
	sortByAngle: function (points, sign) {
16964
		points.sort(function (a, b) {
16965
			return a.angle !== undefined && (b.angle - a.angle) * sign;
16966
		});
16967
	},
16968
 
16969
	/**
16970
	 * Use a simple symbol from LegendSymbolMixin
16971
	 */
16972
	drawLegendSymbol: LegendSymbolMixin.drawRectangle,
16973
 
16974
	/**
16975
	 * Use the getCenter method from drawLegendSymbol
16976
	 */
16977
	getCenter: CenteredSeriesMixin.getCenter,
16978
 
16979
	/**
16980
	 * Pies don't have point marker symbols
16981
	 */
16982
	getSymbol: noop
16983
 
16984
};
16985
PieSeries = extendClass(Series, PieSeries);
16986
seriesTypes.pie = PieSeries;
16987
 
16988
/**
16989
 * Draw the data labels
16990
 */
16991
Series.prototype.drawDataLabels = function () {
16992
 
16993
	var series = this,
16994
		seriesOptions = series.options,
16995
		cursor = seriesOptions.cursor,
16996
		options = seriesOptions.dataLabels,
16997
		points = series.points,
16998
		pointOptions,
16999
		generalOptions,
17000
		hasRendered = series.hasRendered || 0,
17001
		str,
17002
		dataLabelsGroup,
17003
		renderer = series.chart.renderer;
17004
 
17005
	if (options.enabled || series._hasPointLabels) {
17006
 
17007
		// Process default alignment of data labels for columns
17008
		if (series.dlProcessOptions) {
17009
			series.dlProcessOptions(options);
17010
		}
17011
 
17012
		// Create a separate group for the data labels to avoid rotation
17013
		dataLabelsGroup = series.plotGroup(
17014
			'dataLabelsGroup',
17015
			'data-labels',
17016
			options.defer ? HIDDEN : VISIBLE,
17017
			options.zIndex || 6
17018
		);
17019
 
17020
		if (pick(options.defer, true)) {
17021
			dataLabelsGroup.attr({ opacity: +hasRendered }); // #3300
17022
			if (!hasRendered) {
17023
				addEvent(series, 'afterAnimate', function () {
17024
					if (series.visible) { // #3023, #3024
17025
						dataLabelsGroup.show();
17026
					}
17027
					dataLabelsGroup[seriesOptions.animation ? 'animate' : 'attr']({ opacity: 1 }, { duration: 200 });
17028
				});
17029
			}
17030
		}
17031
 
17032
		// Make the labels for each point
17033
		generalOptions = options;
17034
		each(points, function (point) {
17035
 
17036
			var enabled,
17037
				dataLabel = point.dataLabel,
17038
				labelConfig,
17039
				attr,
17040
				name,
17041
				rotation,
17042
				connector = point.connector,
17043
				isNew = true,
17044
				style,
17045
				moreStyle = {};
17046
 
17047
			// Determine if each data label is enabled
17048
			pointOptions = point.dlOptions || (point.options && point.options.dataLabels); // dlOptions is used in treemaps
17049
			enabled = pick(pointOptions && pointOptions.enabled, generalOptions.enabled); // #2282
17050
 
17051
 
17052
			// If the point is outside the plot area, destroy it. #678, #820
17053
			if (dataLabel && !enabled) {
17054
				point.dataLabel = dataLabel.destroy();
17055
 
17056
			// Individual labels are disabled if the are explicitly disabled
17057
			// in the point options, or if they fall outside the plot area.
17058
			} else if (enabled) {
17059
 
17060
				// Create individual options structure that can be extended without
17061
				// affecting others
17062
				options = merge(generalOptions, pointOptions);
17063
				style = options.style;
17064
 
17065
				rotation = options.rotation;
17066
 
17067
				// Get the string
17068
				labelConfig = point.getLabelConfig();
17069
				str = options.format ?
17070
					format(options.format, labelConfig) :
17071
					options.formatter.call(labelConfig, options);
17072
 
17073
				// Determine the color
17074
				style.color = pick(options.color, style.color, series.color, 'black');
17075
 
17076
 
17077
				// update existing label
17078
				if (dataLabel) {
17079
 
17080
					if (defined(str)) {
17081
						dataLabel
17082
							.attr({
17083
								text: str
17084
							});
17085
						isNew = false;
17086
 
17087
					} else { // #1437 - the label is shown conditionally
17088
						point.dataLabel = dataLabel = dataLabel.destroy();
17089
						if (connector) {
17090
							point.connector = connector.destroy();
17091
						}
17092
					}
17093
 
17094
				// create new label
17095
				} else if (defined(str)) {
17096
					attr = {
17097
						//align: align,
17098
						fill: options.backgroundColor,
17099
						stroke: options.borderColor,
17100
						'stroke-width': options.borderWidth,
17101
						r: options.borderRadius || 0,
17102
						rotation: rotation,
17103
						padding: options.padding,
17104
						zIndex: 1
17105
					};
17106
 
17107
					// Get automated contrast color
17108
					if (style.color === 'contrast') {
17109
						moreStyle.color = options.inside || options.distance < 0 || !!seriesOptions.stacking ?
17110
							renderer.getContrast(point.color || series.color) :
17111
							'#000000';
17112
					}
17113
					if (cursor) {
17114
						moreStyle.cursor = cursor;
17115
					}
17116
 
17117
 
17118
					// Remove unused attributes (#947)
17119
					for (name in attr) {
17120
						if (attr[name] === UNDEFINED) {
17121
							delete attr[name];
17122
						}
17123
					}
17124
 
17125
					dataLabel = point.dataLabel = renderer[rotation ? 'text' : 'label']( // labels don't support rotation
17126
						str,
17127
						0,
17128
						-999,
17129
						options.shape,
17130
						null,
17131
						null,
17132
						options.useHTML
17133
					)
17134
					.attr(attr)
17135
					.css(extend(style, moreStyle))
17136
					.add(dataLabelsGroup)
17137
					.shadow(options.shadow);
17138
 
17139
				}
17140
 
17141
				if (dataLabel) {
17142
					// Now the data label is created and placed at 0,0, so we need to align it
17143
					series.alignDataLabel(point, dataLabel, options, null, isNew);
17144
				}
17145
			}
17146
		});
17147
	}
17148
};
17149
 
17150
/**
17151
 * Align each individual data label
17152
 */
17153
Series.prototype.alignDataLabel = function (point, dataLabel, options, alignTo, isNew) {
17154
	var chart = this.chart,
17155
		inverted = chart.inverted,
17156
		plotX = pick(point.plotX, -999),
17157
		plotY = pick(point.plotY, -999),
17158
		bBox = dataLabel.getBBox(),
17159
		baseline = chart.renderer.fontMetrics(options.style.fontSize).b,
17160
		rotCorr, // rotation correction
17161
		// Math.round for rounding errors (#2683), alignTo to allow column labels (#2700)
17162
		visible = this.visible && (point.series.forceDL || chart.isInsidePlot(plotX, mathRound(plotY), inverted) ||
17163
			(alignTo && chart.isInsidePlot(plotX, inverted ? alignTo.x + 1 : alignTo.y + alignTo.height - 1, inverted))),
17164
		alignAttr; // the final position;
17165
 
17166
	if (visible) {
17167
 
17168
		// The alignment box is a singular point
17169
		alignTo = extend({
17170
			x: inverted ? chart.plotWidth - plotY : plotX,
17171
			y: mathRound(inverted ? chart.plotHeight - plotX : plotY),
17172
			width: 0,
17173
			height: 0
17174
		}, alignTo);
17175
 
17176
		// Add the text size for alignment calculation
17177
		extend(options, {
17178
			width: bBox.width,
17179
			height: bBox.height
17180
		});
17181
 
17182
		// Allow a hook for changing alignment in the last moment, then do the alignment
17183
		if (options.rotation) { // Fancy box alignment isn't supported for rotated text
17184
			rotCorr = chart.renderer.rotCorr(baseline, options.rotation); // #3723
17185
			dataLabel[isNew ? 'attr' : 'animate']({
17186
					x: alignTo.x + options.x + alignTo.width / 2 + rotCorr.x,
17187
					y: alignTo.y + options.y + alignTo.height / 2
17188
				})
17189
				.attr({ // #3003
17190
					align: options.align
17191
				});
17192
		} else {
17193
			dataLabel.align(options, null, alignTo);
17194
			alignAttr = dataLabel.alignAttr;
17195
 
17196
			// Handle justify or crop
17197
			if (pick(options.overflow, 'justify') === 'justify') {
17198
				this.justifyDataLabel(dataLabel, options, alignAttr, bBox, alignTo, isNew);
17199
 
17200
			} else if (pick(options.crop, true)) {
17201
				// Now check that the data label is within the plot area
17202
				visible = chart.isInsidePlot(alignAttr.x, alignAttr.y) && chart.isInsidePlot(alignAttr.x + bBox.width, alignAttr.y + bBox.height);
17203
 
17204
			}
17205
 
17206
			// When we're using a shape, make it possible with a connector or an arrow pointing to thie point
17207
			if (options.shape) {
17208
				dataLabel.attr({
17209
					anchorX: point.plotX,
17210
					anchorY: point.plotY
17211
				});
17212
			}
17213
 
17214
		}
17215
	}
17216
 
17217
	// Show or hide based on the final aligned position
17218
	if (!visible) {
17219
		dataLabel.attr({ y: -999 });
17220
		dataLabel.placed = false; // don't animate back in
17221
	}
17222
 
17223
};
17224
 
17225
/**
17226
 * If data labels fall partly outside the plot area, align them back in, in a way that
17227
 * doesn't hide the point.
17228
 */
17229
Series.prototype.justifyDataLabel = function (dataLabel, options, alignAttr, bBox, alignTo, isNew) {
17230
	var chart = this.chart,
17231
		align = options.align,
17232
		verticalAlign = options.verticalAlign,
17233
		off,
17234
		justified,
17235
		padding = dataLabel.box ? 0 : (dataLabel.padding || 0);
17236
 
17237
	// Off left
17238
	off = alignAttr.x + padding;
17239
	if (off < 0) {
17240
		if (align === 'right') {
17241
			options.align = 'left';
17242
		} else {
17243
			options.x = -off;
17244
		}
17245
		justified = true;
17246
	}
17247
 
17248
	// Off right
17249
	off = alignAttr.x + bBox.width - padding;
17250
	if (off > chart.plotWidth) {
17251
		if (align === 'left') {
17252
			options.align = 'right';
17253
		} else {
17254
			options.x = chart.plotWidth - off;
17255
		}
17256
		justified = true;
17257
	}
17258
 
17259
	// Off top
17260
	off = alignAttr.y + padding;
17261
	if (off < 0) {
17262
		if (verticalAlign === 'bottom') {
17263
			options.verticalAlign = 'top';
17264
		} else {
17265
			options.y = -off;
17266
		}
17267
		justified = true;
17268
	}
17269
 
17270
	// Off bottom
17271
	off = alignAttr.y + bBox.height - padding;
17272
	if (off > chart.plotHeight) {
17273
		if (verticalAlign === 'top') {
17274
			options.verticalAlign = 'bottom';
17275
		} else {
17276
			options.y = chart.plotHeight - off;
17277
		}
17278
		justified = true;
17279
	}
17280
 
17281
	if (justified) {
17282
		dataLabel.placed = !isNew;
17283
		dataLabel.align(options, null, alignTo);
17284
	}
17285
};
17286
 
17287
/**
17288
 * Override the base drawDataLabels method by pie specific functionality
17289
 */
17290
if (seriesTypes.pie) {
17291
	seriesTypes.pie.prototype.drawDataLabels = function () {
17292
		var series = this,
17293
			data = series.data,
17294
			point,
17295
			chart = series.chart,
17296
			options = series.options.dataLabels,
17297
			connectorPadding = pick(options.connectorPadding, 10),
17298
			connectorWidth = pick(options.connectorWidth, 1),
17299
			plotWidth = chart.plotWidth,
17300
			plotHeight = chart.plotHeight,
17301
			connector,
17302
			connectorPath,
17303
			softConnector = pick(options.softConnector, true),
17304
			distanceOption = options.distance,
17305
			seriesCenter = series.center,
17306
			radius = seriesCenter[2] / 2,
17307
			centerY = seriesCenter[1],
17308
			outside = distanceOption > 0,
17309
			dataLabel,
17310
			dataLabelWidth,
17311
			labelPos,
17312
			labelHeight,
17313
			halves = [// divide the points into right and left halves for anti collision
17314
				[], // right
17315
				[]  // left
17316
			],
17317
			x,
17318
			y,
17319
			visibility,
17320
			rankArr,
17321
			i,
17322
			j,
17323
			overflow = [0, 0, 0, 0], // top, right, bottom, left
17324
			sort = function (a, b) {
17325
				return b.y - a.y;
17326
			};
17327
 
17328
		// get out if not enabled
17329
		if (!series.visible || (!options.enabled && !series._hasPointLabels)) {
17330
			return;
17331
		}
17332
 
17333
		// run parent method
17334
		Series.prototype.drawDataLabels.apply(series);
17335
 
17336
		// arrange points for detection collision
17337
		each(data, function (point) {
17338
			if (point.dataLabel && point.visible) { // #407, #2510
17339
				halves[point.half].push(point);
17340
			}
17341
		});
17342
 
17343
		/* Loop over the points in each half, starting from the top and bottom
17344
		 * of the pie to detect overlapping labels.
17345
		 */
17346
		i = 2;
17347
		while (i--) {
17348
 
17349
			var slots = [],
17350
				slotsLength,
17351
				usedSlots = [],
17352
				points = halves[i],
17353
				pos,
17354
				bottom,
17355
				length = points.length,
17356
				slotIndex;
17357
 
17358
			if (!length) {
17359
				continue;
17360
			}
17361
 
17362
			// Sort by angle
17363
			series.sortByAngle(points, i - 0.5);
17364
 
17365
			// Assume equal label heights on either hemisphere (#2630)
17366
			j = labelHeight = 0;
17367
			while (!labelHeight && points[j]) { // #1569
17368
				labelHeight = points[j] && points[j].dataLabel && (points[j].dataLabel.getBBox().height || 21); // 21 is for #968
17369
				j++;
17370
			}
17371
 
17372
			// Only do anti-collision when we are outside the pie and have connectors (#856)
17373
			if (distanceOption > 0) {
17374
 
17375
				// Build the slots
17376
				bottom = mathMin(centerY + radius + distanceOption, chart.plotHeight);
17377
				for (pos = mathMax(0, centerY - radius - distanceOption); pos <= bottom; pos += labelHeight) {
17378
					slots.push(pos);
17379
				}
17380
				slotsLength = slots.length;
17381
 
17382
 
17383
				/* Visualize the slots
17384
				if (!series.slotElements) {
17385
					series.slotElements = [];
17386
				}
17387
				if (i === 1) {
17388
					series.slotElements.forEach(function (elem) {
17389
						elem.destroy();
17390
					});
17391
					series.slotElements.length = 0;
17392
				}
17393
 
17394
				slots.forEach(function (pos, no) {
17395
					var slotX = series.getX(pos, i) + chart.plotLeft - (i ? 100 : 0),
17396
						slotY = pos + chart.plotTop;
17397
 
17398
					if (!isNaN(slotX)) {
17399
						series.slotElements.push(chart.renderer.rect(slotX, slotY - 7, 100, labelHeight, 1)
17400
							.attr({
17401
								'stroke-width': 1,
17402
								stroke: 'silver',
17403
								fill: 'rgba(0,0,255,0.1)'
17404
							})
17405
							.add());
17406
						series.slotElements.push(chart.renderer.text('Slot '+ no, slotX, slotY + 4)
17407
							.attr({
17408
								fill: 'silver'
17409
							}).add());
17410
					}
17411
				});
17412
				// */
17413
 
17414
				// if there are more values than available slots, remove lowest values
17415
				if (length > slotsLength) {
17416
					// create an array for sorting and ranking the points within each quarter
17417
					rankArr = [].concat(points);
17418
					rankArr.sort(sort);
17419
					j = length;
17420
					while (j--) {
17421
						rankArr[j].rank = j;
17422
					}
17423
					j = length;
17424
					while (j--) {
17425
						if (points[j].rank >= slotsLength) {
17426
							points.splice(j, 1);
17427
						}
17428
					}
17429
					length = points.length;
17430
				}
17431
 
17432
				// The label goes to the nearest open slot, but not closer to the edge than
17433
				// the label's index.
17434
				for (j = 0; j < length; j++) {
17435
 
17436
					point = points[j];
17437
					labelPos = point.labelPos;
17438
 
17439
					var closest = 9999,
17440
						distance,
17441
						slotI;
17442
 
17443
					// find the closest slot index
17444
					for (slotI = 0; slotI < slotsLength; slotI++) {
17445
						distance = mathAbs(slots[slotI] - labelPos[1]);
17446
						if (distance < closest) {
17447
							closest = distance;
17448
							slotIndex = slotI;
17449
						}
17450
					}
17451
 
17452
					// if that slot index is closer to the edges of the slots, move it
17453
					// to the closest appropriate slot
17454
					if (slotIndex < j && slots[j] !== null) { // cluster at the top
17455
						slotIndex = j;
17456
					} else if (slotsLength  < length - j + slotIndex && slots[j] !== null) { // cluster at the bottom
17457
						slotIndex = slotsLength - length + j;
17458
						while (slots[slotIndex] === null) { // make sure it is not taken
17459
							slotIndex++;
17460
						}
17461
					} else {
17462
						// Slot is taken, find next free slot below. In the next run, the next slice will find the
17463
						// slot above these, because it is the closest one
17464
						while (slots[slotIndex] === null) { // make sure it is not taken
17465
							slotIndex++;
17466
						}
17467
					}
17468
 
17469
					usedSlots.push({ i: slotIndex, y: slots[slotIndex] });
17470
					slots[slotIndex] = null; // mark as taken
17471
				}
17472
				// sort them in order to fill in from the top
17473
				usedSlots.sort(sort);
17474
			}
17475
 
17476
			// now the used slots are sorted, fill them up sequentially
17477
			for (j = 0; j < length; j++) {
17478
 
17479
				var slot, naturalY;
17480
 
17481
				point = points[j];
17482
				labelPos = point.labelPos;
17483
				dataLabel = point.dataLabel;
17484
				visibility = point.visible === false ? HIDDEN : 'inherit';
17485
				naturalY = labelPos[1];
17486
 
17487
				if (distanceOption > 0) {
17488
					slot = usedSlots.pop();
17489
					slotIndex = slot.i;
17490
 
17491
					// if the slot next to currrent slot is free, the y value is allowed
17492
					// to fall back to the natural position
17493
					y = slot.y;
17494
					if ((naturalY > y && slots[slotIndex + 1] !== null) ||
17495
							(naturalY < y &&  slots[slotIndex - 1] !== null)) {
17496
						y = mathMin(mathMax(0, naturalY), chart.plotHeight);
17497
					}
17498
 
17499
				} else {
17500
					y = naturalY;
17501
				}
17502
 
17503
				// get the x - use the natural x position for first and last slot, to prevent the top
17504
				// and botton slice connectors from touching each other on either side
17505
				x = options.justify ?
17506
					seriesCenter[0] + (i ? -1 : 1) * (radius + distanceOption) :
17507
					series.getX(y === centerY - radius - distanceOption || y === centerY + radius + distanceOption ? naturalY : y, i);
17508
 
17509
 
17510
				// Record the placement and visibility
17511
				dataLabel._attr = {
17512
					visibility: visibility,
17513
					align: labelPos[6]
17514
				};
17515
				dataLabel._pos = {
17516
					x: x + options.x +
17517
						({ left: connectorPadding, right: -connectorPadding }[labelPos[6]] || 0),
17518
					y: y + options.y - 10 // 10 is for the baseline (label vs text)
17519
				};
17520
				dataLabel.connX = x;
17521
				dataLabel.connY = y;
17522
 
17523
 
17524
				// Detect overflowing data labels
17525
				if (this.options.size === null) {
17526
					dataLabelWidth = dataLabel.width;
17527
					// Overflow left
17528
					if (x - dataLabelWidth < connectorPadding) {
17529
						overflow[3] = mathMax(mathRound(dataLabelWidth - x + connectorPadding), overflow[3]);
17530
 
17531
					// Overflow right
17532
					} else if (x + dataLabelWidth > plotWidth - connectorPadding) {
17533
						overflow[1] = mathMax(mathRound(x + dataLabelWidth - plotWidth + connectorPadding), overflow[1]);
17534
					}
17535
 
17536
					// Overflow top
17537
					if (y - labelHeight / 2 < 0) {
17538
						overflow[0] = mathMax(mathRound(-y + labelHeight / 2), overflow[0]);
17539
 
17540
					// Overflow left
17541
					} else if (y + labelHeight / 2 > plotHeight) {
17542
						overflow[2] = mathMax(mathRound(y + labelHeight / 2 - plotHeight), overflow[2]);
17543
					}
17544
				}
17545
			} // for each point
17546
		} // for each half
17547
 
17548
		// Do not apply the final placement and draw the connectors until we have verified
17549
		// that labels are not spilling over.
17550
		if (arrayMax(overflow) === 0 || this.verifyDataLabelOverflow(overflow)) {
17551
 
17552
			// Place the labels in the final position
17553
			this.placeDataLabels();
17554
 
17555
			// Draw the connectors
17556
			if (outside && connectorWidth) {
17557
				each(this.points, function (point) {
17558
					connector = point.connector;
17559
					labelPos = point.labelPos;
17560
					dataLabel = point.dataLabel;
17561
 
17562
					if (dataLabel && dataLabel._pos && point.visible) {
17563
						visibility = dataLabel._attr.visibility;
17564
						x = dataLabel.connX;
17565
						y = dataLabel.connY;
17566
						connectorPath = softConnector ? [
17567
							M,
17568
							x + (labelPos[6] === 'left' ? 5 : -5), y, // end of the string at the label
17569
							'C',
17570
							x, y, // first break, next to the label
17571
							2 * labelPos[2] - labelPos[4], 2 * labelPos[3] - labelPos[5],
17572
							labelPos[2], labelPos[3], // second break
17573
							L,
17574
							labelPos[4], labelPos[5] // base
17575
						] : [
17576
							M,
17577
							x + (labelPos[6] === 'left' ? 5 : -5), y, // end of the string at the label
17578
							L,
17579
							labelPos[2], labelPos[3], // second break
17580
							L,
17581
							labelPos[4], labelPos[5] // base
17582
						];
17583
 
17584
						if (connector) {
17585
							connector.animate({ d: connectorPath });
17586
							connector.attr('visibility', visibility);
17587
 
17588
						} else {
17589
							point.connector = connector = series.chart.renderer.path(connectorPath).attr({
17590
								'stroke-width': connectorWidth,
17591
								stroke: options.connectorColor || point.color || '#606060',
17592
								visibility: visibility
17593
								//zIndex: 0 // #2722 (reversed)
17594
							})
17595
							.add(series.dataLabelsGroup);
17596
						}
17597
					} else if (connector) {
17598
						point.connector = connector.destroy();
17599
					}
17600
				});
17601
			}
17602
		}
17603
	};
17604
	/**
17605
	 * Perform the final placement of the data labels after we have verified that they
17606
	 * fall within the plot area.
17607
	 */
17608
	seriesTypes.pie.prototype.placeDataLabels = function () {
17609
		each(this.points, function (point) {
17610
			var dataLabel = point.dataLabel,
17611
				_pos;
17612
 
17613
			if (dataLabel && point.visible) {
17614
				_pos = dataLabel._pos;
17615
				if (_pos) {
17616
					dataLabel.attr(dataLabel._attr);
17617
					dataLabel[dataLabel.moved ? 'animate' : 'attr'](_pos);
17618
					dataLabel.moved = true;
17619
				} else if (dataLabel) {
17620
					dataLabel.attr({ y: -999 });
17621
				}
17622
			}
17623
		});
17624
	};
17625
 
17626
	seriesTypes.pie.prototype.alignDataLabel =  noop;
17627
 
17628
	/**
17629
	 * Verify whether the data labels are allowed to draw, or we should run more translation and data
17630
	 * label positioning to keep them inside the plot area. Returns true when data labels are ready
17631
	 * to draw.
17632
	 */
17633
	seriesTypes.pie.prototype.verifyDataLabelOverflow = function (overflow) {
17634
 
17635
		var center = this.center,
17636
			options = this.options,
17637
			centerOption = options.center,
17638
			minSize = options.minSize || 80,
17639
			newSize = minSize,
17640
			ret;
17641
 
17642
		// Handle horizontal size and center
17643
		if (centerOption[0] !== null) { // Fixed center
17644
			newSize = mathMax(center[2] - mathMax(overflow[1], overflow[3]), minSize);
17645
 
17646
		} else { // Auto center
17647
			newSize = mathMax(
17648
				center[2] - overflow[1] - overflow[3], // horizontal overflow
17649
				minSize
17650
			);
17651
			center[0] += (overflow[3] - overflow[1]) / 2; // horizontal center
17652
		}
17653
 
17654
		// Handle vertical size and center
17655
		if (centerOption[1] !== null) { // Fixed center
17656
			newSize = mathMax(mathMin(newSize, center[2] - mathMax(overflow[0], overflow[2])), minSize);
17657
 
17658
		} else { // Auto center
17659
			newSize = mathMax(
17660
				mathMin(
17661
					newSize,
17662
					center[2] - overflow[0] - overflow[2] // vertical overflow
17663
				),
17664
				minSize
17665
			);
17666
			center[1] += (overflow[0] - overflow[2]) / 2; // vertical center
17667
		}
17668
 
17669
		// If the size must be decreased, we need to run translate and drawDataLabels again
17670
		if (newSize < center[2]) {
17671
			center[2] = newSize;
17672
			center[3] = relativeLength(options.innerSize || 0, newSize);
17673
			this.translate(center);
17674
			each(this.points, function (point) {
17675
				if (point.dataLabel) {
17676
					point.dataLabel._pos = null; // reset
17677
				}
17678
			});
17679
 
17680
			if (this.drawDataLabels) {
17681
				this.drawDataLabels();
17682
			}
17683
		// Else, return true to indicate that the pie and its labels is within the plot area
17684
		} else {
17685
			ret = true;
17686
		}
17687
		return ret;
17688
	};
17689
}
17690
 
17691
if (seriesTypes.column) {
17692
 
17693
	/**
17694
	 * Override the basic data label alignment by adjusting for the position of the column
17695
	 */
17696
	seriesTypes.column.prototype.alignDataLabel = function (point, dataLabel, options,  alignTo, isNew) {
17697
		var inverted = this.chart.inverted,
17698
			series = point.series,
17699
			dlBox = point.dlBox || point.shapeArgs, // data label box for alignment
17700
			below = pick(point.below, point.plotY > pick(this.translatedThreshold, series.yAxis.len)), // point.below is used in range series
17701
			inside = pick(options.inside, !!this.options.stacking); // draw it inside the box?
17702
 
17703
		// Align to the column itself, or the top of it
17704
		if (dlBox) { // Area range uses this method but not alignTo
17705
			alignTo = merge(dlBox);
17706
 
17707
			if (inverted) {
17708
				alignTo = {
17709
					x: series.yAxis.len - alignTo.y - alignTo.height,
17710
					y: series.xAxis.len - alignTo.x - alignTo.width,
17711
					width: alignTo.height,
17712
					height: alignTo.width
17713
				};
17714
			}
17715
 
17716
			// Compute the alignment box
17717
			if (!inside) {
17718
				if (inverted) {
17719
					alignTo.x += below ? 0 : alignTo.width;
17720
					alignTo.width = 0;
17721
				} else {
17722
					alignTo.y += below ? alignTo.height : 0;
17723
					alignTo.height = 0;
17724
				}
17725
			}
17726
		}
17727
 
17728
 
17729
		// When alignment is undefined (typically columns and bars), display the individual
17730
		// point below or above the point depending on the threshold
17731
		options.align = pick(
17732
			options.align,
17733
			!inverted || inside ? 'center' : below ? 'right' : 'left'
17734
		);
17735
		options.verticalAlign = pick(
17736
			options.verticalAlign,
17737
			inverted || inside ? 'middle' : below ? 'top' : 'bottom'
17738
		);
17739
 
17740
		// Call the parent method
17741
		Series.prototype.alignDataLabel.call(this, point, dataLabel, options, alignTo, isNew);
17742
	};
17743
}
17744
 
17745
 
17746
 
17747
/**
17748
 * Highstock JS v2.1.8 (2015-08-20)
17749
 * Highcharts module to hide overlapping data labels. This module is included by default in Highmaps.
17750
 *
17751
 * (c) 2010-2014 Torstein Honsi
17752
 *
17753
 * License: www.highcharts.com/license
17754
 */
17755
 
17756
/*global Highcharts, HighchartsAdapter */
17757
(function (H) {
17758
	var Chart = H.Chart,
17759
		each = H.each,
17760
		pick = H.pick,
17761
		addEvent = HighchartsAdapter.addEvent;
17762
 
17763
	// Collect potensial overlapping data labels. Stack labels probably don't need to be
17764
	// considered because they are usually accompanied by data labels that lie inside the columns.
17765
	Chart.prototype.callbacks.push(function (chart) {
17766
		function collectAndHide() {
17767
			var labels = [];
17768
 
17769
			each(chart.series, function (series) {
17770
				var dlOptions = series.options.dataLabels,
17771
					collections = series.dataLabelCollections || ['dataLabel']; // Range series have two collections
17772
				if ((dlOptions.enabled || series._hasPointLabels) && !dlOptions.allowOverlap && series.visible) { // #3866
17773
					each(collections, function (coll) {
17774
						each(series.points, function (point) {
17775
							if (point[coll]) {
17776
								point[coll].labelrank = pick(point.labelrank, point.shapeArgs && point.shapeArgs.height); // #4118
17777
								labels.push(point[coll]);
17778
							}
17779
						});
17780
					});
17781
				}
17782
			});
17783
			chart.hideOverlappingLabels(labels);
17784
		}
17785
 
17786
		// Do it now ...
17787
		collectAndHide();
17788
 
17789
		// ... and after each chart redraw
17790
		addEvent(chart, 'redraw', collectAndHide);
17791
 
17792
	});
17793
 
17794
	/**
17795
	 * Hide overlapping labels. Labels are moved and faded in and out on zoom to provide a smooth
17796
	 * visual imression.
17797
	 */
17798
	Chart.prototype.hideOverlappingLabels = function (labels) {
17799
 
17800
		var len = labels.length,
17801
			label,
17802
			i,
17803
			j,
17804
			label1,
17805
			label2,
17806
			isIntersecting,
17807
			pos1,
17808
			pos2,
17809
			padding,
17810
			intersectRect = function (x1, y1, w1, h1, x2, y2, w2, h2) {
17811
				return !(
17812
					x2 > x1 + w1 ||
17813
					x2 + w2 < x1 ||
17814
					y2 > y1 + h1 ||
17815
					y2 + h2 < y1
17816
				);
17817
			};
17818
 
17819
		// Mark with initial opacity
17820
		for (i = 0; i < len; i++) {
17821
			label = labels[i];
17822
			if (label) {
17823
				label.oldOpacity = label.opacity;
17824
				label.newOpacity = 1;
17825
			}
17826
		}
17827
 
17828
		// Prevent a situation in a gradually rising slope, that each label
17829
		// will hide the previous one because the previous one always has
17830
		// lower rank.
17831
		labels.sort(function (a, b) {
17832
			return (b.labelrank || 0) - (a.labelrank || 0);
17833
		});
17834
 
17835
		// Detect overlapping labels
17836
		for (i = 0; i < len; i++) {
17837
			label1 = labels[i];
17838
 
17839
			for (j = i + 1; j < len; ++j) {
17840
				label2 = labels[j];
17841
				if (label1 && label2 && label1.placed && label2.placed && label1.newOpacity !== 0 && label2.newOpacity !== 0) {
17842
					pos1 = label1.alignAttr;
17843
					pos2 = label2.alignAttr;
17844
					padding = 2 * (label1.box ? 0 : label1.padding); // Substract the padding if no background or border (#4333)
17845
					isIntersecting = intersectRect(
17846
						pos1.x,
17847
						pos1.y,
17848
						label1.width - padding,
17849
						label1.height - padding,
17850
						pos2.x,
17851
						pos2.y,
17852
						label2.width - padding,
17853
						label2.height - padding
17854
					);
17855
 
17856
					if (isIntersecting) {
17857
						(label1.labelrank < label2.labelrank ? label1 : label2).newOpacity = 0;
17858
					}
17859
				}
17860
			}
17861
		}
17862
 
17863
		// Hide or show
17864
		each(labels, function (label) {
17865
			var complete,
17866
				newOpacity;
17867
 
17868
			if (label) {
17869
				newOpacity = label.newOpacity;
17870
 
17871
				if (label.oldOpacity !== newOpacity && label.placed) {
17872
 
17873
					// Make sure the label is completely hidden to avoid catching clicks (#4362)
17874
					if (newOpacity) {
17875
						label.show(true);
17876
					} else {
17877
						complete = function () {
17878
							label.hide();
17879
						};
17880
					}
17881
 
17882
					// Animate or set the opacity
17883
					label.alignAttr.opacity = newOpacity;
17884
					label[label.isOld ? 'animate' : 'attr'](label.alignAttr, null, complete);
17885
 
17886
				}
17887
				label.isOld = true;
17888
			}
17889
		});
17890
	};
17891
 
17892
}(Highcharts));/**
17893
 * TrackerMixin for points and graphs
17894
 */
17895
 
17896
var TrackerMixin = Highcharts.TrackerMixin = {
17897
 
17898
	drawTrackerPoint: function () {
17899
		var series = this,
17900
			chart = series.chart,
17901
			pointer = chart.pointer,
17902
			cursor = series.options.cursor,
17903
			css = cursor && { cursor: cursor },
17904
			onMouseOver = function (e) {
17905
				var target = e.target,
17906
				point;
17907
 
17908
				while (target && !point) {
17909
					point = target.point;
17910
					target = target.parentNode;
17911
				}
17912
 
17913
				if (point !== UNDEFINED && point !== chart.hoverPoint) { // undefined on graph in scatterchart
17914
					point.onMouseOver(e);
17915
				}
17916
			};
17917
 
17918
		// Add reference to the point
17919
		each(series.points, function (point) {
17920
			if (point.graphic) {
17921
				point.graphic.element.point = point;
17922
			}
17923
			if (point.dataLabel) {
17924
				point.dataLabel.element.point = point;
17925
			}
17926
		});
17927
 
17928
		// Add the event listeners, we need to do this only once
17929
		if (!series._hasTracking) {
17930
			each(series.trackerGroups, function (key) {
17931
				if (series[key]) { // we don't always have dataLabelsGroup
17932
					series[key]
17933
						.addClass(PREFIX + 'tracker')
17934
						.on('mouseover', onMouseOver)
17935
						.on('mouseout', function (e) { pointer.onTrackerMouseOut(e); })
17936
						.css(css);
17937
					if (hasTouch) {
17938
						series[key].on('touchstart', onMouseOver);
17939
					}
17940
				}
17941
			});
17942
			series._hasTracking = true;
17943
		}
17944
	},
17945
 
17946
	/**
17947
	 * Draw the tracker object that sits above all data labels and markers to
17948
	 * track mouse events on the graph or points. For the line type charts
17949
	 * the tracker uses the same graphPath, but with a greater stroke width
17950
	 * for better control.
17951
	 */
17952
	drawTrackerGraph: function () {
17953
		var series = this,
17954
			options = series.options,
17955
			trackByArea = options.trackByArea,
17956
			trackerPath = [].concat(trackByArea ? series.areaPath : series.graphPath),
17957
			trackerPathLength = trackerPath.length,
17958
			chart = series.chart,
17959
			pointer = chart.pointer,
17960
			renderer = chart.renderer,
17961
			snap = chart.options.tooltip.snap,
17962
			tracker = series.tracker,
17963
			cursor = options.cursor,
17964
			css = cursor && { cursor: cursor },
17965
			singlePoints = series.singlePoints,
17966
			singlePoint,
17967
			i,
17968
			onMouseOver = function () {
17969
				if (chart.hoverSeries !== series) {
17970
					series.onMouseOver();
17971
				}
17972
			},
17973
			/*
17974
			 * Empirical lowest possible opacities for TRACKER_FILL for an element to stay invisible but clickable
17975
			 * IE6: 0.002
17976
			 * IE7: 0.002
17977
			 * IE8: 0.002
17978
			 * IE9: 0.00000000001 (unlimited)
17979
			 * IE10: 0.0001 (exporting only)
17980
			 * FF: 0.00000000001 (unlimited)
17981
			 * Chrome: 0.000001
17982
			 * Safari: 0.000001
17983
			 * Opera: 0.00000000001 (unlimited)
17984
			 */
17985
			TRACKER_FILL = 'rgba(192,192,192,' + (hasSVG ? 0.0001 : 0.002) + ')';
17986
 
17987
		// Extend end points. A better way would be to use round linecaps,
17988
		// but those are not clickable in VML.
17989
		if (trackerPathLength && !trackByArea) {
17990
			i = trackerPathLength + 1;
17991
			while (i--) {
17992
				if (trackerPath[i] === M) { // extend left side
17993
					trackerPath.splice(i + 1, 0, trackerPath[i + 1] - snap, trackerPath[i + 2], L);
17994
				}
17995
				if ((i && trackerPath[i] === M) || i === trackerPathLength) { // extend right side
17996
					trackerPath.splice(i, 0, L, trackerPath[i - 2] + snap, trackerPath[i - 1]);
17997
				}
17998
			}
17999
		}
18000
 
18001
		// handle single points
18002
		for (i = 0; i < singlePoints.length; i++) {
18003
			singlePoint = singlePoints[i];
18004
			trackerPath.push(M, singlePoint.plotX - snap, singlePoint.plotY,
18005
			L, singlePoint.plotX + snap, singlePoint.plotY);
18006
		}
18007
 
18008
		// draw the tracker
18009
		if (tracker) {
18010
			tracker.attr({ d: trackerPath });
18011
		} else { // create
18012
 
18013
			series.tracker = renderer.path(trackerPath)
18014
			.attr({
18015
				'stroke-linejoin': 'round', // #1225
18016
				visibility: series.visible ? VISIBLE : HIDDEN,
18017
				stroke: TRACKER_FILL,
18018
				fill: trackByArea ? TRACKER_FILL : NONE,
18019
				'stroke-width' : options.lineWidth + (trackByArea ? 0 : 2 * snap),
18020
				zIndex: 2
18021
			})
18022
			.add(series.group);
18023
 
18024
			// The tracker is added to the series group, which is clipped, but is covered
18025
			// by the marker group. So the marker group also needs to capture events.
18026
			each([series.tracker, series.markerGroup], function (tracker) {
18027
				tracker.addClass(PREFIX + 'tracker')
18028
					.on('mouseover', onMouseOver)
18029
					.on('mouseout', function (e) { pointer.onTrackerMouseOut(e); })
18030
					.css(css);
18031
 
18032
				if (hasTouch) {
18033
					tracker.on('touchstart', onMouseOver);
18034
				}
18035
			});
18036
		}
18037
	}
18038
};
18039
/* End TrackerMixin */
18040
 
18041
 
18042
/**
18043
 * Add tracking event listener to the series group, so the point graphics
18044
 * themselves act as trackers
18045
 */
18046
 
18047
if (seriesTypes.column) {
18048
	ColumnSeries.prototype.drawTracker = TrackerMixin.drawTrackerPoint;
18049
}
18050
 
18051
if (seriesTypes.pie) {
18052
	seriesTypes.pie.prototype.drawTracker = TrackerMixin.drawTrackerPoint;
18053
}
18054
 
18055
if (seriesTypes.scatter) {
18056
	ScatterSeries.prototype.drawTracker = TrackerMixin.drawTrackerPoint;
18057
}
18058
 
18059
/*
18060
 * Extend Legend for item events
18061
 */
18062
extend(Legend.prototype, {
18063
 
18064
	setItemEvents: function (item, legendItem, useHTML, itemStyle, itemHiddenStyle) {
18065
	var legend = this;
18066
	// Set the events on the item group, or in case of useHTML, the item itself (#1249)
18067
	(useHTML ? legendItem : item.legendGroup).on('mouseover', function () {
18068
			item.setState(HOVER_STATE);
18069
			legendItem.css(legend.options.itemHoverStyle);
18070
		})
18071
		.on('mouseout', function () {
18072
			legendItem.css(item.visible ? itemStyle : itemHiddenStyle);
18073
			item.setState();
18074
		})
18075
		.on('click', function (event) {
18076
			var strLegendItemClick = 'legendItemClick',
18077
				fnLegendItemClick = function () {
18078
					item.setVisible();
18079
				};
18080
 
18081
			// Pass over the click/touch event. #4.
18082
			event = {
18083
				browserEvent: event
18084
			};
18085
 
18086
			// click the name or symbol
18087
			if (item.firePointEvent) { // point
18088
				item.firePointEvent(strLegendItemClick, event, fnLegendItemClick);
18089
			} else {
18090
				fireEvent(item, strLegendItemClick, event, fnLegendItemClick);
18091
			}
18092
		});
18093
	},
18094
 
18095
	createCheckboxForItem: function (item) {
18096
		var legend = this;
18097
 
18098
		item.checkbox = createElement('input', {
18099
			type: 'checkbox',
18100
			checked: item.selected,
18101
			defaultChecked: item.selected // required by IE7
18102
		}, legend.options.itemCheckboxStyle, legend.chart.container);
18103
 
18104
		addEvent(item.checkbox, 'click', function (event) {
18105
			var target = event.target;
18106
			fireEvent(item.series || item, 'checkboxClick', { // #3712
18107
					checked: target.checked,
18108
					item: item
18109
				},
18110
				function () {
18111
					item.select();
18112
				}
18113
			);
18114
		});
18115
	}
18116
});
18117
 
18118
/*
18119
 * Add pointer cursor to legend itemstyle in defaultOptions
18120
 */
18121
defaultOptions.legend.itemStyle.cursor = 'pointer';
18122
 
18123
 
18124
/*
18125
 * Extend the Chart object with interaction
18126
 */
18127
 
18128
extend(Chart.prototype, {
18129
	/**
18130
	 * Display the zoom button
18131
	 */
18132
	showResetZoom: function () {
18133
		var chart = this,
18134
			lang = defaultOptions.lang,
18135
			btnOptions = chart.options.chart.resetZoomButton,
18136
			theme = btnOptions.theme,
18137
			states = theme.states,
18138
			alignTo = btnOptions.relativeTo === 'chart' ? null : 'plotBox';
18139
 
18140
		this.resetZoomButton = chart.renderer.button(lang.resetZoom, null, null, function () { chart.zoomOut(); }, theme, states && states.hover)
18141
			.attr({
18142
				align: btnOptions.position.align,
18143
				title: lang.resetZoomTitle
18144
			})
18145
			.add()
18146
			.align(btnOptions.position, false, alignTo);
18147
 
18148
	},
18149
 
18150
	/**
18151
	 * Zoom out to 1:1
18152
	 */
18153
	zoomOut: function () {
18154
		var chart = this;
18155
		fireEvent(chart, 'selection', { resetSelection: true }, function () {
18156
			chart.zoom();
18157
		});
18158
	},
18159
 
18160
	/**
18161
	 * Zoom into a given portion of the chart given by axis coordinates
18162
	 * @param {Object} event
18163
	 */
18164
	zoom: function (event) {
18165
		var chart = this,
18166
			hasZoomed,
18167
			pointer = chart.pointer,
18168
			displayButton = false,
18169
			resetZoomButton;
18170
 
18171
		// If zoom is called with no arguments, reset the axes
18172
		if (!event || event.resetSelection) {
18173
			each(chart.axes, function (axis) {
18174
				hasZoomed = axis.zoom();
18175
			});
18176
		} else { // else, zoom in on all axes
18177
			each(event.xAxis.concat(event.yAxis), function (axisData) {
18178
				var axis = axisData.axis,
18179
					isXAxis = axis.isXAxis;
18180
 
18181
				// don't zoom more than minRange
18182
				if (pointer[isXAxis ? 'zoomX' : 'zoomY'] || pointer[isXAxis ? 'pinchX' : 'pinchY']) {
18183
					hasZoomed = axis.zoom(axisData.min, axisData.max);
18184
					if (axis.displayBtn) {
18185
						displayButton = true;
18186
					}
18187
				}
18188
			});
18189
		}
18190
 
18191
		// Show or hide the Reset zoom button
18192
		resetZoomButton = chart.resetZoomButton;
18193
		if (displayButton && !resetZoomButton) {
18194
			chart.showResetZoom();
18195
		} else if (!displayButton && isObject(resetZoomButton)) {
18196
			chart.resetZoomButton = resetZoomButton.destroy();
18197
		}
18198
 
18199
 
18200
		// Redraw
18201
		if (hasZoomed) {
18202
			chart.redraw(
18203
				pick(chart.options.chart.animation, event && event.animation, chart.pointCount < 100) // animation
18204
			);
18205
		}
18206
	},
18207
 
18208
	/**
18209
	 * Pan the chart by dragging the mouse across the pane. This function is called
18210
	 * on mouse move, and the distance to pan is computed from chartX compared to
18211
	 * the first chartX position in the dragging operation.
18212
	 */
18213
	pan: function (e, panning) {
18214
 
18215
		var chart = this,
18216
			hoverPoints = chart.hoverPoints,
18217
			doRedraw;
18218
 
18219
		// remove active points for shared tooltip
18220
		if (hoverPoints) {
18221
			each(hoverPoints, function (point) {
18222
				point.setState();
18223
			});
18224
		}
18225
 
18226
		each(panning === 'xy' ? [1, 0] : [1], function (isX) { // xy is used in maps
18227
			var mousePos = e[isX ? 'chartX' : 'chartY'],
18228
				axis = chart[isX ? 'xAxis' : 'yAxis'][0],
18229
				startPos = chart[isX ? 'mouseDownX' : 'mouseDownY'],
18230
				halfPointRange = (axis.pointRange || 0) / 2,
18231
				extremes = axis.getExtremes(),
18232
				newMin = axis.toValue(startPos - mousePos, true) + halfPointRange,
18233
				newMax = axis.toValue(startPos + chart[isX ? 'plotWidth' : 'plotHeight'] - mousePos, true) - halfPointRange,
18234
				goingLeft = startPos > mousePos; // #3613
18235
 
18236
			if (axis.series.length &&
18237
					(goingLeft || newMin > mathMin(extremes.dataMin, extremes.min)) &&
18238
					(!goingLeft || newMax < mathMax(extremes.dataMax, extremes.max))) {
18239
				axis.setExtremes(newMin, newMax, false, false, { trigger: 'pan' });
18240
				doRedraw = true;
18241
			}
18242
 
18243
			chart[isX ? 'mouseDownX' : 'mouseDownY'] = mousePos; // set new reference for next run
18244
		});
18245
 
18246
		if (doRedraw) {
18247
			chart.redraw(false);
18248
		}
18249
		css(chart.container, { cursor: 'move' });
18250
	}
18251
});
18252
 
18253
/*
18254
 * Extend the Point object with interaction
18255
 */
18256
extend(Point.prototype, {
18257
	/**
18258
	 * Toggle the selection status of a point
18259
	 * @param {Boolean} selected Whether to select or unselect the point.
18260
	 * @param {Boolean} accumulate Whether to add to the previous selection. By default,
18261
	 *		 this happens if the control key (Cmd on Mac) was pressed during clicking.
18262
	 */
18263
	select: function (selected, accumulate) {
18264
		var point = this,
18265
			series = point.series,
18266
			chart = series.chart;
18267
 
18268
		selected = pick(selected, !point.selected);
18269
 
18270
		// fire the event with the defalut handler
18271
		point.firePointEvent(selected ? 'select' : 'unselect', { accumulate: accumulate }, function () {
18272
			point.selected = point.options.selected = selected;
18273
			series.options.data[inArray(point, series.data)] = point.options;
18274
 
18275
			point.setState(selected && SELECT_STATE);
18276
 
18277
			// unselect all other points unless Ctrl or Cmd + click
18278
			if (!accumulate) {
18279
				each(chart.getSelectedPoints(), function (loopPoint) {
18280
					if (loopPoint.selected && loopPoint !== point) {
18281
						loopPoint.selected = loopPoint.options.selected = false;
18282
						series.options.data[inArray(loopPoint, series.data)] = loopPoint.options;
18283
						loopPoint.setState(NORMAL_STATE);
18284
							loopPoint.firePointEvent('unselect');
18285
					}
18286
				});
18287
			}
18288
		});
18289
	},
18290
 
18291
	/**
18292
	 * Runs on mouse over the point
18293
	 *
18294
	 * @param {Object} e The event arguments
18295
	 * @param {Boolean} byProximity Falsy for kd points that are closest to the mouse, or to
18296
	 *        actually hovered points. True for other points in shared tooltip.
18297
	 */
18298
	onMouseOver: function (e, byProximity) {
18299
		var point = this,
18300
			series = point.series,
18301
			chart = series.chart,
18302
			tooltip = chart.tooltip,
18303
			hoverPoint = chart.hoverPoint;
18304
 
18305
		if (chart.hoverSeries !== series) {
18306
			series.onMouseOver();
18307
		}
18308
 
18309
		// set normal state to previous series
18310
		if (hoverPoint && hoverPoint !== point) {
18311
			hoverPoint.onMouseOut();
18312
		}
18313
 
18314
		if (point.series) { // It may have been destroyed, #4130
18315
 
18316
			// trigger the event
18317
			point.firePointEvent('mouseOver');
18318
 
18319
			// update the tooltip
18320
			if (tooltip && (!tooltip.shared || series.noSharedTooltip)) {
18321
				tooltip.refresh(point, e);
18322
			}
18323
 
18324
			// hover this
18325
			point.setState(HOVER_STATE);
18326
			if (!byProximity) {
18327
				chart.hoverPoint = point;
18328
			}
18329
		}
18330
	},
18331
 
18332
	/**
18333
	 * Runs on mouse out from the point
18334
	 */
18335
	onMouseOut: function () {
18336
		var chart = this.series.chart,
18337
			hoverPoints = chart.hoverPoints;
18338
 
18339
		this.firePointEvent('mouseOut');
18340
 
18341
		if (!hoverPoints || inArray(this, hoverPoints) === -1) { // #887, #2240
18342
			this.setState();
18343
			chart.hoverPoint = null;
18344
		}
18345
	},
18346
 
18347
	/**
18348
	 * Import events from the series' and point's options. Only do it on
18349
	 * demand, to save processing time on hovering.
18350
	 */
18351
	importEvents: function () {
18352
		if (!this.hasImportedEvents) {
18353
			var point = this,
18354
				options = merge(point.series.options.point, point.options),
18355
				events = options.events,
18356
				eventType;
18357
 
18358
			point.events = events;
18359
 
18360
			for (eventType in events) {
18361
				addEvent(point, eventType, events[eventType]);
18362
			}
18363
			this.hasImportedEvents = true;
18364
 
18365
		}
18366
	},
18367
 
18368
	/**
18369
	 * Set the point's state
18370
	 * @param {String} state
18371
	 */
18372
	setState: function (state, move) {
18373
		var point = this,
18374
			plotX = point.plotX,
18375
			plotY = point.plotY,
18376
			series = point.series,
18377
			stateOptions = series.options.states,
18378
			markerOptions = defaultPlotOptions[series.type].marker && series.options.marker,
18379
			normalDisabled = markerOptions && !markerOptions.enabled,
18380
			markerStateOptions = markerOptions && markerOptions.states[state],
18381
			stateDisabled = markerStateOptions && markerStateOptions.enabled === false,
18382
			stateMarkerGraphic = series.stateMarkerGraphic,
18383
			pointMarker = point.marker || {},
18384
			chart = series.chart,
18385
			radius,
18386
			halo = series.halo,
18387
			haloOptions,
18388
			newSymbol,
18389
			pointAttr;
18390
 
18391
		state = state || NORMAL_STATE; // empty string
18392
		pointAttr = point.pointAttr[state] || series.pointAttr[state];
18393
 
18394
		if (
18395
				// already has this state
18396
				(state === point.state && !move) ||
18397
				// selected points don't respond to hover
18398
				(point.selected && state !== SELECT_STATE) ||
18399
				// series' state options is disabled
18400
				(stateOptions[state] && stateOptions[state].enabled === false) ||
18401
				// general point marker's state options is disabled
18402
				(state && (stateDisabled || (normalDisabled && markerStateOptions.enabled === false))) ||
18403
				// individual point marker's state options is disabled
18404
				(state && pointMarker.states && pointMarker.states[state] && pointMarker.states[state].enabled === false) // #1610
18405
 
18406
			) {
18407
			return;
18408
		}
18409
 
18410
		// apply hover styles to the existing point
18411
		if (point.graphic) {
18412
			radius = markerOptions && point.graphic.symbolName && pointAttr.r;
18413
			point.graphic.attr(merge(
18414
				pointAttr,
18415
				radius ? { // new symbol attributes (#507, #612)
18416
					x: plotX - radius,
18417
					y: plotY - radius,
18418
					width: 2 * radius,
18419
					height: 2 * radius
18420
				} : {}
18421
			));
18422
 
18423
			// Zooming in from a range with no markers to a range with markers
18424
			if (stateMarkerGraphic) {
18425
				stateMarkerGraphic.hide();
18426
			}
18427
		} else {
18428
			// if a graphic is not applied to each point in the normal state, create a shared
18429
			// graphic for the hover state
18430
			if (state && markerStateOptions) {
18431
				radius = markerStateOptions.radius;
18432
				newSymbol = pointMarker.symbol || series.symbol;
18433
 
18434
				// If the point has another symbol than the previous one, throw away the
18435
				// state marker graphic and force a new one (#1459)
18436
				if (stateMarkerGraphic && stateMarkerGraphic.currentSymbol !== newSymbol) {
18437
					stateMarkerGraphic = stateMarkerGraphic.destroy();
18438
				}
18439
 
18440
				// Add a new state marker graphic
18441
				if (!stateMarkerGraphic) {
18442
					if (newSymbol) {
18443
						series.stateMarkerGraphic = stateMarkerGraphic = chart.renderer.symbol(
18444
							newSymbol,
18445
							plotX - radius,
18446
							plotY - radius,
18447
							2 * radius,
18448
							2 * radius
18449
						)
18450
						.attr(pointAttr)
18451
						.add(series.markerGroup);
18452
						stateMarkerGraphic.currentSymbol = newSymbol;
18453
					}
18454
 
18455
				// Move the existing graphic
18456
				} else {
18457
					stateMarkerGraphic[move ? 'animate' : 'attr']({ // #1054
18458
						x: plotX - radius,
18459
						y: plotY - radius
18460
					});
18461
				}
18462
			}
18463
 
18464
			if (stateMarkerGraphic) {
18465
				stateMarkerGraphic[state && chart.isInsidePlot(plotX, plotY, chart.inverted) ? 'show' : 'hide'](); // #2450
18466
				stateMarkerGraphic.element.point = point; // #4310
18467
			}
18468
		}
18469
 
18470
		// Show me your halo
18471
		haloOptions = stateOptions[state] && stateOptions[state].halo;
18472
		if (haloOptions && haloOptions.size) {
18473
			if (!halo) {
18474
				series.halo = halo = chart.renderer.path()
18475
					.add(chart.seriesGroup);
18476
			}
18477
			halo.attr(extend({
18478
				fill: Color(point.color || series.color).setOpacity(haloOptions.opacity).get()
18479
			}, haloOptions.attributes))[move ? 'animate' : 'attr']({
18480
				d: point.haloPath(haloOptions.size)
18481
			});
18482
		} else if (halo) {
18483
			halo.attr({ d: [] });
18484
		}
18485
 
18486
		point.state = state;
18487
	},
18488
 
18489
	haloPath: function (size) {
18490
		var series = this.series,
18491
			chart = series.chart,
18492
			plotBox = series.getPlotBox(),
18493
			inverted = chart.inverted;
18494
 
18495
		return chart.renderer.symbols.circle(
18496
			plotBox.translateX + (inverted ? series.yAxis.len - this.plotY : this.plotX) - size,
18497
			plotBox.translateY + (inverted ? series.xAxis.len - this.plotX : this.plotY) - size,
18498
			size * 2,
18499
			size * 2
18500
		);
18501
	}
18502
});
18503
 
18504
/*
18505
 * Extend the Series object with interaction
18506
 */
18507
 
18508
extend(Series.prototype, {
18509
	/**
18510
	 * Series mouse over handler
18511
	 */
18512
	onMouseOver: function () {
18513
		var series = this,
18514
			chart = series.chart,
18515
			hoverSeries = chart.hoverSeries;
18516
 
18517
		// set normal state to previous series
18518
		if (hoverSeries && hoverSeries !== series) {
18519
			hoverSeries.onMouseOut();
18520
		}
18521
 
18522
		// trigger the event, but to save processing time,
18523
		// only if defined
18524
		if (series.options.events.mouseOver) {
18525
			fireEvent(series, 'mouseOver');
18526
		}
18527
 
18528
		// hover this
18529
		series.setState(HOVER_STATE);
18530
		chart.hoverSeries = series;
18531
	},
18532
 
18533
	/**
18534
	 * Series mouse out handler
18535
	 */
18536
	onMouseOut: function () {
18537
		// trigger the event only if listeners exist
18538
		var series = this,
18539
			options = series.options,
18540
			chart = series.chart,
18541
			tooltip = chart.tooltip,
18542
			hoverPoint = chart.hoverPoint;
18543
 
18544
		chart.hoverSeries = null; // #182, set to null before the mouseOut event fires
18545
 
18546
		// trigger mouse out on the point, which must be in this series
18547
		if (hoverPoint) {
18548
			hoverPoint.onMouseOut();
18549
		}
18550
 
18551
		// fire the mouse out event
18552
		if (series && options.events.mouseOut) {
18553
			fireEvent(series, 'mouseOut');
18554
		}
18555
 
18556
 
18557
		// hide the tooltip
18558
		if (tooltip && !options.stickyTracking && (!tooltip.shared || series.noSharedTooltip)) {
18559
			tooltip.hide();
18560
		}
18561
 
18562
		// set normal state
18563
		series.setState();
18564
	},
18565
 
18566
	/**
18567
	 * Set the state of the graph
18568
	 */
18569
	setState: function (state) {
18570
		var series = this,
18571
			options = series.options,
18572
			graph = series.graph,
18573
			stateOptions = options.states,
18574
			lineWidth = options.lineWidth,
18575
			attribs,
18576
			i = 0;
18577
 
18578
		state = state || NORMAL_STATE;
18579
 
18580
		if (series.state !== state) {
18581
			series.state = state;
18582
 
18583
			if (stateOptions[state] && stateOptions[state].enabled === false) {
18584
				return;
18585
			}
18586
 
18587
			if (state) {
18588
				lineWidth = stateOptions[state].lineWidth || lineWidth + (stateOptions[state].lineWidthPlus || 0); // #4035
18589
			}
18590
 
18591
			if (graph && !graph.dashstyle) { // hover is turned off for dashed lines in VML
18592
				attribs = {
18593
					'stroke-width': lineWidth
18594
				};
18595
				// use attr because animate will cause any other animation on the graph to stop
18596
				graph.attr(attribs);
18597
				while (series['zoneGraph' + i]) {
18598
					series['zoneGraph' + i].attr(attribs);
18599
					i = i + 1;
18600
				}
18601
			}
18602
		}
18603
	},
18604
 
18605
	/**
18606
	 * Set the visibility of the graph
18607
	 *
18608
	 * @param vis {Boolean} True to show the series, false to hide. If UNDEFINED,
18609
	 *				the visibility is toggled.
18610
	 */
18611
	setVisible: function (vis, redraw) {
18612
		var series = this,
18613
			chart = series.chart,
18614
			legendItem = series.legendItem,
18615
			showOrHide,
18616
			ignoreHiddenSeries = chart.options.chart.ignoreHiddenSeries,
18617
			oldVisibility = series.visible;
18618
 
18619
		// if called without an argument, toggle visibility
18620
		series.visible = vis = series.userOptions.visible = vis === UNDEFINED ? !oldVisibility : vis;
18621
		showOrHide = vis ? 'show' : 'hide';
18622
 
18623
		// show or hide elements
18624
		each(['group', 'dataLabelsGroup', 'markerGroup', 'tracker'], function (key) {
18625
			if (series[key]) {
18626
				series[key][showOrHide]();
18627
			}
18628
		});
18629
 
18630
 
18631
		// hide tooltip (#1361)
18632
		if (chart.hoverSeries === series || (chart.hoverPoint && chart.hoverPoint.series) === series) {
18633
			series.onMouseOut();
18634
		}
18635
 
18636
 
18637
		if (legendItem) {
18638
			chart.legend.colorizeItem(series, vis);
18639
		}
18640
 
18641
 
18642
		// rescale or adapt to resized chart
18643
		series.isDirty = true;
18644
		// in a stack, all other series are affected
18645
		if (series.options.stacking) {
18646
			each(chart.series, function (otherSeries) {
18647
				if (otherSeries.options.stacking && otherSeries.visible) {
18648
					otherSeries.isDirty = true;
18649
				}
18650
			});
18651
		}
18652
 
18653
		// show or hide linked series
18654
		each(series.linkedSeries, function (otherSeries) {
18655
			otherSeries.setVisible(vis, false);
18656
		});
18657
 
18658
		if (ignoreHiddenSeries) {
18659
			chart.isDirtyBox = true;
18660
		}
18661
		if (redraw !== false) {
18662
			chart.redraw();
18663
		}
18664
 
18665
		fireEvent(series, showOrHide);
18666
	},
18667
 
18668
	/**
18669
	 * Show the graph
18670
	 */
18671
	show: function () {
18672
		this.setVisible(true);
18673
	},
18674
 
18675
	/**
18676
	 * Hide the graph
18677
	 */
18678
	hide: function () {
18679
		this.setVisible(false);
18680
	},
18681
 
18682
 
18683
	/**
18684
	 * Set the selected state of the graph
18685
	 *
18686
	 * @param selected {Boolean} True to select the series, false to unselect. If
18687
	 *				UNDEFINED, the selection state is toggled.
18688
	 */
18689
	select: function (selected) {
18690
		var series = this;
18691
		// if called without an argument, toggle
18692
		series.selected = selected = (selected === UNDEFINED) ? !series.selected : selected;
18693
 
18694
		if (series.checkbox) {
18695
			series.checkbox.checked = selected;
18696
		}
18697
 
18698
		fireEvent(series, selected ? 'select' : 'unselect');
18699
	},
18700
 
18701
	drawTracker: TrackerMixin.drawTrackerGraph
18702
});/* ****************************************************************************
18703
 * Start ordinal axis logic                                                   *
18704
 *****************************************************************************/
18705
 
18706
 
18707
wrap(Series.prototype, 'init', function (proceed) {
18708
	var series = this,
18709
		xAxis;
18710
 
18711
	// call the original function
18712
	proceed.apply(this, Array.prototype.slice.call(arguments, 1));
18713
 
18714
	xAxis = series.xAxis;
18715
 
18716
	// Destroy the extended ordinal index on updated data
18717
	if (xAxis && xAxis.options.ordinal) {
18718
		addEvent(series, 'updatedData', function () {
18719
			delete xAxis.ordinalIndex;
18720
		});
18721
	}
18722
});
18723
 
18724
/**
18725
 * In an ordinal axis, there might be areas with dense consentrations of points, then large
18726
 * gaps between some. Creating equally distributed ticks over this entire range
18727
 * may lead to a huge number of ticks that will later be removed. So instead, break the
18728
 * positions up in segments, find the tick positions for each segment then concatenize them.
18729
 * This method is used from both data grouping logic and X axis tick position logic.
18730
 */
18731
wrap(Axis.prototype, 'getTimeTicks', function (proceed, normalizedInterval, min, max, startOfWeek, positions, closestDistance, findHigherRanks) {
18732
 
18733
	var start = 0,
18734
		end = 0,
18735
		segmentPositions,
18736
		higherRanks = {},
18737
		hasCrossedHigherRank,
18738
		info,
18739
		posLength,
18740
		outsideMax,
18741
		groupPositions = [],
18742
		lastGroupPosition = -Number.MAX_VALUE,
18743
		tickPixelIntervalOption = this.options.tickPixelInterval;
18744
 
18745
	// The positions are not always defined, for example for ordinal positions when data
18746
	// has regular interval (#1557, #2090)
18747
	if ((!this.options.ordinal && !this.options.breaks) || !positions || positions.length < 3 || min === UNDEFINED) {
18748
		return proceed.call(this, normalizedInterval, min, max, startOfWeek);
18749
	}
18750
 
18751
	// Analyze the positions array to split it into segments on gaps larger than 5 times
18752
	// the closest distance. The closest distance is already found at this point, so
18753
	// we reuse that instead of computing it again.
18754
	posLength = positions.length;
18755
 
18756
	for (; end < posLength; end++) {
18757
 
18758
		outsideMax = end && positions[end - 1] > max;
18759
 
18760
		if (positions[end] < min) { // Set the last position before min
18761
			start = end;
18762
		}
18763
 
18764
		if (end === posLength - 1 || positions[end + 1] - positions[end] > closestDistance * 5 || outsideMax) {
18765
 
18766
			// For each segment, calculate the tick positions from the getTimeTicks utility
18767
			// function. The interval will be the same regardless of how long the segment is.
18768
			if (positions[end] > lastGroupPosition) { // #1475
18769
 
18770
				segmentPositions = proceed.call(this, normalizedInterval, positions[start], positions[end], startOfWeek);
18771
 
18772
				// Prevent duplicate groups, for example for multiple segments within one larger time frame (#1475)
18773
				while (segmentPositions.length && segmentPositions[0] <= lastGroupPosition) {
18774
					segmentPositions.shift();
18775
				}
18776
				if (segmentPositions.length) {
18777
					lastGroupPosition = segmentPositions[segmentPositions.length - 1];
18778
				}
18779
 
18780
				groupPositions = groupPositions.concat(segmentPositions);
18781
			}
18782
			// Set start of next segment
18783
			start = end + 1;
18784
		}
18785
 
18786
		if (outsideMax) {
18787
			break;
18788
		}
18789
	}
18790
 
18791
	// Get the grouping info from the last of the segments. The info is the same for
18792
	// all segments.
18793
	info = segmentPositions.info;
18794
 
18795
	// Optionally identify ticks with higher rank, for example when the ticks
18796
	// have crossed midnight.
18797
	if (findHigherRanks && info.unitRange <= timeUnits.hour) {
18798
		end = groupPositions.length - 1;
18799
 
18800
		// Compare points two by two
18801
		for (start = 1; start < end; start++) {
18802
			if (dateFormat('%d', groupPositions[start]) !== dateFormat('%d', groupPositions[start - 1])) {
18803
				higherRanks[groupPositions[start]] = 'day';
18804
				hasCrossedHigherRank = true;
18805
			}
18806
		}
18807
 
18808
		// If the complete array has crossed midnight, we want to mark the first
18809
		// positions also as higher rank
18810
		if (hasCrossedHigherRank) {
18811
			higherRanks[groupPositions[0]] = 'day';
18812
		}
18813
		info.higherRanks = higherRanks;
18814
	}
18815
 
18816
	// Save the info
18817
	groupPositions.info = info;
18818
 
18819
 
18820
 
18821
	// Don't show ticks within a gap in the ordinal axis, where the space between
18822
	// two points is greater than a portion of the tick pixel interval
18823
	if (findHigherRanks && defined(tickPixelIntervalOption)) { // check for squashed ticks
18824
 
18825
		var length = groupPositions.length,
18826
			i = length,
18827
			itemToRemove,
18828
			translated,
18829
			translatedArr = [],
18830
			lastTranslated,
18831
			medianDistance,
18832
			distance,
18833
			distances = [];
18834
 
18835
		// Find median pixel distance in order to keep a reasonably even distance between
18836
		// ticks (#748)
18837
		while (i--) {
18838
			translated = this.translate(groupPositions[i]);
18839
			if (lastTranslated) {
18840
				distances[i] = lastTranslated - translated;
18841
			}
18842
			translatedArr[i] = lastTranslated = translated;
18843
		}
18844
		distances.sort();
18845
		medianDistance = distances[mathFloor(distances.length / 2)];
18846
		if (medianDistance < tickPixelIntervalOption * 0.6) {
18847
			medianDistance = null;
18848
		}
18849
 
18850
		// Now loop over again and remove ticks where needed
18851
		i = groupPositions[length - 1] > max ? length - 1 : length; // #817
18852
		lastTranslated = undefined;
18853
		while (i--) {
18854
			translated = translatedArr[i];
18855
			distance = lastTranslated - translated;
18856
 
18857
			// Remove ticks that are closer than 0.6 times the pixel interval from the one to the right,
18858
			// but not if it is close to the median distance (#748).
18859
			if (lastTranslated && distance < tickPixelIntervalOption * 0.8 &&
18860
					(medianDistance === null || distance < medianDistance * 0.8)) {
18861
 
18862
				// Is this a higher ranked position with a normal position to the right?
18863
				if (higherRanks[groupPositions[i]] && !higherRanks[groupPositions[i + 1]]) {
18864
 
18865
					// Yes: remove the lower ranked neighbour to the right
18866
					itemToRemove = i + 1;
18867
					lastTranslated = translated; // #709
18868
 
18869
				} else {
18870
 
18871
					// No: remove this one
18872
					itemToRemove = i;
18873
				}
18874
 
18875
				groupPositions.splice(itemToRemove, 1);
18876
 
18877
			} else {
18878
				lastTranslated = translated;
18879
			}
18880
		}
18881
	}
18882
	return groupPositions;
18883
});
18884
 
18885
// Extend the Axis prototype
18886
extend(Axis.prototype, {
18887
 
18888
	/**
18889
	 * Calculate the ordinal positions before tick positions are calculated.
18890
	 */
18891
	beforeSetTickPositions: function () {
18892
		var axis = this,
18893
			len,
18894
			ordinalPositions = [],
18895
			useOrdinal = false,
18896
			dist,
18897
			extremes = axis.getExtremes(),
18898
			min = extremes.min,
18899
			max = extremes.max,
18900
			minIndex,
18901
			maxIndex,
18902
			slope,
18903
			hasBreaks = axis.isXAxis && !!axis.options.breaks,
18904
			isOrdinal = axis.options.ordinal,
18905
			i;
18906
 
18907
		// apply the ordinal logic
18908
		if (isOrdinal || hasBreaks) { // #4167 YAxis is never ordinal ?
18909
 
18910
			each(axis.series, function (series, i) {
18911
 
18912
				if (series.visible !== false && (series.takeOrdinalPosition !== false || hasBreaks)) {
18913
 
18914
					// concatenate the processed X data into the existing positions, or the empty array
18915
					ordinalPositions = ordinalPositions.concat(series.processedXData);
18916
					len = ordinalPositions.length;
18917
 
18918
					// remove duplicates (#1588)
18919
					ordinalPositions.sort(function (a, b) {
18920
						return a - b; // without a custom function it is sorted as strings
18921
					});
18922
 
18923
					if (len) {
18924
						i = len - 1;
18925
						while (i--) {
18926
							if (ordinalPositions[i] === ordinalPositions[i + 1]) {
18927
								ordinalPositions.splice(i, 1);
18928
							}
18929
						}
18930
					}
18931
				}
18932
 
18933
			});
18934
 
18935
			// cache the length
18936
			len = ordinalPositions.length;
18937
 
18938
			// Check if we really need the overhead of mapping axis data against the ordinal positions.
18939
			// If the series consist of evenly spaced data any way, we don't need any ordinal logic.
18940
			if (len > 2) { // two points have equal distance by default
18941
				dist = ordinalPositions[1] - ordinalPositions[0];
18942
				i = len - 1;
18943
				while (i-- && !useOrdinal) {
18944
					if (ordinalPositions[i + 1] - ordinalPositions[i] !== dist) {
18945
						useOrdinal = true;
18946
					}
18947
				}
18948
 
18949
				// When zooming in on a week, prevent axis padding for weekends even though the data within
18950
				// the week is evenly spaced.
18951
				if (!axis.options.keepOrdinalPadding && (ordinalPositions[0] - min > dist || max - ordinalPositions[ordinalPositions.length - 1] > dist)) {
18952
					useOrdinal = true;
18953
				}
18954
			}
18955
 
18956
			// Record the slope and offset to compute the linear values from the array index.
18957
			// Since the ordinal positions may exceed the current range, get the start and
18958
			// end positions within it (#719, #665b)
18959
			if (useOrdinal) {
18960
 
18961
				// Register
18962
				axis.ordinalPositions = ordinalPositions;
18963
 
18964
				// This relies on the ordinalPositions being set. Use mathMax and mathMin to prevent
18965
				// padding on either sides of the data.
18966
				minIndex = axis.val2lin(mathMax(min, ordinalPositions[0]), true);
18967
				maxIndex = mathMax(axis.val2lin(mathMin(max, ordinalPositions[ordinalPositions.length - 1]), true), 1); // #3339
18968
 
18969
				// Set the slope and offset of the values compared to the indices in the ordinal positions
18970
				axis.ordinalSlope = slope = (max - min) / (maxIndex - minIndex);
18971
				axis.ordinalOffset = min - (minIndex * slope);
18972
 
18973
			} else {
18974
				axis.ordinalPositions = axis.ordinalSlope = axis.ordinalOffset = UNDEFINED;
18975
			}
18976
		}
18977
		axis.doPostTranslate = (isOrdinal && useOrdinal) || hasBreaks; // #3818, #4196
18978
		axis.groupIntervalFactor = null; // reset for next run
18979
	},
18980
	/**
18981
	 * Translate from a linear axis value to the corresponding ordinal axis position. If there
18982
	 * are no gaps in the ordinal axis this will be the same. The translated value is the value
18983
	 * that the point would have if the axis were linear, using the same min and max.
18984
	 *
18985
	 * @param Number val The axis value
18986
	 * @param Boolean toIndex Whether to return the index in the ordinalPositions or the new value
18987
	 */
18988
	val2lin: function (val, toIndex) {
18989
		var axis = this,
18990
			ordinalPositions = axis.ordinalPositions;
18991
 
18992
		if (!ordinalPositions) {
18993
			return val;
18994
 
18995
		} else {
18996
 
18997
			var ordinalLength = ordinalPositions.length,
18998
				i,
18999
				distance,
19000
				ordinalIndex;
19001
 
19002
			// first look for an exact match in the ordinalpositions array
19003
			i = ordinalLength;
19004
			while (i--) {
19005
				if (ordinalPositions[i] === val) {
19006
					ordinalIndex = i;
19007
					break;
19008
				}
19009
			}
19010
 
19011
			// if that failed, find the intermediate position between the two nearest values
19012
			i = ordinalLength - 1;
19013
			while (i--) {
19014
				if (val > ordinalPositions[i] || i === 0) { // interpolate
19015
					distance = (val - ordinalPositions[i]) / (ordinalPositions[i + 1] - ordinalPositions[i]); // something between 0 and 1
19016
					ordinalIndex = i + distance;
19017
					break;
19018
				}
19019
			}
19020
			return toIndex ?
19021
				ordinalIndex :
19022
				axis.ordinalSlope * (ordinalIndex || 0) + axis.ordinalOffset;
19023
		}
19024
	},
19025
	/**
19026
	 * Translate from linear (internal) to axis value
19027
	 *
19028
	 * @param Number val The linear abstracted value
19029
	 * @param Boolean fromIndex Translate from an index in the ordinal positions rather than a value
19030
	 */
19031
	lin2val: function (val, fromIndex) {
19032
		var axis = this,
19033
			ordinalPositions = axis.ordinalPositions;
19034
 
19035
		if (!ordinalPositions) { // the visible range contains only equally spaced values
19036
			return val;
19037
 
19038
		} else {
19039
 
19040
			var ordinalSlope = axis.ordinalSlope,
19041
				ordinalOffset = axis.ordinalOffset,
19042
				i = ordinalPositions.length - 1,
19043
				linearEquivalentLeft,
19044
				linearEquivalentRight,
19045
				distance;
19046
 
19047
 
19048
			// Handle the case where we translate from the index directly, used only
19049
			// when panning an ordinal axis
19050
			if (fromIndex) {
19051
 
19052
				if (val < 0) { // out of range, in effect panning to the left
19053
					val = ordinalPositions[0];
19054
				} else if (val > i) { // out of range, panning to the right
19055
					val = ordinalPositions[i];
19056
				} else { // split it up
19057
					i = mathFloor(val);
19058
					distance = val - i; // the decimal
19059
				}
19060
 
19061
			// Loop down along the ordinal positions. When the linear equivalent of i matches
19062
			// an ordinal position, interpolate between the left and right values.
19063
			} else {
19064
				while (i--) {
19065
					linearEquivalentLeft = (ordinalSlope * i) + ordinalOffset;
19066
					if (val >= linearEquivalentLeft) {
19067
						linearEquivalentRight = (ordinalSlope * (i + 1)) + ordinalOffset;
19068
						distance = (val - linearEquivalentLeft) / (linearEquivalentRight - linearEquivalentLeft); // something between 0 and 1
19069
						break;
19070
					}
19071
				}
19072
			}
19073
 
19074
			// If the index is within the range of the ordinal positions, return the associated
19075
			// or interpolated value. If not, just return the value
19076
			return distance !== UNDEFINED && ordinalPositions[i] !== UNDEFINED ?
19077
				ordinalPositions[i] + (distance ? distance * (ordinalPositions[i + 1] - ordinalPositions[i]) : 0) :
19078
				val;
19079
		}
19080
	},
19081
	/**
19082
	 * Get the ordinal positions for the entire data set. This is necessary in chart panning
19083
	 * because we need to find out what points or data groups are available outside the
19084
	 * visible range. When a panning operation starts, if an index for the given grouping
19085
	 * does not exists, it is created and cached. This index is deleted on updated data, so
19086
	 * it will be regenerated the next time a panning operation starts.
19087
	 */
19088
	getExtendedPositions: function () {
19089
		var axis = this,
19090
			chart = axis.chart,
19091
			grouping = axis.series[0].currentDataGrouping,
19092
			ordinalIndex = axis.ordinalIndex,
19093
			key = grouping ? grouping.count + grouping.unitName : 'raw',
19094
			extremes = axis.getExtremes(),
19095
			fakeAxis,
19096
			fakeSeries;
19097
 
19098
		// If this is the first time, or the ordinal index is deleted by updatedData,
19099
		// create it.
19100
		if (!ordinalIndex) {
19101
			ordinalIndex = axis.ordinalIndex = {};
19102
		}
19103
 
19104
 
19105
		if (!ordinalIndex[key]) {
19106
 
19107
			// Create a fake axis object where the extended ordinal positions are emulated
19108
			fakeAxis = {
19109
				series: [],
19110
				getExtremes: function () {
19111
					return {
19112
						min: extremes.dataMin,
19113
						max: extremes.dataMax
19114
					};
19115
				},
19116
				options: {
19117
					ordinal: true
19118
				},
19119
				val2lin: Axis.prototype.val2lin // #2590
19120
			};
19121
 
19122
			// Add the fake series to hold the full data, then apply processData to it
19123
			each(axis.series, function (series) {
19124
				fakeSeries = {
19125
					xAxis: fakeAxis,
19126
					xData: series.xData,
19127
					chart: chart,
19128
					destroyGroupedData: noop
19129
				};
19130
				fakeSeries.options = {
19131
					dataGrouping : grouping ? {
19132
						enabled: true,
19133
						forced: true,
19134
						approximation: 'open', // doesn't matter which, use the fastest
19135
						units: [[grouping.unitName, [grouping.count]]]
19136
					} : {
19137
						enabled: false
19138
					}
19139
				};
19140
				series.processData.apply(fakeSeries);
19141
 
19142
				fakeAxis.series.push(fakeSeries);
19143
			});
19144
 
19145
			// Run beforeSetTickPositions to compute the ordinalPositions
19146
			axis.beforeSetTickPositions.apply(fakeAxis);
19147
 
19148
			// Cache it
19149
			ordinalIndex[key] = fakeAxis.ordinalPositions;
19150
		}
19151
		return ordinalIndex[key];
19152
	},
19153
 
19154
	/**
19155
	 * Find the factor to estimate how wide the plot area would have been if ordinal
19156
	 * gaps were included. This value is used to compute an imagined plot width in order
19157
	 * to establish the data grouping interval.
19158
	 *
19159
	 * A real world case is the intraday-candlestick
19160
	 * example. Without this logic, it would show the correct data grouping when viewing
19161
	 * a range within each day, but once moving the range to include the gap between two
19162
	 * days, the interval would include the cut-away night hours and the data grouping
19163
	 * would be wrong. So the below method tries to compensate by identifying the most
19164
	 * common point interval, in this case days.
19165
	 *
19166
	 * An opposite case is presented in issue #718. We have a long array of daily data,
19167
	 * then one point is appended one hour after the last point. We expect the data grouping
19168
	 * not to change.
19169
	 *
19170
	 * In the future, if we find cases where this estimation doesn't work optimally, we
19171
	 * might need to add a second pass to the data grouping logic, where we do another run
19172
	 * with a greater interval if the number of data groups is more than a certain fraction
19173
	 * of the desired group count.
19174
	 */
19175
	getGroupIntervalFactor: function (xMin, xMax, series) {
19176
		var i = 0,
19177
			processedXData = series.processedXData,
19178
			len = processedXData.length,
19179
			distances = [],
19180
			median,
19181
			groupIntervalFactor = this.groupIntervalFactor;
19182
 
19183
		// Only do this computation for the first series, let the other inherit it (#2416)
19184
		if (!groupIntervalFactor) {
19185
 
19186
			// Register all the distances in an array
19187
			for (; i < len - 1; i++) {
19188
				distances[i] = processedXData[i + 1] - processedXData[i];
19189
			}
19190
 
19191
			// Sort them and find the median
19192
			distances.sort(function (a, b) {
19193
					return a - b;
19194
			});
19195
			median = distances[mathFloor(len / 2)];
19196
 
19197
			// Compensate for series that don't extend through the entire axis extent. #1675.
19198
			xMin = mathMax(xMin, processedXData[0]);
19199
			xMax = mathMin(xMax, processedXData[len - 1]);
19200
 
19201
			this.groupIntervalFactor = groupIntervalFactor = (len * median) / (xMax - xMin);
19202
		}
19203
 
19204
		// Return the factor needed for data grouping
19205
		return groupIntervalFactor;
19206
	},
19207
 
19208
	/**
19209
	 * Make the tick intervals closer because the ordinal gaps make the ticks spread out or cluster
19210
	 */
19211
	postProcessTickInterval: function (tickInterval) {
19212
		// TODO: http://jsfiddle.net/highcharts/FQm4E/1/
19213
		// This is a case where this algorithm doesn't work optimally. In this case, the
19214
		// tick labels are spread out per week, but all the gaps reside within weeks. So
19215
		// we have a situation where the labels are courser than the ordinal gaps, and
19216
		// thus the tick interval should not be altered
19217
		var ordinalSlope = this.ordinalSlope;
19218
 
19219
 
19220
		if (ordinalSlope) {
19221
			if (!this.options.breaks) {
19222
				return tickInterval / (ordinalSlope / this.closestPointRange);
19223
			} else {
19224
				return this.closestPointRange;
19225
			}
19226
		} else {
19227
			return tickInterval;
19228
		}
19229
	}
19230
});
19231
 
19232
// Extending the Chart.pan method for ordinal axes
19233
wrap(Chart.prototype, 'pan', function (proceed, e) {
19234
	var chart = this,
19235
		xAxis = chart.xAxis[0],
19236
		chartX = e.chartX,
19237
		runBase = false;
19238
 
19239
	if (xAxis.options.ordinal && xAxis.series.length) {
19240
 
19241
		var mouseDownX = chart.mouseDownX,
19242
			extremes = xAxis.getExtremes(),
19243
			dataMax = extremes.dataMax,
19244
			min = extremes.min,
19245
			max = extremes.max,
19246
			trimmedRange,
19247
			hoverPoints = chart.hoverPoints,
19248
			closestPointRange = xAxis.closestPointRange,
19249
			pointPixelWidth = xAxis.translationSlope * (xAxis.ordinalSlope || closestPointRange),
19250
			movedUnits = (mouseDownX - chartX) / pointPixelWidth, // how many ordinal units did we move?
19251
			extendedAxis = { ordinalPositions: xAxis.getExtendedPositions() }, // get index of all the chart's points
19252
			ordinalPositions,
19253
			searchAxisLeft,
19254
			lin2val = xAxis.lin2val,
19255
			val2lin = xAxis.val2lin,
19256
			searchAxisRight;
19257
 
19258
		if (!extendedAxis.ordinalPositions) { // we have an ordinal axis, but the data is equally spaced
19259
			runBase = true;
19260
 
19261
		} else if (mathAbs(movedUnits) > 1) {
19262
 
19263
			// Remove active points for shared tooltip
19264
			if (hoverPoints) {
19265
				each(hoverPoints, function (point) {
19266
					point.setState();
19267
				});
19268
			}
19269
 
19270
			if (movedUnits < 0) {
19271
				searchAxisLeft = extendedAxis;
19272
				searchAxisRight = xAxis.ordinalPositions ? xAxis : extendedAxis;
19273
			} else {
19274
				searchAxisLeft = xAxis.ordinalPositions ? xAxis : extendedAxis;
19275
				searchAxisRight = extendedAxis;
19276
			}
19277
 
19278
			// In grouped data series, the last ordinal position represents the grouped data, which is
19279
			// to the left of the real data max. If we don't compensate for this, we will be allowed
19280
			// to pan grouped data series passed the right of the plot area.
19281
			ordinalPositions = searchAxisRight.ordinalPositions;
19282
			if (dataMax > ordinalPositions[ordinalPositions.length - 1]) {
19283
				ordinalPositions.push(dataMax);
19284
			}
19285
 
19286
			// Get the new min and max values by getting the ordinal index for the current extreme,
19287
			// then add the moved units and translate back to values. This happens on the
19288
			// extended ordinal positions if the new position is out of range, else it happens
19289
			// on the current x axis which is smaller and faster.
19290
			chart.fixedRange = max - min;
19291
			trimmedRange = xAxis.toFixedRange(null, null,
19292
				lin2val.apply(searchAxisLeft, [
19293
					val2lin.apply(searchAxisLeft, [min, true]) + movedUnits, // the new index
19294
					true // translate from index
19295
				]),
19296
				lin2val.apply(searchAxisRight, [
19297
					val2lin.apply(searchAxisRight, [max, true]) + movedUnits, // the new index
19298
					true // translate from index
19299
				])
19300
			);
19301
 
19302
			// Apply it if it is within the available data range
19303
			if (trimmedRange.min >= mathMin(extremes.dataMin, min) && trimmedRange.max <= mathMax(dataMax, max)) {
19304
				xAxis.setExtremes(trimmedRange.min, trimmedRange.max, true, false, { trigger: 'pan' });
19305
			}
19306
 
19307
			chart.mouseDownX = chartX; // set new reference for next run
19308
			css(chart.container, { cursor: 'move' });
19309
		}
19310
 
19311
	} else {
19312
		runBase = true;
19313
	}
19314
 
19315
	// revert to the linear chart.pan version
19316
	if (runBase) {
19317
		// call the original function
19318
		proceed.apply(this, Array.prototype.slice.call(arguments, 1));
19319
	}
19320
});
19321
 
19322
 
19323
 
19324
/**
19325
 * Extend getSegments by identifying gaps in the ordinal data so that we can draw a gap in the
19326
 * line or area
19327
 */
19328
wrap(Series.prototype, 'getSegments', function (proceed) {
19329
 
19330
	var series = this,
19331
		segments,
19332
		gapSize = series.options.gapSize,
19333
		xAxis = series.xAxis;
19334
 
19335
	// call base method
19336
	proceed.apply(this, Array.prototype.slice.call(arguments, 1));
19337
 
19338
	if (gapSize) {
19339
 
19340
		// properties
19341
		segments = series.segments;
19342
 
19343
		// extension for ordinal breaks
19344
		each(segments, function (segment, no) {
19345
			var i = segment.length - 1;
19346
			while (i--) {
19347
				if (segment[i].x < xAxis.min && segment[i + 1].x > xAxis.max) {
19348
					segments.length = 0;
19349
					break;
19350
				} else if (segment[i + 1].x - segment[i].x > xAxis.closestPointRange * gapSize) {
19351
					segments.splice( // insert after this one
19352
						no + 1,
19353
						0,
19354
						segment.splice(i + 1, segment.length - i)
19355
					);
19356
				}
19357
			}
19358
		});
19359
	}
19360
});
19361
 
19362
/* ****************************************************************************
19363
 * End ordinal axis logic                                                   *
19364
 *****************************************************************************/
19365
/**
19366
 * Highstock JS v2.1.8 (2015-08-20)
19367
 * Highcharts Broken Axis module
19368
 *
19369
 * Author: Stephane Vanraes, Torstein Honsi
19370
 * License: www.highcharts.com/license
19371
 */
19372
 
19373
/*global HighchartsAdapter*/
19374
(function (H) {
19375
 
19376
	"use strict";
19377
 
19378
	var pick = H.pick,
19379
		wrap = H.wrap,
19380
		extend = H.extend,
19381
		fireEvent = HighchartsAdapter.fireEvent,
19382
		Axis = H.Axis,
19383
		Series = H.Series;
19384
 
19385
	function stripArguments() {
19386
		return Array.prototype.slice.call(arguments, 1);
19387
	}
19388
 
19389
	extend(Axis.prototype, {
19390
		isInBreak: function (brk, val) {
19391
			var ret,
19392
				repeat = brk.repeat || Infinity,
19393
				from = brk.from,
19394
				length = brk.to - brk.from,
19395
				test = (val >= from ? (val - from) % repeat :  repeat - ((from - val) % repeat));
19396
 
19397
			if (!brk.inclusive) {
19398
				ret = test < length && test !== 0;
19399
			} else {
19400
				ret = test <= length;
19401
			}
19402
			return ret;
19403
		},
19404
 
19405
		isInAnyBreak: function (val, testKeep) {
19406
 
19407
			var breaks = this.options.breaks,
19408
				i = breaks && breaks.length,
19409
				inbrk,
19410
				keep,
19411
				ret;
19412
 
19413
 
19414
			if (i) {
19415
 
19416
				while (i--) {
19417
					if (this.isInBreak(breaks[i], val)) {
19418
						inbrk = true;
19419
						if (!keep) {
19420
							keep = pick(breaks[i].showPoints, this.isXAxis ? false : true);
19421
						}
19422
					}
19423
				}
19424
 
19425
				if (inbrk && testKeep) {
19426
					ret = inbrk && !keep;
19427
				} else {
19428
					ret = inbrk;
19429
				}
19430
			}
19431
			return ret;
19432
		}
19433
	});
19434
 
19435
	wrap(Axis.prototype, 'setTickPositions', function (proceed) {
19436
		proceed.apply(this, Array.prototype.slice.call(arguments, 1));
19437
 
19438
		if (this.options.breaks) {
19439
			var axis = this,
19440
				tickPositions = this.tickPositions,
19441
				info = this.tickPositions.info,
19442
				newPositions = [],
19443
				i;
19444
 
19445
			if (info && info.totalRange >= axis.closestPointRange) {
19446
				return;
19447
			}
19448
 
19449
			for (i = 0; i < tickPositions.length; i++) {
19450
				if (!axis.isInAnyBreak(tickPositions[i])) {
19451
					newPositions.push(tickPositions[i]);
19452
				}
19453
			}
19454
 
19455
			this.tickPositions = newPositions;
19456
			this.tickPositions.info = info;
19457
		}
19458
	});
19459
 
19460
	wrap(Axis.prototype, 'init', function (proceed, chart, userOptions) {
19461
		// Force Axis to be not-ordinal when breaks are defined
19462
		if (userOptions.breaks && userOptions.breaks.length) {
19463
			userOptions.ordinal = false;
19464
		}
19465
 
19466
		proceed.call(this, chart, userOptions);
19467
 
19468
		if (this.options.breaks) {
19469
 
19470
			var axis = this;
19471
 
19472
			axis.doPostTranslate = true;
19473
 
19474
			this.val2lin = function (val) {
19475
				var nval = val,
19476
					brk,
19477
					i;
19478
 
19479
				for (i = 0; i < axis.breakArray.length; i++) {
19480
					brk = axis.breakArray[i];
19481
					if (brk.to <= val) {
19482
						nval -= brk.len;
19483
					} else if (brk.from >= val) {
19484
						break;
19485
					} else if (axis.isInBreak(brk, val)) {
19486
						nval -= (val - brk.from);
19487
						break;
19488
					}
19489
				}
19490
 
19491
				return nval;
19492
			};
19493
 
19494
			this.lin2val = function (val) {
19495
				var nval = val,
19496
					brk,
19497
					i;
19498
 
19499
				for (i = 0; i < axis.breakArray.length; i++) {
19500
					brk = axis.breakArray[i];
19501
					if (brk.from >= nval) {
19502
						break;
19503
					} else if (brk.to < nval) {
19504
						nval += brk.len;
19505
					} else if (axis.isInBreak(brk, nval)) {
19506
						nval += brk.len;
19507
					}
19508
				}
19509
				return nval;
19510
			};
19511
 
19512
			this.setExtremes = function (newMin, newMax, redraw, animation, eventArguments) {
19513
				// If trying to set extremes inside a break, extend it to before and after the break ( #3857 )
19514
				while (this.isInAnyBreak(newMin)) {
19515
					newMin -= this.closestPointRange;
19516
				}
19517
				while (this.isInAnyBreak(newMax)) {
19518
					newMax -= this.closestPointRange;
19519
				}
19520
				Axis.prototype.setExtremes.call(this, newMin, newMax, redraw, animation, eventArguments);
19521
			};
19522
 
19523
			this.setAxisTranslation = function (saveOld) {
19524
				Axis.prototype.setAxisTranslation.call(this, saveOld);
19525
 
19526
				var breaks = axis.options.breaks,
19527
					breakArrayT = [],	// Temporary one
19528
					breakArray = [],
19529
					length = 0,
19530
					inBrk,
19531
					repeat,
19532
					brk,
19533
					min = axis.userMin || axis.min,
19534
					max = axis.userMax || axis.max,
19535
					start,
19536
					i,
19537
					j;
19538
 
19539
				// Min & max check (#4247)
19540
				for (i in breaks) {
19541
					brk = breaks[i];
19542
					repeat = brk.repeat || Infinity;
19543
					if (axis.isInBreak(brk, min)) {
19544
						min += (brk.to % repeat) - (min % repeat);
19545
					}
19546
					if (axis.isInBreak(brk, max)) {
19547
						max -= (max % repeat) - (brk.from % repeat);
19548
					}
19549
				}
19550
 
19551
				// Construct an array holding all breaks in the axis
19552
				for (i in breaks) {
19553
					brk = breaks[i];
19554
					start = brk.from;
19555
					repeat = brk.repeat || Infinity;
19556
 
19557
					while (start - repeat > min) {
19558
						start -= repeat;
19559
					}
19560
					while (start < min) {
19561
						start += repeat;
19562
					}
19563
 
19564
					for (j = start; j < max; j += repeat) {
19565
						breakArrayT.push({
19566
							value: j,
19567
							move: 'in'
19568
						});
19569
						breakArrayT.push({
19570
							value: j + (brk.to - brk.from),
19571
							move: 'out',
19572
							size: brk.breakSize
19573
						});
19574
					}
19575
				}
19576
 
19577
				breakArrayT.sort(function (a, b) {
19578
					if (a.value === b.value) {
19579
						return (a.move === 'in' ? 0 : 1) - (b.move === 'in' ? 0 : 1);
19580
					} else {
19581
						return a.value - b.value;
19582
					}
19583
				});
19584
 
19585
				// Simplify the breaks
19586
				inBrk = 0;
19587
				start = min;
19588
 
19589
				for (i in breakArrayT) {
19590
					brk = breakArrayT[i];
19591
					inBrk += (brk.move === 'in' ? 1 : -1);
19592
 
19593
					if (inBrk === 1 && brk.move === 'in') {
19594
						start = brk.value;
19595
					}
19596
					if (inBrk === 0) {
19597
						breakArray.push({
19598
							from: start,
19599
							to: brk.value,
19600
							len: brk.value - start - (brk.size || 0)
19601
						});
19602
						length += brk.value - start - (brk.size || 0);
19603
					}
19604
				}
19605
 
19606
				axis.breakArray = breakArray;
19607
 
19608
				fireEvent(axis, 'afterBreaks');
19609
 
19610
				axis.transA *= ((max - axis.min) / (max - min - length));
19611
 
19612
				axis.min = min;
19613
				axis.max = max;
19614
			};
19615
		}
19616
	});
19617
 
19618
	wrap(Series.prototype, 'generatePoints', function (proceed) {
19619
 
19620
		proceed.apply(this, stripArguments(arguments));
19621
 
19622
		var series = this,
19623
			xAxis = series.xAxis,
19624
			yAxis = series.yAxis,
19625
			points = series.points,
19626
			point,
19627
			i = points.length,
19628
			connectNulls = series.options.connectNulls,
19629
			nullGap;
19630
 
19631
 
19632
		if (xAxis && yAxis && (xAxis.options.breaks || yAxis.options.breaks)) {
19633
			while (i--) {
19634
				point = points[i];
19635
 
19636
				nullGap = point.y === null && connectNulls === false; // respect nulls inside the break (#4275)
19637
				if (!nullGap && (xAxis.isInAnyBreak(point.x, true) || yAxis.isInAnyBreak(point.y, true))) {
19638
					points.splice(i, 1);
19639
					if (this.data[i]) {
19640
						this.data[i].destroyElements(); // removes the graphics for this point if they exist
19641
					}
19642
				}
19643
			}
19644
		}
19645
 
19646
	});
19647
 
19648
	wrap(H.seriesTypes.column.prototype, 'drawPoints', function (proceed) {
19649
		proceed.apply(this);
19650
 
19651
		var series = this,
19652
			points = series.points,
19653
			yAxis = series.yAxis,
19654
			breaks = yAxis.breakArray || [],
19655
			threshold = pick(this.options.threshold, yAxis.min),
19656
			eventName,
19657
			point,
19658
			brk,
19659
			i,
19660
			j,
19661
			y;
19662
 
19663
		for (i = 0; i < points.length; i++) {
19664
			point = points[i];
19665
			y = point.stackY || point.y;
19666
			for (j = 0; j < breaks.length; j++) {
19667
				brk = breaks[j];
19668
				eventName = false;
19669
 
19670
				if ((threshold < brk.from && y > brk.to) || (threshold > brk.from && y < brk.from)) {
19671
						eventName = 'pointBreak';
19672
				} else if ((threshold < brk.from && y > brk.from && y < brk.to) || (threshold > brk.from && y > brk.to && y < brk.from)) { // point falls inside the break
19673
						eventName = 'pointInBreak'; // docs
19674
				}
19675
				if (eventName) {
19676
					fireEvent(yAxis, eventName, {point: point, brk: brk});
19677
				}
19678
			}
19679
		}
19680
 
19681
	});
19682
}(Highcharts));
19683
/* ****************************************************************************
19684
 * Start data grouping module												 *
19685
 ******************************************************************************/
19686
/*jslint white:true */
19687
var DATA_GROUPING = 'dataGrouping',
19688
	seriesProto = Series.prototype,
19689
	tooltipProto = Tooltip.prototype,
19690
	baseProcessData = seriesProto.processData,
19691
	baseGeneratePoints = seriesProto.generatePoints,
19692
	baseDestroy = seriesProto.destroy,
19693
	baseTooltipFooterHeaderFormatter = tooltipProto.tooltipFooterHeaderFormatter,
19694
	NUMBER = 'number',
19695
 
19696
	commonOptions = {
19697
		approximation: 'average', // average, open, high, low, close, sum
19698
		//enabled: null, // (true for stock charts, false for basic),
19699
		//forced: undefined,
19700
		groupPixelWidth: 2,
19701
		// the first one is the point or start value, the second is the start value if we're dealing with range,
19702
		// the third one is the end value if dealing with a range
19703
		dateTimeLabelFormats: {
19704
			millisecond: ['%A, %b %e, %H:%M:%S.%L', '%A, %b %e, %H:%M:%S.%L', '-%H:%M:%S.%L'],
19705
			second: ['%A, %b %e, %H:%M:%S', '%A, %b %e, %H:%M:%S', '-%H:%M:%S'],
19706
			minute: ['%A, %b %e, %H:%M', '%A, %b %e, %H:%M', '-%H:%M'],
19707
			hour: ['%A, %b %e, %H:%M', '%A, %b %e, %H:%M', '-%H:%M'],
19708
			day: ['%A, %b %e, %Y', '%A, %b %e', '-%A, %b %e, %Y'],
19709
			week: ['Week from %A, %b %e, %Y', '%A, %b %e', '-%A, %b %e, %Y'],
19710
			month: ['%B %Y', '%B', '-%B %Y'],
19711
			year: ['%Y', '%Y', '-%Y']
19712
		}
19713
		// smoothed = false, // enable this for navigator series only
19714
	},
19715
 
19716
	specificOptions = { // extends common options
19717
		line: {},
19718
		spline: {},
19719
		area: {},
19720
		areaspline: {},
19721
		column: {
19722
			approximation: 'sum',
19723
			groupPixelWidth: 10
19724
		},
19725
		arearange: {
19726
			approximation: 'range'
19727
		},
19728
		areasplinerange: {
19729
			approximation: 'range'
19730
		},
19731
		columnrange: {
19732
			approximation: 'range',
19733
			groupPixelWidth: 10
19734
		},
19735
		candlestick: {
19736
			approximation: 'ohlc',
19737
			groupPixelWidth: 10
19738
		},
19739
		ohlc: {
19740
			approximation: 'ohlc',
19741
			groupPixelWidth: 5
19742
		}
19743
	},
19744
 
19745
	// units are defined in a separate array to allow complete overriding in case of a user option
19746
	defaultDataGroupingUnits = [[
19747
			'millisecond', // unit name
19748
			[1, 2, 5, 10, 20, 25, 50, 100, 200, 500] // allowed multiples
19749
		], [
19750
			'second',
19751
			[1, 2, 5, 10, 15, 30]
19752
		], [
19753
			'minute',
19754
			[1, 2, 5, 10, 15, 30]
19755
		], [
19756
			'hour',
19757
			[1, 2, 3, 4, 6, 8, 12]
19758
		], [
19759
			'day',
19760
			[1]
19761
		], [
19762
			'week',
19763
			[1]
19764
		], [
19765
			'month',
19766
			[1, 3, 6]
19767
		], [
19768
			'year',
19769
			null
19770
		]
19771
	],
19772
 
19773
 
19774
	/**
19775
	 * Define the available approximation types. The data grouping approximations takes an array
19776
	 * or numbers as the first parameter. In case of ohlc, four arrays are sent in as four parameters.
19777
	 * Each array consists only of numbers. In case null values belong to the group, the property
19778
	 * .hasNulls will be set to true on the array.
19779
	 */
19780
	approximations = {
19781
		sum: function (arr) {
19782
			var len = arr.length,
19783
				ret;
19784
 
19785
			// 1. it consists of nulls exclusively
19786
			if (!len && arr.hasNulls) {
19787
				ret = null;
19788
			// 2. it has a length and real values
19789
			} else if (len) {
19790
				ret = 0;
19791
				while (len--) {
19792
					ret += arr[len];
19793
				}
19794
			}
19795
			// 3. it has zero length, so just return undefined
19796
			// => doNothing()
19797
 
19798
			return ret;
19799
		},
19800
		average: function (arr) {
19801
			var len = arr.length,
19802
				ret = approximations.sum(arr);
19803
 
19804
			// If we have a number, return it divided by the length. If not, return
19805
			// null or undefined based on what the sum method finds.
19806
			if (typeof ret === NUMBER && len) {
19807
				ret = ret / len;
19808
			}
19809
 
19810
			return ret;
19811
		},
19812
		open: function (arr) {
19813
			return arr.length ? arr[0] : (arr.hasNulls ? null : UNDEFINED);
19814
		},
19815
		high: function (arr) {
19816
			return arr.length ? arrayMax(arr) : (arr.hasNulls ? null : UNDEFINED);
19817
		},
19818
		low: function (arr) {
19819
			return arr.length ? arrayMin(arr) : (arr.hasNulls ? null : UNDEFINED);
19820
		},
19821
		close: function (arr) {
19822
			return arr.length ? arr[arr.length - 1] : (arr.hasNulls ? null : UNDEFINED);
19823
		},
19824
		// ohlc and range are special cases where a multidimensional array is input and an array is output
19825
		ohlc: function (open, high, low, close) {
19826
			open = approximations.open(open);
19827
			high = approximations.high(high);
19828
			low = approximations.low(low);
19829
			close = approximations.close(close);
19830
 
19831
			if (typeof open === NUMBER || typeof high === NUMBER || typeof low === NUMBER || typeof close === NUMBER) {
19832
				return [open, high, low, close];
19833
			}
19834
			// else, return is undefined
19835
		},
19836
		range: function (low, high) {
19837
			low = approximations.low(low);
19838
			high = approximations.high(high);
19839
 
19840
			if (typeof low === NUMBER || typeof high === NUMBER) {
19841
				return [low, high];
19842
			}
19843
			// else, return is undefined
19844
		}
19845
	};
19846
 
19847
/*jslint white:false */
19848
 
19849
/**
19850
 * Takes parallel arrays of x and y data and groups the data into intervals defined by groupPositions, a collection
19851
 * of starting x values for each group.
19852
 */
19853
seriesProto.groupData = function (xData, yData, groupPositions, approximation) {
19854
	var series = this,
19855
		data = series.data,
19856
		dataOptions = series.options.data,
19857
		groupedXData = [],
19858
		groupedYData = [],
19859
		dataLength = xData.length,
19860
		pointX,
19861
		pointY,
19862
		groupedY,
19863
		handleYData = !!yData, // when grouping the fake extended axis for panning, we don't need to consider y
19864
		values = [[], [], [], []],
19865
		approximationFn = typeof approximation === 'function' ? approximation : approximations[approximation],
19866
		pointArrayMap = series.pointArrayMap,
19867
		pointArrayMapLength = pointArrayMap && pointArrayMap.length,
19868
		i;
19869
 
19870
	// Start with the first point within the X axis range (#2696)
19871
	for (i = 0; i <= dataLength; i++) {
19872
		if (xData[i] >= groupPositions[0]) {
19873
			break;
19874
		}
19875
	}
19876
 
19877
	for (; i <= dataLength; i++) {
19878
 
19879
		// when a new group is entered, summarize and initiate the previous group
19880
		while ((groupPositions[1] !== UNDEFINED && xData[i] >= groupPositions[1]) ||
19881
				i === dataLength) { // get the last group
19882
 
19883
			// get group x and y
19884
			pointX = groupPositions.shift();
19885
			groupedY = approximationFn.apply(0, values);
19886
 
19887
			// push the grouped data
19888
			if (groupedY !== UNDEFINED) {
19889
				groupedXData.push(pointX);
19890
				groupedYData.push(groupedY);
19891
			}
19892
 
19893
			// reset the aggregate arrays
19894
			values[0] = [];
19895
			values[1] = [];
19896
			values[2] = [];
19897
			values[3] = [];
19898
 
19899
			// don't loop beyond the last group
19900
			if (i === dataLength) {
19901
				break;
19902
			}
19903
		}
19904
 
19905
		// break out
19906
		if (i === dataLength) {
19907
			break;
19908
		}
19909
 
19910
		// for each raw data point, push it to an array that contains all values for this specific group
19911
		if (pointArrayMap) {
19912
 
19913
			var index = series.cropStart + i,
19914
				point = (data && data[index]) || series.pointClass.prototype.applyOptions.apply({ series: series }, [dataOptions[index]]),
19915
				j,
19916
				val;
19917
 
19918
			for (j = 0; j < pointArrayMapLength; j++) {
19919
				val = point[pointArrayMap[j]];
19920
				if (typeof val === NUMBER) {
19921
					values[j].push(val);
19922
				} else if (val === null) {
19923
					values[j].hasNulls = true;
19924
				}
19925
			}
19926
 
19927
		} else {
19928
			pointY = handleYData ? yData[i] : null;
19929
 
19930
			if (typeof pointY === NUMBER) {
19931
				values[0].push(pointY);
19932
			} else if (pointY === null) {
19933
				values[0].hasNulls = true;
19934
			}
19935
		}
19936
	}
19937
 
19938
	return [groupedXData, groupedYData];
19939
};
19940
 
19941
/**
19942
 * Extend the basic processData method, that crops the data to the current zoom
19943
 * range, with data grouping logic.
19944
 */
19945
seriesProto.processData = function () {
19946
	var series = this,
19947
		chart = series.chart,
19948
		options = series.options,
19949
		dataGroupingOptions = options[DATA_GROUPING],
19950
		groupingEnabled = series.allowDG !== false && dataGroupingOptions && pick(dataGroupingOptions.enabled, chart.options._stock),
19951
		hasGroupedData;
19952
 
19953
	// run base method
19954
	series.forceCrop = groupingEnabled; // #334
19955
	series.groupPixelWidth = null; // #2110
19956
	series.hasProcessed = true; // #2692
19957
 
19958
	// skip if processData returns false or if grouping is disabled (in that order)
19959
	if (baseProcessData.apply(series, arguments) === false || !groupingEnabled) {
19960
		return;
19961
 
19962
	} else {
19963
		series.destroyGroupedData();
19964
 
19965
	}
19966
	var i,
19967
		processedXData = series.processedXData,
19968
		processedYData = series.processedYData,
19969
		plotSizeX = chart.plotSizeX,
19970
		xAxis = series.xAxis,
19971
		ordinal = xAxis.options.ordinal,
19972
		groupPixelWidth = series.groupPixelWidth = xAxis.getGroupPixelWidth && xAxis.getGroupPixelWidth(),
19973
		nonGroupedPointRange = series.pointRange;
19974
 
19975
	// Execute grouping if the amount of points is greater than the limit defined in groupPixelWidth
19976
	if (groupPixelWidth) {
19977
		hasGroupedData = true;
19978
 
19979
		series.points = null; // force recreation of point instances in series.translate
19980
 
19981
		var extremes = xAxis.getExtremes(),
19982
			xMin = extremes.min,
19983
			xMax = extremes.max,
19984
			groupIntervalFactor = (ordinal && xAxis.getGroupIntervalFactor(xMin, xMax, series)) || 1,
19985
			interval = (groupPixelWidth * (xMax - xMin) / plotSizeX) * groupIntervalFactor,
19986
			groupPositions = xAxis.getTimeTicks(
19987
				xAxis.normalizeTimeTickInterval(interval, dataGroupingOptions.units || defaultDataGroupingUnits),
19988
				xMin,
19989
				xMax,
19990
				xAxis.options.startOfWeek,
19991
				processedXData,
19992
				series.closestPointRange
19993
			),
19994
			groupedXandY = seriesProto.groupData.apply(series, [processedXData, processedYData, groupPositions, dataGroupingOptions.approximation]),
19995
			groupedXData = groupedXandY[0],
19996
			groupedYData = groupedXandY[1];
19997
 
19998
		// prevent the smoothed data to spill out left and right, and make
19999
		// sure data is not shifted to the left
20000
		if (dataGroupingOptions.smoothed) {
20001
			i = groupedXData.length - 1;
20002
			groupedXData[i] = xMax;
20003
			while (i-- && i > 0) {
20004
				groupedXData[i] += interval / 2;
20005
			}
20006
			groupedXData[0] = xMin;
20007
		}
20008
 
20009
		// record what data grouping values were used
20010
		series.currentDataGrouping = groupPositions.info;
20011
		if (options.pointRange === null) { // null means auto, as for columns, candlesticks and OHLC
20012
			series.pointRange = groupPositions.info.totalRange;
20013
		}
20014
		series.closestPointRange = groupPositions.info.totalRange;
20015
 
20016
		// Make sure the X axis extends to show the first group (#2533)
20017
		if (defined(groupedXData[0]) && groupedXData[0] < xAxis.dataMin) {
20018
			if (xAxis.min === xAxis.dataMin) {
20019
				xAxis.min = groupedXData[0];
20020
			}
20021
			xAxis.dataMin = groupedXData[0];
20022
		}
20023
 
20024
		// set series props
20025
		series.processedXData = groupedXData;
20026
		series.processedYData = groupedYData;
20027
	} else {
20028
		series.currentDataGrouping = null;
20029
		series.pointRange = nonGroupedPointRange;
20030
	}
20031
	series.hasGroupedData = hasGroupedData;
20032
};
20033
 
20034
/**
20035
 * Destroy the grouped data points. #622, #740
20036
 */
20037
seriesProto.destroyGroupedData = function () {
20038
 
20039
	var groupedData = this.groupedData;
20040
 
20041
	// clear previous groups
20042
	each(groupedData || [], function (point, i) {
20043
		if (point) {
20044
			groupedData[i] = point.destroy ? point.destroy() : null;
20045
		}
20046
	});
20047
	this.groupedData = null;
20048
};
20049
 
20050
/**
20051
 * Override the generatePoints method by adding a reference to grouped data
20052
 */
20053
seriesProto.generatePoints = function () {
20054
 
20055
	baseGeneratePoints.apply(this);
20056
 
20057
	// record grouped data in order to let it be destroyed the next time processData runs
20058
	this.destroyGroupedData(); // #622
20059
	this.groupedData = this.hasGroupedData ? this.points : null;
20060
};
20061
 
20062
/**
20063
 * Extend the original method, make the tooltip's header reflect the grouped range
20064
 */
20065
tooltipProto.tooltipFooterHeaderFormatter = function (point, isFooter) {
20066
	var tooltip = this,
20067
		series = point.series,
20068
		options = series.options,
20069
		tooltipOptions = series.tooltipOptions,
20070
		dataGroupingOptions = options.dataGrouping,
20071
		xDateFormat = tooltipOptions.xDateFormat,
20072
		xDateFormatEnd,
20073
		xAxis = series.xAxis,
20074
		currentDataGrouping,
20075
		dateTimeLabelFormats,
20076
		labelFormats,
20077
		formattedKey,
20078
		ret;
20079
 
20080
	// apply only to grouped series
20081
	if (xAxis && xAxis.options.type === 'datetime' && dataGroupingOptions && isNumber(point.key)) {
20082
 
20083
		// set variables
20084
		currentDataGrouping = series.currentDataGrouping;
20085
		dateTimeLabelFormats = dataGroupingOptions.dateTimeLabelFormats;
20086
 
20087
		// if we have grouped data, use the grouping information to get the right format
20088
		if (currentDataGrouping) {
20089
			labelFormats = dateTimeLabelFormats[currentDataGrouping.unitName];
20090
			if (currentDataGrouping.count === 1) {
20091
				xDateFormat = labelFormats[0];
20092
			} else {
20093
				xDateFormat = labelFormats[1];
20094
				xDateFormatEnd = labelFormats[2];
20095
			}
20096
		// if not grouped, and we don't have set the xDateFormat option, get the best fit,
20097
		// so if the least distance between points is one minute, show it, but if the
20098
		// least distance is one day, skip hours and minutes etc.
20099
		} else if (!xDateFormat && dateTimeLabelFormats) {
20100
			xDateFormat = tooltip.getXDateFormat(point, tooltipOptions, xAxis);
20101
		}
20102
 
20103
		// now format the key
20104
		formattedKey = dateFormat(xDateFormat, point.key);
20105
		if (xDateFormatEnd) {
20106
			formattedKey += dateFormat(xDateFormatEnd, point.key + currentDataGrouping.totalRange - 1);
20107
		}
20108
 
20109
		// return the replaced format
20110
		ret = tooltipOptions[(isFooter ? 'footer' : 'header') + 'Format'].replace('{point.key}', formattedKey);
20111
 
20112
	// else, fall back to the regular formatter
20113
	} else {
20114
		ret = baseTooltipFooterHeaderFormatter.call(tooltip, point, isFooter);
20115
	}
20116
 
20117
	return ret;
20118
};
20119
 
20120
/**
20121
 * Extend the series destroyer
20122
 */
20123
seriesProto.destroy = function () {
20124
	var series = this,
20125
		groupedData = series.groupedData || [],
20126
		i = groupedData.length;
20127
 
20128
	while (i--) {
20129
		if (groupedData[i]) {
20130
			groupedData[i].destroy();
20131
		}
20132
	}
20133
	baseDestroy.apply(series);
20134
};
20135
 
20136
 
20137
// Handle default options for data grouping. This must be set at runtime because some series types are
20138
// defined after this.
20139
wrap(seriesProto, 'setOptions', function (proceed, itemOptions) {
20140
 
20141
	var options = proceed.call(this, itemOptions),
20142
		type = this.type,
20143
		plotOptions = this.chart.options.plotOptions,
20144
		defaultOptions = defaultPlotOptions[type].dataGrouping;
20145
 
20146
	if (specificOptions[type]) { // #1284
20147
		if (!defaultOptions) {
20148
			defaultOptions = merge(commonOptions, specificOptions[type]);
20149
		}
20150
 
20151
		options.dataGrouping = merge(
20152
			defaultOptions,
20153
			plotOptions.series && plotOptions.series.dataGrouping, // #1228
20154
			plotOptions[type].dataGrouping, // Set by the StockChart constructor
20155
			itemOptions.dataGrouping
20156
		);
20157
	}
20158
 
20159
	if (this.chart.options._stock) {
20160
		this.requireSorting = true;
20161
	}
20162
 
20163
	return options;
20164
});
20165
 
20166
 
20167
/**
20168
 * When resetting the scale reset the hasProccessed flag to avoid taking previous data grouping
20169
 * of neighbour series into accound when determining group pixel width (#2692).
20170
 */
20171
wrap(Axis.prototype, 'setScale', function (proceed) {
20172
	proceed.call(this);
20173
	each(this.series, function (series) {
20174
		series.hasProcessed = false;
20175
	});
20176
});
20177
 
20178
/**
20179
 * Get the data grouping pixel width based on the greatest defined individual width
20180
 * of the axis' series, and if whether one of the axes need grouping.
20181
 */
20182
Axis.prototype.getGroupPixelWidth = function () {
20183
 
20184
	var series = this.series,
20185
		len = series.length,
20186
		i,
20187
		groupPixelWidth = 0,
20188
		doGrouping = false,
20189
		dataLength,
20190
		dgOptions;
20191
 
20192
	// If multiple series are compared on the same x axis, give them the same
20193
	// group pixel width (#334)
20194
	i = len;
20195
	while (i--) {
20196
		dgOptions = series[i].options.dataGrouping;
20197
		if (dgOptions) {
20198
			groupPixelWidth = mathMax(groupPixelWidth, dgOptions.groupPixelWidth);
20199
 
20200
		}
20201
	}
20202
 
20203
	// If one of the series needs grouping, apply it to all (#1634)
20204
	i = len;
20205
	while (i--) {
20206
		dgOptions = series[i].options.dataGrouping;
20207
 
20208
		if (dgOptions && series[i].hasProcessed) { // #2692
20209
 
20210
			dataLength = (series[i].processedXData || series[i].data).length;
20211
 
20212
			// Execute grouping if the amount of points is greater than the limit defined in groupPixelWidth
20213
			if (series[i].groupPixelWidth || dataLength > (this.chart.plotSizeX / groupPixelWidth) || (dataLength && dgOptions.forced)) {
20214
				doGrouping = true;
20215
			}
20216
		}
20217
	}
20218
 
20219
	return doGrouping ? groupPixelWidth : 0;
20220
};
20221
 
20222
/**
20223
 * Force data grouping on all the axis' series.
20224
 */
20225
Axis.prototype.setDataGrouping = function (dataGrouping, redraw) {
20226
	var i;
20227
 
20228
	redraw = pick(redraw, true);
20229
 
20230
	if (!dataGrouping) {
20231
		dataGrouping = {
20232
			forced: false,
20233
			units: null
20234
		};
20235
	}
20236
 
20237
	// Axis is instantiated, update all series
20238
	if (this instanceof Axis) {
20239
		i = this.series.length;
20240
		while (i--) {
20241
			this.series[i].update({
20242
				dataGrouping: dataGrouping
20243
			}, false);
20244
		}
20245
 
20246
	// Axis not yet instanciated, alter series options
20247
	} else {
20248
		each(this.chart.options.series, function (seriesOptions) {
20249
			seriesOptions.dataGrouping = dataGrouping;
20250
		}, false);
20251
	}
20252
 
20253
	if (redraw) {
20254
		this.chart.redraw();
20255
	}
20256
};
20257
 
20258
 
20259
 
20260
/* ****************************************************************************
20261
 * End data grouping module												   *
20262
 ******************************************************************************//* ****************************************************************************
20263
 * Start OHLC series code													 *
20264
 *****************************************************************************/
20265
 
20266
// 1 - Set default options
20267
defaultPlotOptions.ohlc = merge(defaultPlotOptions.column, {
20268
	lineWidth: 1,
20269
	tooltip: {
20270
		pointFormat: '<span style="color:{point.color}">\u25CF</span> <b> {series.name}</b><br/>' + // docs
20271
			'Open: {point.open}<br/>' +
20272
			'High: {point.high}<br/>' +
20273
			'Low: {point.low}<br/>' +
20274
			'Close: {point.close}<br/>'
20275
	},
20276
	states: {
20277
		hover: {
20278
			lineWidth: 3
20279
		}
20280
	},
20281
	threshold: null
20282
	//upColor: undefined
20283
});
20284
 
20285
// 2 - Create the OHLCSeries object
20286
var OHLCSeries = extendClass(seriesTypes.column, {
20287
	type: 'ohlc',
20288
	pointArrayMap: ['open', 'high', 'low', 'close'], // array point configs are mapped to this
20289
	toYData: function (point) { // return a plain array for speedy calculation
20290
		return [point.open, point.high, point.low, point.close];
20291
	},
20292
	pointValKey: 'high',
20293
 
20294
	pointAttrToOptions: { // mapping between SVG attributes and the corresponding options
20295
		stroke: 'color',
20296
		'stroke-width': 'lineWidth'
20297
	},
20298
	upColorProp: 'stroke',
20299
 
20300
	/**
20301
	 * Postprocess mapping between options and SVG attributes
20302
	 */
20303
	getAttribs: function () {
20304
		seriesTypes.column.prototype.getAttribs.apply(this, arguments);
20305
		var series = this,
20306
			options = series.options,
20307
			stateOptions = options.states,
20308
			upColor = options.upColor || series.color,
20309
			seriesDownPointAttr = merge(series.pointAttr),
20310
			upColorProp = series.upColorProp;
20311
 
20312
		seriesDownPointAttr[''][upColorProp] = upColor;
20313
		seriesDownPointAttr.hover[upColorProp] = stateOptions.hover.upColor || upColor;
20314
		seriesDownPointAttr.select[upColorProp] = stateOptions.select.upColor || upColor;
20315
 
20316
		each(series.points, function (point) {
20317
			if (point.open < point.close && !point.options.color) {
20318
				point.pointAttr = seriesDownPointAttr;
20319
			}
20320
		});
20321
	},
20322
 
20323
	/**
20324
	 * Translate data points from raw values x and y to plotX and plotY
20325
	 */
20326
	translate: function () {
20327
		var series = this,
20328
			yAxis = series.yAxis;
20329
 
20330
		seriesTypes.column.prototype.translate.apply(series);
20331
 
20332
		// do the translation
20333
		each(series.points, function (point) {
20334
			// the graphics
20335
			if (point.open !== null) {
20336
				point.plotOpen = yAxis.translate(point.open, 0, 1, 0, 1);
20337
			}
20338
			if (point.close !== null) {
20339
				point.plotClose = yAxis.translate(point.close, 0, 1, 0, 1);
20340
			}
20341
 
20342
		});
20343
	},
20344
 
20345
	/**
20346
	 * Draw the data points
20347
	 */
20348
	drawPoints: function () {
20349
		var series = this,
20350
			points = series.points,
20351
			chart = series.chart,
20352
			pointAttr,
20353
			plotOpen,
20354
			plotClose,
20355
			crispCorr,
20356
			halfWidth,
20357
			path,
20358
			graphic,
20359
			crispX;
20360
 
20361
 
20362
		each(points, function (point) {
20363
			if (point.plotY !== UNDEFINED) {
20364
 
20365
				graphic = point.graphic;
20366
				pointAttr = point.pointAttr[point.selected ? 'selected' : ''] || series.pointAttr[NORMAL_STATE];
20367
 
20368
				// crisp vector coordinates
20369
				crispCorr = (pointAttr['stroke-width'] % 2) / 2;
20370
				crispX = mathRound(point.plotX) - crispCorr;  // #2596
20371
				halfWidth = mathRound(point.shapeArgs.width / 2);
20372
 
20373
				// the vertical stem
20374
				path = [
20375
					'M',
20376
					crispX, mathRound(point.yBottom),
20377
					'L',
20378
					crispX, mathRound(point.plotY)
20379
				];
20380
 
20381
				// open
20382
				if (point.open !== null) {
20383
					plotOpen = mathRound(point.plotOpen) + crispCorr;
20384
					path.push(
20385
						'M',
20386
						crispX,
20387
						plotOpen,
20388
						'L',
20389
						crispX - halfWidth,
20390
						plotOpen
20391
					);
20392
				}
20393
 
20394
				// close
20395
				if (point.close !== null) {
20396
					plotClose = mathRound(point.plotClose) + crispCorr;
20397
					path.push(
20398
						'M',
20399
						crispX,
20400
						plotClose,
20401
						'L',
20402
						crispX + halfWidth,
20403
						plotClose
20404
					);
20405
				}
20406
 
20407
				// create and/or update the graphic
20408
				if (graphic) {
20409
					graphic
20410
						.attr(pointAttr) // #3897
20411
						.animate({ d: path });
20412
				} else {
20413
					point.graphic = chart.renderer.path(path)
20414
						.attr(pointAttr)
20415
						.add(series.group);
20416
				}
20417
 
20418
			}
20419
 
20420
 
20421
		});
20422
 
20423
	},
20424
 
20425
	/**
20426
	 * Disable animation
20427
	 */
20428
	animate: null
20429
 
20430
 
20431
});
20432
seriesTypes.ohlc = OHLCSeries;
20433
/* ****************************************************************************
20434
 * End OHLC series code													   *
20435
 *****************************************************************************/
20436
/* ****************************************************************************
20437
 * Start Candlestick series code											  *
20438
 *****************************************************************************/
20439
 
20440
// 1 - set default options
20441
defaultPlotOptions.candlestick = merge(defaultPlotOptions.column, {
20442
	lineColor: 'black',
20443
	lineWidth: 1,
20444
	states: {
20445
		hover: {
20446
			lineWidth: 2
20447
		}
20448
	},
20449
	tooltip: defaultPlotOptions.ohlc.tooltip,
20450
	threshold: null,
20451
	upColor: 'white'
20452
	// upLineColor: null
20453
});
20454
 
20455
// 2 - Create the CandlestickSeries object
20456
var CandlestickSeries = extendClass(OHLCSeries, {
20457
	type: 'candlestick',
20458
 
20459
	/**
20460
	 * One-to-one mapping from options to SVG attributes
20461
	 */
20462
	pointAttrToOptions: { // mapping between SVG attributes and the corresponding options
20463
		fill: 'color',
20464
		stroke: 'lineColor',
20465
		'stroke-width': 'lineWidth'
20466
	},
20467
	upColorProp: 'fill',
20468
 
20469
	/**
20470
	 * Postprocess mapping between options and SVG attributes
20471
	 */
20472
	getAttribs: function () {
20473
		seriesTypes.ohlc.prototype.getAttribs.apply(this, arguments);
20474
		var series = this,
20475
			options = series.options,
20476
			stateOptions = options.states,
20477
			upLineColor = options.upLineColor || options.lineColor,
20478
			hoverStroke = stateOptions.hover.upLineColor || upLineColor,
20479
			selectStroke = stateOptions.select.upLineColor || upLineColor;
20480
 
20481
		// Add custom line color for points going up (close > open).
20482
		// Fill is handled by OHLCSeries' getAttribs.
20483
		each(series.points, function (point) {
20484
			if (point.open < point.close) {
20485
 
20486
				// If an individual line color is set, we need to merge the
20487
				// point attributes, because they are shared between all up
20488
				// points by inheritance from OHCLSeries.
20489
				if (point.lineColor) {
20490
					point.pointAttr = merge(point.pointAttr);
20491
					upLineColor = point.lineColor;
20492
				}
20493
 
20494
				point.pointAttr[''].stroke = upLineColor;
20495
				point.pointAttr.hover.stroke = hoverStroke;
20496
				point.pointAttr.select.stroke = selectStroke;
20497
			}
20498
		});
20499
	},
20500
 
20501
	/**
20502
	 * Draw the data points
20503
	 */
20504
	drawPoints: function () {
20505
		var series = this,  //state = series.state,
20506
			points = series.points,
20507
			chart = series.chart,
20508
			pointAttr,
20509
			seriesPointAttr = series.pointAttr[''],
20510
			plotOpen,
20511
			plotClose,
20512
			topBox,
20513
			bottomBox,
20514
			hasTopWhisker,
20515
			hasBottomWhisker,
20516
			crispCorr,
20517
			crispX,
20518
			graphic,
20519
			path,
20520
			halfWidth;
20521
 
20522
 
20523
		each(points, function (point) {
20524
 
20525
			graphic = point.graphic;
20526
			if (point.plotY !== UNDEFINED) {
20527
 
20528
				pointAttr = point.pointAttr[point.selected ? 'selected' : ''] || seriesPointAttr;
20529
 
20530
				// crisp vector coordinates
20531
				crispCorr = (pointAttr['stroke-width'] % 2) / 2;
20532
				crispX = mathRound(point.plotX) - crispCorr; // #2596
20533
				plotOpen = point.plotOpen;
20534
				plotClose = point.plotClose;
20535
				topBox = math.min(plotOpen, plotClose);
20536
				bottomBox = math.max(plotOpen, plotClose);
20537
				halfWidth = mathRound(point.shapeArgs.width / 2);
20538
				hasTopWhisker = mathRound(topBox) !== mathRound(point.plotY);
20539
				hasBottomWhisker = bottomBox !== point.yBottom;
20540
				topBox = mathRound(topBox) + crispCorr;
20541
				bottomBox = mathRound(bottomBox) + crispCorr;
20542
 
20543
				// create the path
20544
				path = [
20545
					'M',
20546
					crispX - halfWidth, bottomBox,
20547
					'L',
20548
					crispX - halfWidth, topBox,
20549
					'L',
20550
					crispX + halfWidth, topBox,
20551
					'L',
20552
					crispX + halfWidth, bottomBox,
20553
					'Z', // Use a close statement to ensure a nice rectangle #2602
20554
					'M',
20555
					crispX, topBox,
20556
					'L',
20557
					crispX, hasTopWhisker ? mathRound(point.plotY) : topBox, // #460, #2094
20558
					'M',
20559
					crispX, bottomBox,
20560
					'L',
20561
					crispX, hasBottomWhisker ? mathRound(point.yBottom) : bottomBox // #460, #2094
20562
				];
20563
 
20564
				if (graphic) {
20565
					graphic
20566
						.attr(pointAttr) // #3897
20567
						.animate({ d: path });
20568
				} else {
20569
					point.graphic = chart.renderer.path(path)
20570
						.attr(pointAttr)
20571
						.add(series.group)
20572
						.shadow(series.options.shadow);
20573
				}
20574
 
20575
			}
20576
		});
20577
 
20578
	}
20579
 
20580
 
20581
});
20582
 
20583
seriesTypes.candlestick = CandlestickSeries;
20584
 
20585
/* ****************************************************************************
20586
 * End Candlestick series code												*
20587
 *****************************************************************************/
20588
/* ****************************************************************************
20589
 * Start Flags series code													*
20590
 *****************************************************************************/
20591
 
20592
var symbols = SVGRenderer.prototype.symbols;
20593
 
20594
// 1 - set default options
20595
defaultPlotOptions.flags = merge(defaultPlotOptions.column, {
20596
	fillColor: 'white',
20597
	lineWidth: 1,
20598
	pointRange: 0, // #673
20599
	//radius: 2,
20600
	shape: 'flag',
20601
	stackDistance: 12,
20602
	states: {
20603
		hover: {
20604
			lineColor: 'black',
20605
			fillColor: '#FCFFC5'
20606
		}
20607
	},
20608
	style: {
20609
		fontSize: '11px',
20610
		fontWeight: 'bold',
20611
		textAlign: 'center'
20612
	},
20613
	tooltip: {
20614
		pointFormat: '{point.text}<br/>'
20615
	},
20616
	threshold: null,
20617
	y: -30
20618
});
20619
 
20620
// 2 - Create the CandlestickSeries object
20621
seriesTypes.flags = extendClass(seriesTypes.column, {
20622
	type: 'flags',
20623
	sorted: false,
20624
	noSharedTooltip: true,
20625
	allowDG: false,
20626
	takeOrdinalPosition: false, // #1074
20627
	trackerGroups: ['markerGroup'],
20628
	forceCrop: true,
20629
	/**
20630
	 * Inherit the initialization from base Series
20631
	 */
20632
	init: Series.prototype.init,
20633
 
20634
	/**
20635
	 * One-to-one mapping from options to SVG attributes
20636
	 */
20637
	pointAttrToOptions: { // mapping between SVG attributes and the corresponding options
20638
		fill: 'fillColor',
20639
		stroke: 'color',
20640
		'stroke-width': 'lineWidth',
20641
		r: 'radius'
20642
	},
20643
 
20644
	/**
20645
	 * Extend the translate method by placing the point on the related series
20646
	 */
20647
	translate: function () {
20648
 
20649
		seriesTypes.column.prototype.translate.apply(this);
20650
 
20651
		var series = this,
20652
			options = series.options,
20653
			chart = series.chart,
20654
			points = series.points,
20655
			cursor = points.length - 1,
20656
			point,
20657
			lastPoint,
20658
			optionsOnSeries = options.onSeries,
20659
			onSeries = optionsOnSeries && chart.get(optionsOnSeries),
20660
			step = onSeries && onSeries.options.step,
20661
			onData = onSeries && onSeries.points,
20662
			i = onData && onData.length,
20663
			xAxis = series.xAxis,
20664
			xAxisExt = xAxis.getExtremes(),
20665
			leftPoint,
20666
			lastX,
20667
			rightPoint,
20668
			currentDataGrouping;
20669
 
20670
		// relate to a master series
20671
		if (onSeries && onSeries.visible && i) {
20672
			currentDataGrouping = onSeries.currentDataGrouping;
20673
			lastX = onData[i - 1].x + (currentDataGrouping ? currentDataGrouping.totalRange : 0); // #2374
20674
 
20675
			// sort the data points
20676
			points.sort(function (a, b) {
20677
				return (a.x - b.x);
20678
			});
20679
 
20680
			while (i-- && points[cursor]) {
20681
				point = points[cursor];
20682
				leftPoint = onData[i];
20683
 
20684
				if (leftPoint.x <= point.x && leftPoint.plotY !== UNDEFINED) {
20685
					if (point.x <= lastX) { // #803
20686
 
20687
						point.plotY = leftPoint.plotY;
20688
 
20689
						// interpolate between points, #666
20690
						if (leftPoint.x < point.x && !step) {
20691
							rightPoint = onData[i + 1];
20692
							if (rightPoint && rightPoint.plotY !== UNDEFINED) {
20693
								point.plotY +=
20694
									((point.x - leftPoint.x) / (rightPoint.x - leftPoint.x)) * // the distance ratio, between 0 and 1
20695
									(rightPoint.plotY - leftPoint.plotY); // the y distance
20696
							}
20697
						}
20698
					}
20699
					cursor--;
20700
					i++; // check again for points in the same x position
20701
					if (cursor < 0) {
20702
						break;
20703
					}
20704
				}
20705
			}
20706
		}
20707
 
20708
		// Add plotY position and handle stacking
20709
		each(points, function (point, i) {
20710
 
20711
			var stackIndex;
20712
 
20713
			// Undefined plotY means the point is either on axis, outside series range or hidden series.
20714
			// If the series is outside the range of the x axis it should fall through with
20715
			// an undefined plotY, but then we must remove the shapeArgs (#847).
20716
			if (point.plotY === UNDEFINED) {
20717
				if (point.x >= xAxisExt.min && point.x <= xAxisExt.max) { // we're inside xAxis range
20718
					point.plotY = chart.chartHeight - xAxis.bottom - (xAxis.opposite ? xAxis.height : 0) + xAxis.offset - chart.plotTop;
20719
				} else {
20720
					point.shapeArgs = {}; // 847
20721
				}
20722
			}
20723
			// if multiple flags appear at the same x, order them into a stack
20724
			lastPoint = points[i - 1];
20725
			if (lastPoint && lastPoint.plotX === point.plotX) {
20726
				if (lastPoint.stackIndex === UNDEFINED) {
20727
					lastPoint.stackIndex = 0;
20728
				}
20729
				stackIndex = lastPoint.stackIndex + 1;
20730
			}
20731
			point.stackIndex = stackIndex; // #3639
20732
		});
20733
 
20734
 
20735
	},
20736
 
20737
	/**
20738
	 * Draw the markers
20739
	 */
20740
	drawPoints: function () {
20741
		var series = this,
20742
			pointAttr,
20743
			seriesPointAttr = series.pointAttr[''],
20744
			points = series.points,
20745
			chart = series.chart,
20746
			renderer = chart.renderer,
20747
			plotX,
20748
			plotY,
20749
			options = series.options,
20750
			optionsY = options.y,
20751
			shape,
20752
			i,
20753
			point,
20754
			graphic,
20755
			stackIndex,
20756
			anchorX,
20757
			anchorY,
20758
			outsideRight;
20759
 
20760
		i = points.length;
20761
		while (i--) {
20762
			point = points[i];
20763
			outsideRight = point.plotX > series.xAxis.len;
20764
			plotX = point.plotX;
20765
			if (plotX > 0) { // #3119
20766
				plotX -= pick(point.lineWidth, options.lineWidth) % 2; // #4285
20767
			}
20768
			stackIndex = point.stackIndex;
20769
			shape = point.options.shape || options.shape;
20770
			plotY = point.plotY;
20771
			if (plotY !== UNDEFINED) {
20772
				plotY = point.plotY + optionsY - (stackIndex !== UNDEFINED && stackIndex * options.stackDistance);
20773
			}
20774
			anchorX = stackIndex ? UNDEFINED : point.plotX; // skip connectors for higher level stacked points
20775
			anchorY = stackIndex ? UNDEFINED : point.plotY;
20776
 
20777
			graphic = point.graphic;
20778
 
20779
			// only draw the point if y is defined and the flag is within the visible area
20780
			if (plotY !== UNDEFINED && plotX >= 0 && !outsideRight) {
20781
				// shortcuts
20782
				pointAttr = point.pointAttr[point.selected ? 'select' : ''] || seriesPointAttr;
20783
				if (graphic) { // update
20784
					graphic.attr({
20785
						x: plotX,
20786
						y: plotY,
20787
						r: pointAttr.r,
20788
						anchorX: anchorX,
20789
						anchorY: anchorY
20790
					});
20791
				} else {
20792
					graphic = point.graphic = renderer.label(
20793
						point.options.title || options.title || 'A',
20794
						plotX,
20795
						plotY,
20796
						shape,
20797
						anchorX,
20798
						anchorY,
20799
						options.useHTML
20800
					)
20801
					.css(merge(options.style, point.style))
20802
					.attr(pointAttr)
20803
					.attr({
20804
						align: shape === 'flag' ? 'left' : 'center',
20805
						width: options.width,
20806
						height: options.height
20807
					})
20808
					.add(series.markerGroup)
20809
					.shadow(options.shadow);
20810
 
20811
				}
20812
 
20813
				// Set the tooltip anchor position
20814
				point.tooltipPos = [plotX, plotY];
20815
 
20816
			} else if (graphic) {
20817
				point.graphic = graphic.destroy();
20818
			}
20819
 
20820
		}
20821
 
20822
	},
20823
 
20824
	/**
20825
	 * Extend the column trackers with listeners to expand and contract stacks
20826
	 */
20827
	drawTracker: function () {
20828
		var series = this,
20829
			points = series.points;
20830
 
20831
		TrackerMixin.drawTrackerPoint.apply(this);
20832
 
20833
		// Bring each stacked flag up on mouse over, this allows readability of vertically
20834
		// stacked elements as well as tight points on the x axis. #1924.
20835
		each(points, function (point) {
20836
			var graphic = point.graphic;
20837
			if (graphic) {
20838
				addEvent(graphic.element, 'mouseover', function () {
20839
 
20840
					// Raise this point
20841
					if (point.stackIndex > 0 && !point.raised) {
20842
						point._y = graphic.y;
20843
						graphic.attr({
20844
							y: point._y - 8
20845
						});
20846
						point.raised = true;
20847
					}
20848
 
20849
					// Revert other raised points
20850
					each(points, function (otherPoint) {
20851
						if (otherPoint !== point && otherPoint.raised && otherPoint.graphic) {
20852
							otherPoint.graphic.attr({
20853
								y: otherPoint._y
20854
							});
20855
							otherPoint.raised = false;
20856
						}
20857
					});
20858
				});
20859
			}
20860
		});
20861
	},
20862
 
20863
	/**
20864
	 * Disable animation
20865
	 */
20866
	animate: noop,
20867
	buildKDTree: noop,
20868
	setClip: noop
20869
 
20870
});
20871
 
20872
// create the flag icon with anchor
20873
symbols.flag = function (x, y, w, h, options) {
20874
	var anchorX = (options && options.anchorX) || x,
20875
		anchorY = (options &&  options.anchorY) || y;
20876
 
20877
	return [
20878
		'M', anchorX, anchorY,
20879
		'L', x, y + h,
20880
		x, y,
20881
		x + w, y,
20882
		x + w, y + h,
20883
		x, y + h,
20884
		'Z'
20885
	];
20886
};
20887
 
20888
// create the circlepin and squarepin icons with anchor
20889
each(['circle', 'square'], function (shape) {
20890
	symbols[shape + 'pin'] = function (x, y, w, h, options) {
20891
 
20892
		var anchorX = options && options.anchorX,
20893
			anchorY = options &&  options.anchorY,
20894
			path,
20895
			labelTopOrBottomY;
20896
 
20897
		// For single-letter flags, make sure circular flags are not taller than their width
20898
		if (shape === 'circle' && h > w) {
20899
			x -= mathRound((h - w) / 2);
20900
			w = h;
20901
		}
20902
 
20903
		path = symbols[shape](x, y, w, h);
20904
 
20905
		if (anchorX && anchorY) {
20906
			// if the label is below the anchor, draw the connecting line from the top edge of the label
20907
			// otherwise start drawing from the bottom edge
20908
			labelTopOrBottomY = (y > anchorY) ? y : y + h;
20909
			path.push('M', anchorX, labelTopOrBottomY, 'L', anchorX, anchorY);
20910
		}
20911
 
20912
		return path;
20913
	};
20914
});
20915
 
20916
// The symbol callbacks are generated on the SVGRenderer object in all browsers. Even
20917
// VML browsers need this in order to generate shapes in export. Now share
20918
// them with the VMLRenderer.
20919
if (Renderer === Highcharts.VMLRenderer) {
20920
	each(['flag', 'circlepin', 'squarepin'], function (shape) {
20921
		VMLRenderer.prototype.symbols[shape] = symbols[shape];
20922
	});
20923
}
20924
 
20925
/* ****************************************************************************
20926
 * End Flags series code													  *
20927
 *****************************************************************************/
20928
/* ****************************************************************************
20929
 * Start Scroller code														*
20930
 *****************************************************************************/
20931
var units = [].concat(defaultDataGroupingUnits), // copy
20932
	defaultSeriesType,
20933
 
20934
	// Finding the min or max of a set of variables where we don't know if they are defined,
20935
	// is a pattern that is repeated several places in Highcharts. Consider making this
20936
	// a global utility method.
20937
	numExt = function (extreme) {
20938
		var numbers = grep(arguments, function (n) { return typeof n === 'number'; });
20939
		if (numbers.length) {
20940
			return Math[extreme].apply(0, numbers);
20941
		}
20942
	};
20943
 
20944
// add more resolution to units
20945
units[4] = ['day', [1, 2, 3, 4]]; // allow more days
20946
units[5] = ['week', [1, 2, 3]]; // allow more weeks
20947
 
20948
defaultSeriesType = seriesTypes.areaspline === UNDEFINED ? 'line' : 'areaspline';
20949
 
20950
extend(defaultOptions, {
20951
	navigator: {
20952
		//enabled: true,
20953
		handles: {
20954
			backgroundColor: '#ebe7e8',
20955
			borderColor: '#b2b1b6'
20956
		},
20957
		height: 40,
20958
		margin: 25,
20959
		maskFill: 'rgba(128,179,236,0.3)',
20960
		maskInside: true,
20961
		outlineColor: '#b2b1b6',
20962
		outlineWidth: 1,
20963
		series: {
20964
			type: defaultSeriesType,
20965
			color: '#4572A7',
20966
			compare: null,
20967
			fillOpacity: 0.05,
20968
			dataGrouping: {
20969
				approximation: 'average',
20970
				enabled: true,
20971
				groupPixelWidth: 2,
20972
				smoothed: true,
20973
				units: units
20974
			},
20975
			dataLabels: {
20976
				enabled: false,
20977
				zIndex: 2 // #1839
20978
			},
20979
			id: PREFIX + 'navigator-series',
20980
			lineColor: '#4572A7',
20981
			lineWidth: 1,
20982
			marker: {
20983
				enabled: false
20984
			},
20985
			pointRange: 0,
20986
			shadow: false,
20987
			threshold: null
20988
		},
20989
		//top: undefined,
20990
		xAxis: {
20991
			tickWidth: 0,
20992
			lineWidth: 0,
20993
			gridLineColor: '#EEE',
20994
			gridLineWidth: 1,
20995
			tickPixelInterval: 200,
20996
			labels: {
20997
				align: 'left',
20998
				style: {
20999
					color: '#888'
21000
				},
21001
				x: 3,
21002
				y: -4
21003
			},
21004
			crosshair: false
21005
		},
21006
		yAxis: {
21007
			gridLineWidth: 0,
21008
			startOnTick: false,
21009
			endOnTick: false,
21010
			minPadding: 0.1,
21011
			maxPadding: 0.1,
21012
			labels: {
21013
				enabled: false
21014
			},
21015
			crosshair: false,
21016
			title: {
21017
				text: null
21018
			},
21019
			tickWidth: 0
21020
		}
21021
	},
21022
	scrollbar: {
21023
		//enabled: true
21024
		height: isTouchDevice ? 20 : 14,
21025
		barBackgroundColor: '#bfc8d1',
21026
		barBorderRadius: 0,
21027
		barBorderWidth: 1,
21028
		barBorderColor: '#bfc8d1',
21029
		buttonArrowColor: '#666',
21030
		buttonBackgroundColor: '#ebe7e8',
21031
		buttonBorderColor: '#bbb',
21032
		buttonBorderRadius: 0,
21033
		buttonBorderWidth: 1,
21034
		minWidth: 6,
21035
		rifleColor: '#666',
21036
		trackBackgroundColor: '#eeeeee',
21037
		trackBorderColor: '#eeeeee',
21038
		trackBorderWidth: 1,
21039
		// trackBorderRadius: 0
21040
		liveRedraw: hasSVG && !isTouchDevice
21041
	}
21042
});
21043
 
21044
/**
21045
 * The Scroller class
21046
 * @param {Object} chart
21047
 */
21048
function Scroller(chart) {
21049
	var chartOptions = chart.options,
21050
		navigatorOptions = chartOptions.navigator,
21051
		navigatorEnabled = navigatorOptions.enabled,
21052
		scrollbarOptions = chartOptions.scrollbar,
21053
		scrollbarEnabled = scrollbarOptions.enabled,
21054
		height = navigatorEnabled ? navigatorOptions.height : 0,
21055
		scrollbarHeight = scrollbarEnabled ? scrollbarOptions.height : 0;
21056
 
21057
 
21058
	this.handles = [];
21059
	this.scrollbarButtons = [];
21060
	this.elementsToDestroy = []; // Array containing the elements to destroy when Scroller is destroyed
21061
 
21062
	this.chart = chart;
21063
	this.setBaseSeries();
21064
 
21065
	this.height = height;
21066
	this.scrollbarHeight = scrollbarHeight;
21067
	this.scrollbarEnabled = scrollbarEnabled;
21068
	this.navigatorEnabled = navigatorEnabled;
21069
	this.navigatorOptions = navigatorOptions;
21070
	this.scrollbarOptions = scrollbarOptions;
21071
	this.outlineHeight = height + scrollbarHeight;
21072
 
21073
	// Run scroller
21074
	this.init();
21075
}
21076
 
21077
Scroller.prototype = {
21078
	/**
21079
	 * Draw one of the handles on the side of the zoomed range in the navigator
21080
	 * @param {Number} x The x center for the handle
21081
	 * @param {Number} index 0 for left and 1 for right
21082
	 */
21083
	drawHandle: function (x, index) {
21084
		var scroller = this,
21085
			chart = scroller.chart,
21086
			renderer = chart.renderer,
21087
			elementsToDestroy = scroller.elementsToDestroy,
21088
			handles = scroller.handles,
21089
			handlesOptions = scroller.navigatorOptions.handles,
21090
			attr = {
21091
				fill: handlesOptions.backgroundColor,
21092
				stroke: handlesOptions.borderColor,
21093
				'stroke-width': 1
21094
			},
21095
			tempElem;
21096
 
21097
		// create the elements
21098
		if (!scroller.rendered) {
21099
			// the group
21100
			handles[index] = renderer.g('navigator-handle-' + ['left', 'right'][index])
21101
				.css({ cursor: 'ew-resize' })
21102
				.attr({ zIndex: 4 - index }) // zIndex = 3 for right handle, 4 for left
21103
				.add();
21104
 
21105
			// the rectangle
21106
			tempElem = renderer.rect(-4.5, 0, 9, 16, 0, 1)
21107
				.attr(attr)
21108
				.add(handles[index]);
21109
			elementsToDestroy.push(tempElem);
21110
 
21111
			// the rifles
21112
			tempElem = renderer.path([
21113
					'M',
21114
					-1.5, 4,
21115
					'L',
21116
					-1.5, 12,
21117
					'M',
21118
					0.5, 4,
21119
					'L',
21120
					0.5, 12
21121
				]).attr(attr)
21122
				.add(handles[index]);
21123
			elementsToDestroy.push(tempElem);
21124
		}
21125
 
21126
		// Place it
21127
		handles[index][chart.isResizing ? 'animate' : 'attr']({
21128
			translateX: scroller.scrollerLeft + scroller.scrollbarHeight + parseInt(x, 10),
21129
			translateY: scroller.top + scroller.height / 2 - 8
21130
		});
21131
	},
21132
 
21133
	/**
21134
	 * Draw the scrollbar buttons with arrows
21135
	 * @param {Number} index 0 is left, 1 is right
21136
	 */
21137
	drawScrollbarButton: function (index) {
21138
		var scroller = this,
21139
			chart = scroller.chart,
21140
			renderer = chart.renderer,
21141
			elementsToDestroy = scroller.elementsToDestroy,
21142
			scrollbarButtons = scroller.scrollbarButtons,
21143
			scrollbarHeight = scroller.scrollbarHeight,
21144
			scrollbarOptions = scroller.scrollbarOptions,
21145
			tempElem;
21146
 
21147
		if (!scroller.rendered) {
21148
			scrollbarButtons[index] = renderer.g().add(scroller.scrollbarGroup);
21149
 
21150
			tempElem = renderer.rect(
21151
					-0.5,
21152
					-0.5,
21153
					scrollbarHeight + 1, // +1 to compensate for crispifying in rect method
21154
					scrollbarHeight + 1,
21155
					scrollbarOptions.buttonBorderRadius,
21156
					scrollbarOptions.buttonBorderWidth
21157
				).attr({
21158
					stroke: scrollbarOptions.buttonBorderColor,
21159
					'stroke-width': scrollbarOptions.buttonBorderWidth,
21160
					fill: scrollbarOptions.buttonBackgroundColor
21161
				}).add(scrollbarButtons[index]);
21162
			elementsToDestroy.push(tempElem);
21163
 
21164
			tempElem = renderer.path([
21165
					'M',
21166
					scrollbarHeight / 2 + (index ? -1 : 1), scrollbarHeight / 2 - 3,
21167
					'L',
21168
					scrollbarHeight / 2 + (index ? -1 : 1), scrollbarHeight / 2 + 3,
21169
					scrollbarHeight / 2 + (index ? 2 : -2), scrollbarHeight / 2
21170
				]).attr({
21171
					fill: scrollbarOptions.buttonArrowColor
21172
				}).add(scrollbarButtons[index]);
21173
			elementsToDestroy.push(tempElem);
21174
		}
21175
 
21176
		// adjust the right side button to the varying length of the scroll track
21177
		if (index) {
21178
			scrollbarButtons[index].attr({
21179
				translateX: scroller.scrollerWidth - scrollbarHeight
21180
			});
21181
		}
21182
	},
21183
 
21184
	/**
21185
	 * Render the navigator and scroll bar
21186
	 * @param {Number} min X axis value minimum
21187
	 * @param {Number} max X axis value maximum
21188
	 * @param {Number} pxMin Pixel value minimum
21189
	 * @param {Number} pxMax Pixel value maximum
21190
	 */
21191
	render: function (min, max, pxMin, pxMax) {
21192
		var scroller = this,
21193
			chart = scroller.chart,
21194
			renderer = chart.renderer,
21195
			navigatorLeft,
21196
			navigatorWidth,
21197
			scrollerLeft,
21198
			scrollerWidth,
21199
			scrollbarGroup = scroller.scrollbarGroup,
21200
			navigatorGroup = scroller.navigatorGroup,
21201
			scrollbar = scroller.scrollbar,
21202
			xAxis = scroller.xAxis,
21203
			scrollbarTrack = scroller.scrollbarTrack,
21204
			scrollbarHeight = scroller.scrollbarHeight,
21205
			scrollbarEnabled = scroller.scrollbarEnabled,
21206
			navigatorOptions = scroller.navigatorOptions,
21207
			scrollbarOptions = scroller.scrollbarOptions,
21208
			scrollbarMinWidth = scrollbarOptions.minWidth,
21209
			height = scroller.height,
21210
			top = scroller.top,
21211
			navigatorEnabled = scroller.navigatorEnabled,
21212
			outlineWidth = navigatorOptions.outlineWidth,
21213
			halfOutline = outlineWidth / 2,
21214
			zoomedMin,
21215
			zoomedMax,
21216
			range,
21217
			scrX,
21218
			scrWidth,
21219
			scrollbarPad = 0,
21220
			outlineHeight = scroller.outlineHeight,
21221
			barBorderRadius = scrollbarOptions.barBorderRadius,
21222
			strokeWidth,
21223
			scrollbarStrokeWidth = scrollbarOptions.barBorderWidth,
21224
			centerBarX,
21225
			outlineTop = top + halfOutline,
21226
			verb,
21227
			unionExtremes;
21228
 
21229
		// Don't render the navigator until we have data (#486, #4202)
21230
		if (!defined(min) || isNaN(min)) {
21231
			return;
21232
		}
21233
 
21234
		scroller.navigatorLeft = navigatorLeft = pick(
21235
			xAxis.left,
21236
			chart.plotLeft + scrollbarHeight // in case of scrollbar only, without navigator
21237
		);
21238
		scroller.navigatorWidth = navigatorWidth = pick(xAxis.len, chart.plotWidth - 2 * scrollbarHeight);
21239
		scroller.scrollerLeft = scrollerLeft = navigatorLeft - scrollbarHeight;
21240
		scroller.scrollerWidth = scrollerWidth = scrollerWidth = navigatorWidth + 2 * scrollbarHeight;
21241
 
21242
		// Set the scroller x axis extremes to reflect the total. The navigator extremes
21243
		// should always be the extremes of the union of all series in the chart as
21244
		// well as the navigator series.
21245
		if (xAxis.getExtremes) {
21246
			unionExtremes = scroller.getUnionExtremes(true);
21247
 
21248
			if (unionExtremes && (unionExtremes.dataMin !== xAxis.min || unionExtremes.dataMax !== xAxis.max)) {
21249
				xAxis.setExtremes(unionExtremes.dataMin, unionExtremes.dataMax, true, false);
21250
			}
21251
		}
21252
 
21253
		// Get the pixel position of the handles
21254
		pxMin = pick(pxMin, xAxis.translate(min));
21255
		pxMax = pick(pxMax, xAxis.translate(max));
21256
		if (isNaN(pxMin) || mathAbs(pxMin) === Infinity) { // Verify (#1851, #2238)
21257
			pxMin = 0;
21258
			pxMax = scrollerWidth;
21259
		}
21260
 
21261
		// Are we below the minRange? (#2618)
21262
		if (xAxis.translate(pxMax, true) - xAxis.translate(pxMin, true) < chart.xAxis[0].minRange) {
21263
			return;
21264
		}
21265
 
21266
 
21267
		// handles are allowed to cross, but never exceed the plot area
21268
		scroller.zoomedMax = mathMin(mathMax(pxMin, pxMax), navigatorWidth);
21269
		scroller.zoomedMin =
21270
			mathMax(scroller.fixedWidth ? scroller.zoomedMax - scroller.fixedWidth : mathMin(pxMin, pxMax), 0);
21271
		scroller.range = scroller.zoomedMax - scroller.zoomedMin;
21272
		zoomedMax = mathRound(scroller.zoomedMax);
21273
		zoomedMin = mathRound(scroller.zoomedMin);
21274
		range = zoomedMax - zoomedMin;
21275
 
21276
 
21277
 
21278
		// on first render, create all elements
21279
		if (!scroller.rendered) {
21280
 
21281
			if (navigatorEnabled) {
21282
 
21283
				// draw the navigator group
21284
				scroller.navigatorGroup = navigatorGroup = renderer.g('navigator')
21285
					.attr({
21286
						zIndex: 3
21287
					})
21288
					.add();
21289
 
21290
				scroller.leftShade = renderer.rect()
21291
					.attr({
21292
						fill: navigatorOptions.maskFill
21293
					}).add(navigatorGroup);
21294
 
21295
				if (navigatorOptions.maskInside) {
21296
					scroller.leftShade.css({ cursor: 'ew-resize '});
21297
				} else {
21298
					scroller.rightShade = renderer.rect()
21299
						.attr({
21300
							fill: navigatorOptions.maskFill
21301
						}).add(navigatorGroup);
21302
				}
21303
 
21304
 
21305
				scroller.outline = renderer.path()
21306
					.attr({
21307
						'stroke-width': outlineWidth,
21308
						stroke: navigatorOptions.outlineColor
21309
					})
21310
					.add(navigatorGroup);
21311
			}
21312
 
21313
			if (scrollbarEnabled) {
21314
 
21315
				// draw the scrollbar group
21316
				scroller.scrollbarGroup = scrollbarGroup = renderer.g('scrollbar').add();
21317
 
21318
				// the scrollbar track
21319
				strokeWidth = scrollbarOptions.trackBorderWidth;
21320
				scroller.scrollbarTrack = scrollbarTrack = renderer.rect().attr({
21321
					x: 0,
21322
					y: -strokeWidth % 2 / 2,
21323
					fill: scrollbarOptions.trackBackgroundColor,
21324
					stroke: scrollbarOptions.trackBorderColor,
21325
					'stroke-width': strokeWidth,
21326
					r: scrollbarOptions.trackBorderRadius || 0,
21327
					height: scrollbarHeight
21328
				}).add(scrollbarGroup);
21329
 
21330
				// the scrollbar itself
21331
				scroller.scrollbar = scrollbar = renderer.rect()
21332
					.attr({
21333
						y: -scrollbarStrokeWidth % 2 / 2,
21334
						height: scrollbarHeight,
21335
						fill: scrollbarOptions.barBackgroundColor,
21336
						stroke: scrollbarOptions.barBorderColor,
21337
						'stroke-width': scrollbarStrokeWidth,
21338
						r: barBorderRadius
21339
					})
21340
					.add(scrollbarGroup);
21341
 
21342
				scroller.scrollbarRifles = renderer.path()
21343
					.attr({
21344
						stroke: scrollbarOptions.rifleColor,
21345
						'stroke-width': 1
21346
					})
21347
					.add(scrollbarGroup);
21348
			}
21349
		}
21350
 
21351
		// place elements
21352
		verb = chart.isResizing ? 'animate' : 'attr';
21353
 
21354
		if (navigatorEnabled) {
21355
			scroller.leftShade[verb](navigatorOptions.maskInside ? {
21356
				x: navigatorLeft + zoomedMin,
21357
				y: top,
21358
				width: zoomedMax - zoomedMin,
21359
				height: height
21360
			} : {
21361
				x: navigatorLeft,
21362
				y: top,
21363
				width: zoomedMin,
21364
				height: height
21365
			});
21366
			if (scroller.rightShade) {
21367
				scroller.rightShade[verb]({
21368
					x: navigatorLeft + zoomedMax,
21369
					y: top,
21370
					width: navigatorWidth - zoomedMax,
21371
					height: height
21372
				});
21373
			}
21374
 
21375
			scroller.outline[verb]({ d: [
21376
				M,
21377
				scrollerLeft, outlineTop, // left
21378
				L,
21379
				navigatorLeft + zoomedMin - halfOutline, outlineTop, // upper left of zoomed range
21380
				navigatorLeft + zoomedMin - halfOutline, outlineTop + outlineHeight, // lower left of z.r.
21381
				L,
21382
				navigatorLeft + zoomedMax - halfOutline, outlineTop + outlineHeight, // lower right of z.r.
21383
				L,
21384
				navigatorLeft + zoomedMax - halfOutline, outlineTop, // upper right of z.r.
21385
				scrollerLeft + scrollerWidth, outlineTop // right
21386
			].concat(navigatorOptions.maskInside ? [
21387
				M,
21388
				navigatorLeft + zoomedMin + halfOutline, outlineTop, // upper left of zoomed range
21389
				L,
21390
				navigatorLeft + zoomedMax - halfOutline, outlineTop // upper right of z.r.
21391
			] : [])});
21392
			// draw handles
21393
			scroller.drawHandle(zoomedMin + halfOutline, 0);
21394
			scroller.drawHandle(zoomedMax + halfOutline, 1);
21395
		}
21396
 
21397
		// draw the scrollbar
21398
		if (scrollbarEnabled && scrollbarGroup) {
21399
 
21400
			// draw the buttons
21401
			scroller.drawScrollbarButton(0);
21402
			scroller.drawScrollbarButton(1);
21403
 
21404
			scrollbarGroup[verb]({
21405
				translateX: scrollerLeft,
21406
				translateY: mathRound(outlineTop + height)
21407
			});
21408
 
21409
			scrollbarTrack[verb]({
21410
				width: scrollerWidth
21411
			});
21412
 
21413
			// prevent the scrollbar from drawing to small (#1246)
21414
			scrX = scrollbarHeight + zoomedMin;
21415
			scrWidth = range - scrollbarStrokeWidth;
21416
			if (scrWidth < scrollbarMinWidth) {
21417
				scrollbarPad = (scrollbarMinWidth - scrWidth) / 2;
21418
				scrWidth = scrollbarMinWidth;
21419
				scrX -= scrollbarPad;
21420
			}
21421
			scroller.scrollbarPad = scrollbarPad;
21422
			scrollbar[verb]({
21423
				x: mathFloor(scrX) + (scrollbarStrokeWidth % 2 / 2),
21424
				width: scrWidth
21425
			});
21426
 
21427
			centerBarX = scrollbarHeight + zoomedMin + range / 2 - 0.5;
21428
 
21429
			scroller.scrollbarRifles
21430
				.attr({
21431
					visibility: range > 12 ? VISIBLE : HIDDEN
21432
				})[verb]({
21433
					d: [
21434
						M,
21435
						centerBarX - 3, scrollbarHeight / 4,
21436
						L,
21437
						centerBarX - 3, 2 * scrollbarHeight / 3,
21438
						M,
21439
						centerBarX, scrollbarHeight / 4,
21440
						L,
21441
						centerBarX, 2 * scrollbarHeight / 3,
21442
						M,
21443
						centerBarX + 3, scrollbarHeight / 4,
21444
						L,
21445
						centerBarX + 3, 2 * scrollbarHeight / 3
21446
					]
21447
				});
21448
		}
21449
 
21450
		scroller.scrollbarPad = scrollbarPad;
21451
		scroller.rendered = true;
21452
	},
21453
 
21454
	/**
21455
	 * Set up the mouse and touch events for the navigator and scrollbar
21456
	 */
21457
	addEvents: function () {
21458
		var container = this.chart.container,
21459
			mouseDownHandler = this.mouseDownHandler,
21460
			mouseMoveHandler = this.mouseMoveHandler,
21461
			mouseUpHandler = this.mouseUpHandler,
21462
			_events;
21463
 
21464
		// Mouse events
21465
		_events = [
21466
			[container, 'mousedown', mouseDownHandler],
21467
			[container, 'mousemove', mouseMoveHandler],
21468
			[document, 'mouseup', mouseUpHandler]
21469
		];
21470
 
21471
		// Touch events
21472
		if (hasTouch) {
21473
			_events.push(
21474
				[container, 'touchstart', mouseDownHandler],
21475
				[container, 'touchmove', mouseMoveHandler],
21476
				[document, 'touchend', mouseUpHandler]
21477
			);
21478
		}
21479
 
21480
		// Add them all
21481
		each(_events, function (args) {
21482
			addEvent.apply(null, args);
21483
		});
21484
		this._events = _events;
21485
	},
21486
 
21487
	/**
21488
	 * Removes the event handlers attached previously with addEvents.
21489
	 */
21490
	removeEvents: function () {
21491
 
21492
		each(this._events, function (args) {
21493
			removeEvent.apply(null, args);
21494
		});
21495
		this._events = UNDEFINED;
21496
		if (this.navigatorEnabled && this.baseSeries) {
21497
			removeEvent(this.baseSeries, 'updatedData', this.updatedDataHandler);
21498
		}
21499
	},
21500
 
21501
	/**
21502
	 * Initiate the Scroller object
21503
	 */
21504
	init: function () {
21505
		var scroller = this,
21506
			chart = scroller.chart,
21507
			xAxis,
21508
			yAxis,
21509
			scrollbarHeight = scroller.scrollbarHeight,
21510
			navigatorOptions = scroller.navigatorOptions,
21511
			height = scroller.height,
21512
			top = scroller.top,
21513
			dragOffset,
21514
			hasDragged,
21515
			baseSeries = scroller.baseSeries;
21516
 
21517
		/**
21518
		 * Event handler for the mouse down event.
21519
		 */
21520
		scroller.mouseDownHandler = function (e) {
21521
			e = chart.pointer.normalize(e);
21522
 
21523
			var zoomedMin = scroller.zoomedMin,
21524
				zoomedMax = scroller.zoomedMax,
21525
				top = scroller.top,
21526
				scrollbarHeight = scroller.scrollbarHeight,
21527
				scrollerLeft = scroller.scrollerLeft,
21528
				scrollerWidth = scroller.scrollerWidth,
21529
				navigatorLeft = scroller.navigatorLeft,
21530
				navigatorWidth = scroller.navigatorWidth,
21531
				scrollbarPad = scroller.scrollbarPad,
21532
				range = scroller.range,
21533
				chartX = e.chartX,
21534
				chartY = e.chartY,
21535
				baseXAxis = chart.xAxis[0],
21536
				fixedMax,
21537
				ext,
21538
				handleSensitivity = isTouchDevice ? 10 : 7,
21539
				left,
21540
				isOnNavigator;
21541
 
21542
			if (chartY > top && chartY < top + height + scrollbarHeight) { // we're vertically inside the navigator
21543
				isOnNavigator = !scroller.scrollbarEnabled || chartY < top + height;
21544
 
21545
				// grab the left handle
21546
				if (isOnNavigator && math.abs(chartX - zoomedMin - navigatorLeft) < handleSensitivity) {
21547
					scroller.grabbedLeft = true;
21548
					scroller.otherHandlePos = zoomedMax;
21549
					scroller.fixedExtreme = baseXAxis.max;
21550
					chart.fixedRange = null;
21551
 
21552
				// grab the right handle
21553
				} else if (isOnNavigator && math.abs(chartX - zoomedMax - navigatorLeft) < handleSensitivity) {
21554
					scroller.grabbedRight = true;
21555
					scroller.otherHandlePos = zoomedMin;
21556
					scroller.fixedExtreme = baseXAxis.min;
21557
					chart.fixedRange = null;
21558
 
21559
				// grab the zoomed range
21560
				} else if (chartX > navigatorLeft + zoomedMin - scrollbarPad && chartX < navigatorLeft + zoomedMax + scrollbarPad) {
21561
					scroller.grabbedCenter = chartX;
21562
					scroller.fixedWidth = range;
21563
 
21564
					dragOffset = chartX - zoomedMin;
21565
 
21566
 
21567
				// shift the range by clicking on shaded areas, scrollbar track or scrollbar buttons
21568
				} else if (chartX > scrollerLeft && chartX < scrollerLeft + scrollerWidth) {
21569
 
21570
					// Center around the clicked point
21571
					if (isOnNavigator) {
21572
						left = chartX - navigatorLeft - range / 2;
21573
 
21574
					// Click on scrollbar
21575
					} else {
21576
 
21577
						// Click left scrollbar button
21578
						if (chartX < navigatorLeft) {
21579
							left = zoomedMin - range * 0.2;
21580
 
21581
						// Click right scrollbar button
21582
						} else if (chartX > scrollerLeft + scrollerWidth - scrollbarHeight) {
21583
							left = zoomedMin + range * 0.2;
21584
 
21585
						// Click on scrollbar track, shift the scrollbar by one range
21586
						} else {
21587
							left = chartX < navigatorLeft + zoomedMin ? // on the left
21588
								zoomedMin - range :
21589
								zoomedMax;
21590
						}
21591
					}
21592
					if (left < 0) {
21593
						left = 0;
21594
					} else if (left + range >= navigatorWidth) {
21595
						left = navigatorWidth - range;
21596
						fixedMax = scroller.getUnionExtremes().dataMax; // #2293, #3543
21597
					}
21598
					if (left !== zoomedMin) { // it has actually moved
21599
						scroller.fixedWidth = range; // #1370
21600
 
21601
						ext = xAxis.toFixedRange(left, left + range, null, fixedMax);
21602
						baseXAxis.setExtremes(
21603
							ext.min,
21604
							ext.max,
21605
							true,
21606
							false,
21607
							{ trigger: 'navigator' }
21608
						);
21609
					}
21610
				}
21611
 
21612
			}
21613
		};
21614
 
21615
		/**
21616
		 * Event handler for the mouse move event.
21617
		 */
21618
		scroller.mouseMoveHandler = function (e) {
21619
			var scrollbarHeight = scroller.scrollbarHeight,
21620
				navigatorLeft = scroller.navigatorLeft,
21621
				navigatorWidth = scroller.navigatorWidth,
21622
				scrollerLeft = scroller.scrollerLeft,
21623
				scrollerWidth = scroller.scrollerWidth,
21624
				range = scroller.range,
21625
				chartX;
21626
 
21627
			// In iOS, a mousemove event with e.pageX === 0 is fired when holding the finger
21628
			// down in the center of the scrollbar. This should be ignored.
21629
			if (e.pageX !== 0) {
21630
 
21631
				e = chart.pointer.normalize(e);
21632
				chartX = e.chartX;
21633
 
21634
				// validation for handle dragging
21635
				if (chartX < navigatorLeft) {
21636
					chartX = navigatorLeft;
21637
				} else if (chartX > scrollerLeft + scrollerWidth - scrollbarHeight) {
21638
					chartX = scrollerLeft + scrollerWidth - scrollbarHeight;
21639
				}
21640
 
21641
				// drag left handle
21642
				if (scroller.grabbedLeft) {
21643
					hasDragged = true;
21644
					scroller.render(0, 0, chartX - navigatorLeft, scroller.otherHandlePos);
21645
 
21646
				// drag right handle
21647
				} else if (scroller.grabbedRight) {
21648
					hasDragged = true;
21649
					scroller.render(0, 0, scroller.otherHandlePos, chartX - navigatorLeft);
21650
 
21651
				// drag scrollbar or open area in navigator
21652
				} else if (scroller.grabbedCenter) {
21653
 
21654
					hasDragged = true;
21655
					if (chartX < dragOffset) { // outside left
21656
						chartX = dragOffset;
21657
					} else if (chartX > navigatorWidth + dragOffset - range) { // outside right
21658
						chartX = navigatorWidth + dragOffset - range;
21659
					}
21660
 
21661
					scroller.render(0, 0, chartX - dragOffset, chartX - dragOffset + range);
21662
 
21663
				}
21664
				if (hasDragged && scroller.scrollbarOptions.liveRedraw) {
21665
					setTimeout(function () {
21666
						scroller.mouseUpHandler(e);
21667
					}, 0);
21668
				}
21669
			}
21670
		};
21671
 
21672
		/**
21673
		 * Event handler for the mouse up event.
21674
		 */
21675
		scroller.mouseUpHandler = function (e) {
21676
			var ext,
21677
				fixedMin,
21678
				fixedMax;
21679
 
21680
			if (hasDragged) {
21681
				// When dragging one handle, make sure the other one doesn't change
21682
				if (scroller.zoomedMin === scroller.otherHandlePos) {
21683
					fixedMin = scroller.fixedExtreme;
21684
				} else if (scroller.zoomedMax === scroller.otherHandlePos) {
21685
					fixedMax = scroller.fixedExtreme;
21686
				}
21687
 
21688
				ext = xAxis.toFixedRange(scroller.zoomedMin, scroller.zoomedMax, fixedMin, fixedMax);
21689
				chart.xAxis[0].setExtremes(
21690
					ext.min,
21691
					ext.max,
21692
					true,
21693
					false,
21694
					{
21695
						trigger: 'navigator',
21696
						triggerOp: 'navigator-drag',
21697
						DOMEvent: e // #1838
21698
					}
21699
				);
21700
			}
21701
 
21702
			if (e.type !== 'mousemove') {
21703
				scroller.grabbedLeft = scroller.grabbedRight = scroller.grabbedCenter = scroller.fixedWidth =
21704
					scroller.fixedExtreme = scroller.otherHandlePos = hasDragged = dragOffset = null;
21705
			}
21706
 
21707
		};
21708
 
21709
 
21710
 
21711
		var xAxisIndex = chart.xAxis.length,
21712
			yAxisIndex = chart.yAxis.length;
21713
 
21714
		// make room below the chart
21715
		chart.extraBottomMargin = scroller.outlineHeight + navigatorOptions.margin;
21716
 
21717
		if (scroller.navigatorEnabled) {
21718
			// an x axis is required for scrollbar also
21719
			scroller.xAxis = xAxis = new Axis(chart, merge({
21720
				// inherit base xAxis' break and ordinal options
21721
				breaks: baseSeries && baseSeries.xAxis.options.breaks,
21722
				ordinal: baseSeries && baseSeries.xAxis.options.ordinal
21723
			}, navigatorOptions.xAxis, {
21724
				id: 'navigator-x-axis',
21725
				isX: true,
21726
				type: 'datetime',
21727
				index: xAxisIndex,
21728
				height: height,
21729
				offset: 0,
21730
				offsetLeft: scrollbarHeight,
21731
				offsetRight: -scrollbarHeight,
21732
				keepOrdinalPadding: true, // #2436
21733
				startOnTick: false,
21734
				endOnTick: false,
21735
				minPadding: 0,
21736
				maxPadding: 0,
21737
				zoomEnabled: false
21738
			}));
21739
 
21740
			scroller.yAxis = yAxis = new Axis(chart, merge(navigatorOptions.yAxis, {
21741
				id: 'navigator-y-axis',
21742
				alignTicks: false,
21743
				height: height,
21744
				offset: 0,
21745
				index: yAxisIndex,
21746
				zoomEnabled: false
21747
			}));
21748
 
21749
			// If we have a base series, initialize the navigator series
21750
			if (baseSeries || navigatorOptions.series.data) {
21751
				scroller.addBaseSeries();
21752
 
21753
			// If not, set up an event to listen for added series
21754
			} else if (chart.series.length === 0) {
21755
 
21756
				wrap(chart, 'redraw', function (proceed, animation) {
21757
					// We've got one, now add it as base and reset chart.redraw
21758
					if (chart.series.length > 0 && !scroller.series) {
21759
						scroller.setBaseSeries();
21760
						chart.redraw = proceed; // reset
21761
					}
21762
					proceed.call(chart, animation);
21763
				});
21764
			}
21765
 
21766
 
21767
		// in case of scrollbar only, fake an x axis to get translation
21768
		} else {
21769
			scroller.xAxis = xAxis = {
21770
				translate: function (value, reverse) {
21771
					var axis = chart.xAxis[0],
21772
						ext = axis.getExtremes(),
21773
						scrollTrackWidth = chart.plotWidth - 2 * scrollbarHeight,
21774
						min = numExt('min', axis.options.min, ext.dataMin),
21775
						valueRange = numExt('max', axis.options.max, ext.dataMax) - min;
21776
 
21777
					return reverse ?
21778
						// from pixel to value
21779
						(value * valueRange / scrollTrackWidth) + min :
21780
						// from value to pixel
21781
						scrollTrackWidth * (value - min) / valueRange;
21782
				},
21783
				toFixedRange: Axis.prototype.toFixedRange
21784
			};
21785
		}
21786
 
21787
 
21788
		/**
21789
		 * For stock charts, extend the Chart.getMargins method so that we can set the final top position
21790
		 * of the navigator once the height of the chart, including the legend, is determined. #367.
21791
		 */
21792
		wrap(chart, 'getMargins', function (proceed) {
21793
 
21794
			var legend = this.legend,
21795
				legendOptions = legend.options;
21796
 
21797
			proceed.apply(this, [].slice.call(arguments, 1));
21798
 
21799
			// Compute the top position
21800
			scroller.top = top = scroller.navigatorOptions.top ||
21801
				this.chartHeight - scroller.height - scroller.scrollbarHeight - this.spacing[2] -
21802
						(legendOptions.verticalAlign === 'bottom' && legendOptions.enabled && !legendOptions.floating ?
21803
							legend.legendHeight + pick(legendOptions.margin, 10) : 0);
21804
 
21805
			if (xAxis && yAxis) { // false if navigator is disabled (#904)
21806
 
21807
				xAxis.options.top = yAxis.options.top = top;
21808
 
21809
				xAxis.setAxisSize();
21810
				yAxis.setAxisSize();
21811
			}
21812
		});
21813
 
21814
 
21815
		scroller.addEvents();
21816
	},
21817
 
21818
	/**
21819
	 * Get the union data extremes of the chart - the outer data extremes of the base
21820
	 * X axis and the navigator axis.
21821
	 */
21822
	getUnionExtremes: function (returnFalseOnNoBaseSeries) {
21823
		var baseAxis = this.chart.xAxis[0],
21824
			navAxis = this.xAxis,
21825
			navAxisOptions = navAxis.options,
21826
			baseAxisOptions = baseAxis.options,
21827
			ret;
21828
 
21829
		if (!returnFalseOnNoBaseSeries || baseAxis.dataMin !== null) {
21830
			ret = {
21831
				dataMin: pick( // #4053
21832
					navAxisOptions && navAxisOptions.min,
21833
					numExt(
21834
						'min',
21835
						baseAxisOptions.min,
21836
						baseAxis.dataMin,
21837
						navAxis.dataMin
21838
					)
21839
				),
21840
				dataMax: pick(
21841
					navAxisOptions && navAxisOptions.max,
21842
					numExt(
21843
						'max',
21844
						baseAxisOptions.max,
21845
						baseAxis.dataMax,
21846
						navAxis.dataMax
21847
					)
21848
				)
21849
			};
21850
		}
21851
		return ret;
21852
	},
21853
 
21854
	/**
21855
	 * Set the base series. With a bit of modification we should be able to make
21856
	 * this an API method to be called from the outside
21857
	 */
21858
	setBaseSeries: function (baseSeriesOption) {
21859
		var chart = this.chart;
21860
 
21861
		baseSeriesOption = baseSeriesOption || chart.options.navigator.baseSeries;
21862
 
21863
		// If we're resetting, remove the existing series
21864
		if (this.series) {
21865
			this.series.remove();
21866
		}
21867
 
21868
		// Set the new base series
21869
		this.baseSeries = chart.series[baseSeriesOption] ||
21870
			(typeof baseSeriesOption === 'string' && chart.get(baseSeriesOption)) ||
21871
			chart.series[0];
21872
 
21873
		// When run after render, this.xAxis already exists
21874
		if (this.xAxis) {
21875
			this.addBaseSeries();
21876
		}
21877
	},
21878
 
21879
	addBaseSeries: function () {
21880
		var baseSeries = this.baseSeries,
21881
			baseOptions = baseSeries ? baseSeries.options : {},
21882
			baseData = baseOptions.data,
21883
			mergedNavSeriesOptions,
21884
			navigatorSeriesOptions = this.navigatorOptions.series,
21885
			navigatorData;
21886
 
21887
		// remove it to prevent merging one by one
21888
		navigatorData = navigatorSeriesOptions.data;
21889
		this.hasNavigatorData = !!navigatorData;
21890
 
21891
		// Merge the series options
21892
		mergedNavSeriesOptions = merge(baseOptions, navigatorSeriesOptions, {
21893
			enableMouseTracking: false,
21894
			group: 'nav', // for columns
21895
			padXAxis: false,
21896
			xAxis: 'navigator-x-axis',
21897
			yAxis: 'navigator-y-axis',
21898
			name: 'Navigator',
21899
			showInLegend: false,
21900
			isInternal: true,
21901
			visible: true
21902
		});
21903
 
21904
		// set the data back
21905
		mergedNavSeriesOptions.data = navigatorData || baseData;
21906
 
21907
		// add the series
21908
		this.series = this.chart.initSeries(mergedNavSeriesOptions);
21909
 
21910
		// Respond to updated data in the base series.
21911
		// Abort if lazy-loading data from the server.
21912
		if (baseSeries && this.navigatorOptions.adaptToUpdatedData !== false) {
21913
			addEvent(baseSeries, 'updatedData', this.updatedDataHandler);
21914
			// Survive Series.update()
21915
			baseSeries.userOptions.events = extend(baseSeries.userOptions.event, { updatedData: this.updatedDataHandler });
21916
 
21917
		}
21918
	},
21919
 
21920
	updatedDataHandler: function () {
21921
		var scroller = this.chart.scroller,
21922
			baseSeries = scroller.baseSeries,
21923
			baseXAxis = baseSeries.xAxis,
21924
			baseExtremes = baseXAxis.getExtremes(),
21925
			baseMin = baseExtremes.min,
21926
			baseMax = baseExtremes.max,
21927
			baseDataMin = baseExtremes.dataMin,
21928
			baseDataMax = baseExtremes.dataMax,
21929
			range = baseMax - baseMin,
21930
			stickToMin,
21931
			stickToMax,
21932
			newMax,
21933
			newMin,
21934
			doRedraw,
21935
			navigatorSeries = scroller.series,
21936
			navXData = navigatorSeries.xData,
21937
			hasSetExtremes = !!baseXAxis.setExtremes;
21938
 
21939
		// detect whether to move the range
21940
		stickToMax = baseMax >= navXData[navXData.length - 1] - (this.closestPointRange || 0); // #570
21941
		stickToMin = baseMin <= baseDataMin;
21942
 
21943
		// set the navigator series data to the new data of the base series
21944
		if (!scroller.hasNavigatorData) {
21945
			navigatorSeries.options.pointStart = baseSeries.xData[0];
21946
			navigatorSeries.setData(baseSeries.options.data, false);
21947
			doRedraw = true;
21948
		}
21949
 
21950
		// if the zoomed range is already at the min, move it to the right as new data
21951
		// comes in
21952
		if (stickToMin) {
21953
			newMin = baseDataMin;
21954
			newMax = newMin + range;
21955
		}
21956
 
21957
		// if the zoomed range is already at the max, move it to the right as new data
21958
		// comes in
21959
		if (stickToMax) {
21960
			newMax = baseDataMax;
21961
			if (!stickToMin) { // if stickToMin is true, the new min value is set above
21962
				newMin = mathMax(newMax - range, navigatorSeries.xData[0]);
21963
			}
21964
		}
21965
 
21966
		// update the extremes
21967
		if (hasSetExtremes && (stickToMin || stickToMax)) {
21968
			if (!isNaN(newMin)) {
21969
				baseXAxis.setExtremes(newMin, newMax, true, false, { trigger: 'updatedData' });
21970
			}
21971
 
21972
		// if it is not at any edge, just move the scroller window to reflect the new series data
21973
		} else {
21974
			if (doRedraw) {
21975
				this.chart.redraw(false);
21976
			}
21977
 
21978
			scroller.render(
21979
				mathMax(baseMin, baseDataMin),
21980
				mathMin(baseMax, baseDataMax)
21981
			);
21982
		}
21983
	},
21984
 
21985
	/**
21986
	 * Destroys allocated elements.
21987
	 */
21988
	destroy: function () {
21989
		var scroller = this;
21990
 
21991
		// Disconnect events added in addEvents
21992
		scroller.removeEvents();
21993
 
21994
		// Destroy properties
21995
		each([scroller.xAxis, scroller.yAxis, scroller.leftShade, scroller.rightShade, scroller.outline, scroller.scrollbarTrack, scroller.scrollbarRifles, scroller.scrollbarGroup, scroller.scrollbar], function (prop) {
21996
			if (prop && prop.destroy) {
21997
				prop.destroy();
21998
			}
21999
		});
22000
		scroller.xAxis = scroller.yAxis = scroller.leftShade = scroller.rightShade = scroller.outline = scroller.scrollbarTrack = scroller.scrollbarRifles = scroller.scrollbarGroup = scroller.scrollbar = null;
22001
 
22002
		// Destroy elements in collection
22003
		each([scroller.scrollbarButtons, scroller.handles, scroller.elementsToDestroy], function (coll) {
22004
			destroyObjectProperties(coll);
22005
		});
22006
	}
22007
};
22008
 
22009
Highcharts.Scroller = Scroller;
22010
 
22011
 
22012
/**
22013
 * For Stock charts, override selection zooming with some special features because
22014
 * X axis zooming is already allowed by the Navigator and Range selector.
22015
 */
22016
wrap(Axis.prototype, 'zoom', function (proceed, newMin, newMax) {
22017
	var chart = this.chart,
22018
		chartOptions = chart.options,
22019
		zoomType = chartOptions.chart.zoomType,
22020
		previousZoom,
22021
		navigator = chartOptions.navigator,
22022
		rangeSelector = chartOptions.rangeSelector,
22023
		ret;
22024
 
22025
	if (this.isXAxis && ((navigator && navigator.enabled) ||
22026
			(rangeSelector && rangeSelector.enabled))) {
22027
 
22028
		// For x only zooming, fool the chart.zoom method not to create the zoom button
22029
		// because the property already exists
22030
		if (zoomType === 'x') {
22031
			chart.resetZoomButton = 'blocked';
22032
 
22033
		// For y only zooming, ignore the X axis completely
22034
		} else if (zoomType === 'y') {
22035
			ret = false;
22036
 
22037
		// For xy zooming, record the state of the zoom before zoom selection, then when
22038
		// the reset button is pressed, revert to this state
22039
		} else if (zoomType === 'xy') {
22040
			previousZoom = this.previousZoom;
22041
			if (defined(newMin)) {
22042
				this.previousZoom = [this.min, this.max];
22043
			} else if (previousZoom) {
22044
				newMin = previousZoom[0];
22045
				newMax = previousZoom[1];
22046
				delete this.previousZoom;
22047
			}
22048
		}
22049
 
22050
	}
22051
	return ret !== UNDEFINED ? ret : proceed.call(this, newMin, newMax);
22052
});
22053
 
22054
// Initialize scroller for stock charts
22055
wrap(Chart.prototype, 'init', function (proceed, options, callback) {
22056
 
22057
	addEvent(this, 'beforeRender', function () {
22058
		var options = this.options;
22059
		if (options.navigator.enabled || options.scrollbar.enabled) {
22060
			this.scroller = new Scroller(this);
22061
		}
22062
	});
22063
 
22064
	proceed.call(this, options, callback);
22065
 
22066
});
22067
 
22068
// Pick up badly formatted point options to addPoint
22069
wrap(Series.prototype, 'addPoint', function (proceed, options, redraw, shift, animation) {
22070
	var turboThreshold = this.options.turboThreshold;
22071
	if (turboThreshold && this.xData.length > turboThreshold && isObject(options) && !isArray(options) && this.chart.scroller) {
22072
		error(20, true);
22073
	}
22074
	proceed.call(this, options, redraw, shift, animation);
22075
});
22076
 
22077
/* ****************************************************************************
22078
 * End Scroller code														  *
22079
 *****************************************************************************/
22080
/* ****************************************************************************
22081
 * Start Range Selector code												  *
22082
 *****************************************************************************/
22083
extend(defaultOptions, {
22084
	rangeSelector: {
22085
		// allButtonsEnabled: false,
22086
		// enabled: true,
22087
		// buttons: {Object}
22088
		// buttonSpacing: 0,
22089
		buttonTheme: {
22090
			width: 28,
22091
			height: 18,
22092
			fill: '#f7f7f7',
22093
			padding: 2,
22094
			r: 0,
22095
			'stroke-width': 0,
22096
			style: {
22097
				color: '#444',
22098
				cursor: 'pointer',
22099
				fontWeight: 'normal'
22100
			},
22101
			zIndex: 7, // #484, #852
22102
			states: {
22103
				hover: {
22104
					fill: '#e7e7e7'
22105
				},
22106
				select: {
22107
					fill: '#e7f0f9',
22108
					style: {
22109
						color: 'black',
22110
						fontWeight: 'bold'
22111
					}
22112
				}
22113
			}
22114
		},
22115
		inputPosition: {
22116
			align: 'right'
22117
		},
22118
		// inputDateFormat: '%b %e, %Y',
22119
		// inputEditDateFormat: '%Y-%m-%d',
22120
		// inputEnabled: true,
22121
		// inputStyle: {},
22122
		labelStyle: {
22123
			color: '#666'
22124
		}
22125
		// selected: undefined
22126
	}
22127
});
22128
defaultOptions.lang = merge(defaultOptions.lang, {
22129
	rangeSelectorZoom: 'Zoom',
22130
	rangeSelectorFrom: 'From',
22131
	rangeSelectorTo: 'To'
22132
});
22133
 
22134
/**
22135
 * The object constructor for the range selector
22136
 * @param {Object} chart
22137
 */
22138
function RangeSelector(chart) {
22139
 
22140
	// Run RangeSelector
22141
	this.init(chart);
22142
}
22143
 
22144
RangeSelector.prototype = {
22145
	/**
22146
	 * The method to run when one of the buttons in the range selectors is clicked
22147
	 * @param {Number} i The index of the button
22148
	 * @param {Object} rangeOptions
22149
	 * @param {Boolean} redraw
22150
	 */
22151
	clickButton: function (i, redraw) {
22152
		var rangeSelector = this,
22153
			selected = rangeSelector.selected,
22154
			chart = rangeSelector.chart,
22155
			buttons = rangeSelector.buttons,
22156
			rangeOptions = rangeSelector.buttonOptions[i],
22157
			baseAxis = chart.xAxis[0],
22158
			unionExtremes = (chart.scroller && chart.scroller.getUnionExtremes()) || baseAxis || {},
22159
			dataMin = unionExtremes.dataMin,
22160
			dataMax = unionExtremes.dataMax,
22161
			newMin,
22162
			newMax = baseAxis && mathRound(mathMin(baseAxis.max, pick(dataMax, baseAxis.max))), // #1568
22163
			now,
22164
			type = rangeOptions.type,
22165
			baseXAxisOptions,
22166
			range = rangeOptions._range,
22167
			rangeMin,
22168
			year,
22169
			minSetting,
22170
			rangeSetting,
22171
			ctx,
22172
			dataGrouping = rangeOptions.dataGrouping;
22173
 
22174
		if (dataMin === null || dataMax === null || // chart has no data, base series is removed
22175
				i === rangeSelector.selected) { // same button is clicked twice
22176
			return;
22177
		}
22178
 
22179
		// Set the fixed range before range is altered
22180
		chart.fixedRange = range;
22181
 
22182
		// Apply dataGrouping associated to button
22183
		if (dataGrouping) {
22184
			this.forcedDataGrouping = true;
22185
			Axis.prototype.setDataGrouping.call(baseAxis || { chart: this.chart }, dataGrouping, false);
22186
		}
22187
 
22188
		// Apply range
22189
		if (type === 'month' || type === 'year') {
22190
			if (!baseAxis) {
22191
				// This is set to the user options and picked up later when the axis is instantiated
22192
				// so that we know the min and max.
22193
				range = rangeOptions;
22194
			} else {
22195
				ctx = {
22196
					range: rangeOptions,
22197
					max: newMax,
22198
					dataMin: dataMin,
22199
					dataMax: dataMax
22200
				};
22201
				newMin = baseAxis.minFromRange.call(ctx);
22202
				if (typeof ctx.newMax === 'number') {
22203
					newMax = ctx.newMax;
22204
				}
22205
			}
22206
 
22207
		// Fixed times like minutes, hours, days
22208
		} else if (range) {
22209
			newMin = mathMax(newMax - range, dataMin);
22210
			newMax = mathMin(newMin + range, dataMax);
22211
 
22212
		} else if (type === 'ytd') {
22213
 
22214
			// On user clicks on the buttons, or a delayed action running from the beforeRender
22215
			// event (below), the baseAxis is defined.
22216
			if (baseAxis) {
22217
 
22218
				// When "ytd" is the pre-selected button for the initial view, its calculation
22219
				// is delayed and rerun in the beforeRender event (below). When the series
22220
				// are initialized, but before the chart is rendered, we have access to the xData
22221
				// array (#942).
22222
				if (dataMax === UNDEFINED) {
22223
					dataMin = Number.MAX_VALUE;
22224
					dataMax = Number.MIN_VALUE;
22225
					each(chart.series, function (series) {
22226
						var xData = series.xData; // reassign it to the last item
22227
						dataMin = mathMin(xData[0], dataMin);
22228
						dataMax = mathMax(xData[xData.length - 1], dataMax);
22229
					});
22230
					redraw = false;
22231
				}
22232
				now = new Date(dataMax);
22233
				year = now.getFullYear();
22234
				newMin = rangeMin = mathMax(dataMin || 0, Date.UTC(year, 0, 1));
22235
				now = now.getTime();
22236
				newMax = mathMin(dataMax || now, now);
22237
 
22238
			// "ytd" is pre-selected. We don't yet have access to processed point and extremes data
22239
			// (things like pointStart and pointInterval are missing), so we delay the process (#942)
22240
			} else {
22241
				addEvent(chart, 'beforeRender', function () {
22242
					rangeSelector.clickButton(i);
22243
				});
22244
				return;
22245
			}
22246
		} else if (type === 'all' && baseAxis) {
22247
			newMin = dataMin;
22248
			newMax = dataMax;
22249
		}
22250
 
22251
		// Deselect previous button
22252
		if (buttons[selected]) {
22253
			buttons[selected].setState(0);
22254
		}
22255
		// Select this button
22256
		if (buttons[i]) {
22257
			buttons[i].setState(2);
22258
		}
22259
 
22260
		// Update the chart
22261
		if (!baseAxis) {
22262
			// Axis not yet instanciated. Temporarily set min and range
22263
			// options and remove them on chart load (#4317).
22264
			baseXAxisOptions = chart.options.xAxis[0];
22265
			rangeSetting = baseXAxisOptions.range;
22266
			baseXAxisOptions.range = range;
22267
			minSetting = baseXAxisOptions.min;
22268
			baseXAxisOptions.min = rangeMin;
22269
			rangeSelector.setSelected(i);
22270
			addEvent(chart, 'load', function resetMinAndRange() {
22271
				baseXAxisOptions.range = rangeSetting;
22272
				baseXAxisOptions.min = minSetting;
22273
			});
22274
		} else {
22275
			// Existing axis object. Set extremes after render time.
22276
			baseAxis.setExtremes(
22277
				newMin,
22278
				newMax,
22279
				pick(redraw, 1),
22280
				0,
22281
				{
22282
					trigger: 'rangeSelectorButton',
22283
					rangeSelectorButton: rangeOptions
22284
				}
22285
			);
22286
			rangeSelector.setSelected(i);
22287
		}
22288
	},
22289
 
22290
	/**
22291
	 * Set the selected option. This method only sets the internal flag, it doesn't
22292
	 * update the buttons or the actual zoomed range.
22293
	 */
22294
	setSelected: function (selected) {
22295
		this.selected = this.options.selected = selected;
22296
	},
22297
 
22298
	/**
22299
	 * The default buttons for pre-selecting time frames
22300
	 */
22301
	defaultButtons: [{
22302
		type: 'month',
22303
		count: 1,
22304
		text: '1m'
22305
	}, {
22306
		type: 'month',
22307
		count: 3,
22308
		text: '3m'
22309
	}, {
22310
		type: 'month',
22311
		count: 6,
22312
		text: '6m'
22313
	}, {
22314
		type: 'ytd',
22315
		text: 'YTD'
22316
	}, {
22317
		type: 'year',
22318
		count: 1,
22319
		text: '1y'
22320
	}, {
22321
		type: 'all',
22322
		text: 'All'
22323
	}],
22324
 
22325
	/**
22326
	 * Initialize the range selector
22327
	 */
22328
	init: function (chart) {
22329
 
22330
		var rangeSelector = this,
22331
			options = chart.options.rangeSelector,
22332
			buttonOptions = options.buttons || [].concat(rangeSelector.defaultButtons),
22333
			selectedOption = options.selected,
22334
			blurInputs = rangeSelector.blurInputs = function () {
22335
				var minInput = rangeSelector.minInput,
22336
					maxInput = rangeSelector.maxInput;
22337
				if (minInput && minInput.blur) { //#3274 in some case blur is not defined
22338
					fireEvent(minInput, 'blur'); //#3274
22339
				}
22340
				if (maxInput && maxInput.blur) { //#3274 in some case blur is not defined
22341
					fireEvent(maxInput, 'blur'); //#3274
22342
				}
22343
			};
22344
 
22345
		rangeSelector.chart = chart;
22346
		rangeSelector.options = options;
22347
		rangeSelector.buttons = [];
22348
 
22349
		chart.extraTopMargin = 35;
22350
		rangeSelector.buttonOptions = buttonOptions;
22351
 
22352
		addEvent(chart.container, 'mousedown', blurInputs);
22353
		addEvent(chart, 'resize', blurInputs);
22354
 
22355
		// Extend the buttonOptions with actual range
22356
		each(buttonOptions, rangeSelector.computeButtonRange);
22357
 
22358
		// zoomed range based on a pre-selected button index
22359
		if (selectedOption !== UNDEFINED && buttonOptions[selectedOption]) {
22360
			this.clickButton(selectedOption, false);
22361
		}
22362
 
22363
 
22364
		addEvent(chart, 'load', function () {
22365
			// If a data grouping is applied to the current button, release it when extremes change
22366
			addEvent(chart.xAxis[0], 'setExtremes', function (e) {
22367
				if (this.max - this.min !== chart.fixedRange && e.trigger !== 'rangeSelectorButton' &&
22368
						e.trigger !== 'updatedData' && rangeSelector.forcedDataGrouping) {
22369
					this.setDataGrouping(false, false);
22370
				}
22371
			});
22372
			// Normalize the pressed button whenever a new range is selected
22373
			addEvent(chart.xAxis[0], 'afterSetExtremes', function () {
22374
				rangeSelector.updateButtonStates(true);
22375
			});
22376
		});
22377
	},
22378
 
22379
	/**
22380
	 * Dynamically update the range selector buttons after a new range has been set
22381
	 */
22382
	updateButtonStates: function (updating) {
22383
		var rangeSelector = this,
22384
			chart = this.chart,
22385
			baseAxis = chart.xAxis[0],
22386
			unionExtremes = (chart.scroller && chart.scroller.getUnionExtremes()) || baseAxis,
22387
			dataMin = unionExtremes.dataMin,
22388
			dataMax = unionExtremes.dataMax,
22389
			selected = rangeSelector.selected,
22390
			allButtonsEnabled = rangeSelector.options.allButtonsEnabled,
22391
			buttons = rangeSelector.buttons;
22392
 
22393
		if (updating && chart.fixedRange !== mathRound(baseAxis.max - baseAxis.min)) {
22394
			if (buttons[selected]) {
22395
				buttons[selected].setState(0);
22396
			}
22397
			rangeSelector.setSelected(null);
22398
		}
22399
 
22400
		each(rangeSelector.buttonOptions, function (rangeOptions, i) {
22401
			var actualRange = mathRound(baseAxis.max - baseAxis.min),
22402
				range = rangeOptions._range,
22403
				type = rangeOptions.type,
22404
				count = rangeOptions.count || 1,
22405
				// Disable buttons where the range exceeds what is allowed in the current view
22406
				isTooGreatRange = range > dataMax - dataMin,
22407
				// Disable buttons where the range is smaller than the minimum range
22408
				isTooSmallRange = range < baseAxis.minRange,
22409
				// Disable the All button if we're already showing all
22410
				isAllButAlreadyShowingAll = rangeOptions.type === 'all' && baseAxis.max - baseAxis.min >= dataMax - dataMin &&
22411
					buttons[i].state !== 2,
22412
				// Disable the YTD button if the complete range is within the same year
22413
				isYTDButNotAvailable = rangeOptions.type === 'ytd' && dateFormat('%Y', dataMin) === dateFormat('%Y', dataMax),
22414
				// Set a button on export
22415
				isSelectedForExport = chart.renderer.forExport && i === selected,
22416
 
22417
				isSameRange = range === actualRange;
22418
 
22419
			// Months and years have a variable range so we check the extremes
22420
			if ((type === 'month' || type === 'year') && (actualRange >= { month: 28, year: 365 }[type] * 24 * 36e5 * count) &&
22421
					(actualRange <= { month: 31, year: 366 }[type] * 24 * 36e5 * count)) {
22422
				isSameRange = true;
22423
			}
22424
			// The new zoom area happens to match the range for a button - mark it selected.
22425
			// This happens when scrolling across an ordinal gap. It can be seen in the intraday
22426
			// demos when selecting 1h and scroll across the night gap.
22427
			if (isSelectedForExport || (isSameRange && i !== selected)) {
22428
				rangeSelector.setSelected(i);
22429
				buttons[i].setState(2);
22430
 
22431
			} else if (!allButtonsEnabled && (isTooGreatRange || isTooSmallRange || isAllButAlreadyShowingAll || isYTDButNotAvailable)) {
22432
				buttons[i].setState(3);
22433
 
22434
			} else if (buttons[i].state === 3) {
22435
				buttons[i].setState(0);
22436
			}
22437
		});
22438
	},
22439
 
22440
	/**
22441
	 * Compute and cache the range for an individual button
22442
	 */
22443
	computeButtonRange: function (rangeOptions) {
22444
		var type = rangeOptions.type,
22445
			count = rangeOptions.count || 1,
22446
 
22447
			// these time intervals have a fixed number of milliseconds, as opposed
22448
			// to month, ytd and year
22449
			fixedTimes = {
22450
				millisecond: 1,
22451
				second: 1000,
22452
				minute: 60 * 1000,
22453
				hour: 3600 * 1000,
22454
				day: 24 * 3600 * 1000,
22455
				week: 7 * 24 * 3600 * 1000
22456
			};
22457
 
22458
		// Store the range on the button object
22459
		if (fixedTimes[type]) {
22460
			rangeOptions._range = fixedTimes[type] * count;
22461
		} else if (type === 'month' || type === 'year') {
22462
			rangeOptions._range = { month: 30, year: 365 }[type] * 24 * 36e5 * count;
22463
		}
22464
	},
22465
 
22466
	/**
22467
	 * Set the internal and displayed value of a HTML input for the dates
22468
	 * @param {String} name
22469
	 * @param {Number} time
22470
	 */
22471
	setInputValue: function (name, time) {
22472
		var options = this.chart.options.rangeSelector;
22473
 
22474
		if (defined(time)) {
22475
			this[name + 'Input'].HCTime = time;
22476
		}
22477
 
22478
		this[name + 'Input'].value = dateFormat(options.inputEditDateFormat || '%Y-%m-%d', this[name + 'Input'].HCTime);
22479
		this[name + 'DateBox'].attr({ text: dateFormat(options.inputDateFormat || '%b %e, %Y', this[name + 'Input'].HCTime) });
22480
	},
22481
 
22482
	showInput: function (name) {
22483
		var inputGroup = this.inputGroup,
22484
			dateBox = this[name + 'DateBox'];
22485
 
22486
		css(this[name + 'Input'], {
22487
			left: (inputGroup.translateX + dateBox.x) + PX,
22488
			top: inputGroup.translateY + PX,
22489
			width: (dateBox.width - 2) + PX,
22490
			height: (dateBox.height - 2) + PX,
22491
			border: '2px solid silver'
22492
		});
22493
	},
22494
 
22495
	hideInput: function (name) {
22496
		css(this[name + 'Input'], {
22497
			border: 0,
22498
			width: '1px',
22499
			height: '1px'
22500
		});
22501
		this.setInputValue(name);
22502
	},
22503
 
22504
	/**
22505
	 * Draw either the 'from' or the 'to' HTML input box of the range selector
22506
	 * @param {Object} name
22507
	 */
22508
	drawInput: function (name) {
22509
		var rangeSelector = this,
22510
			chart = rangeSelector.chart,
22511
			chartStyle = chart.renderer.style,
22512
			renderer = chart.renderer,
22513
			options = chart.options.rangeSelector,
22514
			lang = defaultOptions.lang,
22515
			div = rangeSelector.div,
22516
			isMin = name === 'min',
22517
			input,
22518
			label,
22519
			dateBox,
22520
			inputGroup = this.inputGroup;
22521
 
22522
		// Create the text label
22523
		this[name + 'Label'] = label = renderer.label(lang[isMin ? 'rangeSelectorFrom' : 'rangeSelectorTo'], this.inputGroup.offset)
22524
			.attr({
22525
				padding: 2
22526
			})
22527
			.css(merge(chartStyle, options.labelStyle))
22528
			.add(inputGroup);
22529
		inputGroup.offset += label.width + 5;
22530
 
22531
		// Create an SVG label that shows updated date ranges and and records click events that
22532
		// bring in the HTML input.
22533
		this[name + 'DateBox'] = dateBox = renderer.label('', inputGroup.offset)
22534
			.attr({
22535
				padding: 2,
22536
				width: options.inputBoxWidth || 90,
22537
				height: options.inputBoxHeight || 17,
22538
				stroke: options.inputBoxBorderColor || 'silver',
22539
				'stroke-width': 1
22540
			})
22541
			.css(merge({
22542
				textAlign: 'center',
22543
				color: '#444'
22544
			}, chartStyle, options.inputStyle))
22545
			.on('click', function () {
22546
				rangeSelector.showInput(name); // If it is already focused, the onfocus event doesn't fire (#3713)
22547
				rangeSelector[name + 'Input'].focus();
22548
			})
22549
			.add(inputGroup);
22550
		inputGroup.offset += dateBox.width + (isMin ? 10 : 0);
22551
 
22552
 
22553
		// Create the HTML input element. This is rendered as 1x1 pixel then set to the right size
22554
		// when focused.
22555
		this[name + 'Input'] = input = createElement('input', {
22556
			name: name,
22557
			className: PREFIX + 'range-selector',
22558
			type: 'text'
22559
		}, extend({
22560
			position: ABSOLUTE,
22561
			border: 0,
22562
			width: '1px', // Chrome needs a pixel to see it
22563
			height: '1px',
22564
			padding: 0,
22565
			textAlign: 'center',
22566
			fontSize: chartStyle.fontSize,
22567
			fontFamily: chartStyle.fontFamily,
22568
			top: chart.plotTop + PX // prevent jump on focus in Firefox
22569
		}, options.inputStyle), div);
22570
 
22571
		// Blow up the input box
22572
		input.onfocus = function () {
22573
			rangeSelector.showInput(name);
22574
		};
22575
		// Hide away the input box
22576
		input.onblur = function () {
22577
			rangeSelector.hideInput(name);
22578
		};
22579
 
22580
		// handle changes in the input boxes
22581
		input.onchange = function () {
22582
			var inputValue = input.value,
22583
				value = (options.inputDateParser || Date.parse)(inputValue),
22584
				xAxis = chart.xAxis[0],
22585
				dataMin = xAxis.dataMin,
22586
				dataMax = xAxis.dataMax;
22587
 
22588
			// If the value isn't parsed directly to a value by the browser's Date.parse method,
22589
			// like YYYY-MM-DD in IE, try parsing it a different way
22590
			if (isNaN(value)) {
22591
				value = inputValue.split('-');
22592
				value = Date.UTC(pInt(value[0]), pInt(value[1]) - 1, pInt(value[2]));
22593
			}
22594
 
22595
			if (!isNaN(value)) {
22596
 
22597
				// Correct for timezone offset (#433)
22598
				if (!defaultOptions.global.useUTC) {
22599
					value = value + new Date().getTimezoneOffset() * 60 * 1000;
22600
				}
22601
 
22602
				// Validate the extremes. If it goes beyound the data min or max, use the
22603
				// actual data extreme (#2438).
22604
				if (isMin) {
22605
					if (value > rangeSelector.maxInput.HCTime) {
22606
						value = UNDEFINED;
22607
					} else if (value < dataMin) {
22608
						value = dataMin;
22609
					}
22610
				} else {
22611
					if (value < rangeSelector.minInput.HCTime) {
22612
						value = UNDEFINED;
22613
					} else if (value > dataMax) {
22614
						value = dataMax;
22615
					}
22616
				}
22617
 
22618
				// Set the extremes
22619
				if (value !== UNDEFINED) {
22620
					chart.xAxis[0].setExtremes(
22621
						isMin ? value : xAxis.min,
22622
						isMin ? xAxis.max : value,
22623
						UNDEFINED,
22624
						UNDEFINED,
22625
						{ trigger: 'rangeSelectorInput' }
22626
					);
22627
				}
22628
			}
22629
		};
22630
	},
22631
 
22632
	/**
22633
	 * Get the position of the range selector buttons and inputs. This can be overridden from outside for custom positioning.
22634
	 */
22635
	getPosition: function () {
22636
		var chart = this.chart,
22637
			options = chart.options.rangeSelector,
22638
			buttonTop = pick((options.buttonPosition || {}).y, chart.plotTop - chart.axisOffset[0] - 35);
22639
 
22640
		return {
22641
			buttonTop: buttonTop,
22642
			inputTop: buttonTop - 10
22643
		};
22644
	},
22645
 
22646
	/**
22647
	 * Render the range selector including the buttons and the inputs. The first time render
22648
	 * is called, the elements are created and positioned. On subsequent calls, they are
22649
	 * moved and updated.
22650
	 * @param {Number} min X axis minimum
22651
	 * @param {Number} max X axis maximum
22652
	 */
22653
	render: function (min, max) {
22654
 
22655
		var rangeSelector = this,
22656
			chart = rangeSelector.chart,
22657
			renderer = chart.renderer,
22658
			container = chart.container,
22659
			chartOptions = chart.options,
22660
			navButtonOptions = chartOptions.exporting && chartOptions.navigation && chartOptions.navigation.buttonOptions,
22661
			options = chartOptions.rangeSelector,
22662
			buttons = rangeSelector.buttons,
22663
			lang = defaultOptions.lang,
22664
			div = rangeSelector.div,
22665
			inputGroup = rangeSelector.inputGroup,
22666
			buttonTheme = options.buttonTheme,
22667
			buttonPosition = options.buttonPosition || {},
22668
			inputEnabled = options.inputEnabled,
22669
			states = buttonTheme && buttonTheme.states,
22670
			plotLeft = chart.plotLeft,
22671
			buttonLeft,
22672
			pos = this.getPosition(),
22673
			buttonGroup = rangeSelector.group,
22674
			buttonBBox,
22675
			rendered = rangeSelector.rendered;
22676
 
22677
 
22678
		// create the elements
22679
		if (!rendered) {
22680
 
22681
			rangeSelector.group = buttonGroup = renderer.g('range-selector-buttons').add();
22682
 
22683
			rangeSelector.zoomText = renderer.text(lang.rangeSelectorZoom, pick(buttonPosition.x, plotLeft), 15)
22684
				.css(options.labelStyle)
22685
				.add(buttonGroup);
22686
 
22687
			// button starting position
22688
			buttonLeft = pick(buttonPosition.x, plotLeft) + rangeSelector.zoomText.getBBox().width + 5;
22689
 
22690
			each(rangeSelector.buttonOptions, function (rangeOptions, i) {
22691
				buttons[i] = renderer.button(
22692
						rangeOptions.text,
22693
						buttonLeft,
22694
						0,
22695
						function () {
22696
							rangeSelector.clickButton(i);
22697
							rangeSelector.isActive = true;
22698
						},
22699
						buttonTheme,
22700
						states && states.hover,
22701
						states && states.select,
22702
						states && states.disabled
22703
					)
22704
					.css({
22705
						textAlign: 'center'
22706
					})
22707
					.add(buttonGroup);
22708
 
22709
				// increase button position for the next button
22710
				buttonLeft += buttons[i].width + pick(options.buttonSpacing, 5);
22711
 
22712
				if (rangeSelector.selected === i) {
22713
					buttons[i].setState(2);
22714
				}
22715
			});
22716
 
22717
			rangeSelector.updateButtonStates();
22718
 
22719
			// first create a wrapper outside the container in order to make
22720
			// the inputs work and make export correct
22721
			if (inputEnabled !== false) {
22722
				rangeSelector.div = div = createElement('div', null, {
22723
					position: 'relative',
22724
					height: 0,
22725
					zIndex: 1 // above container
22726
				});
22727
 
22728
				container.parentNode.insertBefore(div, container);
22729
 
22730
				// Create the group to keep the inputs
22731
				rangeSelector.inputGroup = inputGroup = renderer.g('input-group')
22732
					.add();
22733
				inputGroup.offset = 0;
22734
 
22735
				rangeSelector.drawInput('min');
22736
				rangeSelector.drawInput('max');
22737
			}
22738
		}
22739
 
22740
		// Set or update the group position
22741
		buttonGroup[rendered ? 'animate' : 'attr']({
22742
			translateY: pos.buttonTop
22743
		});
22744
 
22745
		if (inputEnabled !== false) {
22746
 
22747
			// Update the alignment to the updated spacing box
22748
			inputGroup.align(extend({
22749
				y: pos.inputTop,
22750
				width: inputGroup.offset,
22751
				// Detect collision with the exporting buttons
22752
				x: navButtonOptions && (pos.inputTop < (navButtonOptions.y || 0) + navButtonOptions.height - chart.spacing[0]) ?
22753
					-40 : 0
22754
			}, options.inputPosition), true, chart.spacingBox);
22755
 
22756
			// Hide if overlapping - inputEnabled is null or undefined
22757
			if (!defined(inputEnabled)) {
22758
				buttonBBox = buttonGroup.getBBox();
22759
				inputGroup[inputGroup.translateX < buttonBBox.x + buttonBBox.width + 10 ? 'hide' : 'show']();
22760
			}
22761
 
22762
			// Set or reset the input values
22763
			rangeSelector.setInputValue('min', min);
22764
			rangeSelector.setInputValue('max', max);
22765
		}
22766
 
22767
		rangeSelector.rendered = true;
22768
	},
22769
 
22770
	/**
22771
	 * Destroys allocated elements.
22772
	 */
22773
	destroy: function () {
22774
		var minInput = this.minInput,
22775
			maxInput = this.maxInput,
22776
			chart = this.chart,
22777
			blurInputs = this.blurInputs,
22778
			key;
22779
 
22780
		removeEvent(chart.container, 'mousedown', blurInputs);
22781
		removeEvent(chart, 'resize', blurInputs);
22782
 
22783
		// Destroy elements in collections
22784
		destroyObjectProperties(this.buttons);
22785
 
22786
		// Clear input element events
22787
		if (minInput) {
22788
			minInput.onfocus = minInput.onblur = minInput.onchange = null;
22789
		}
22790
		if (maxInput) {
22791
			maxInput.onfocus = maxInput.onblur = maxInput.onchange = null;
22792
		}
22793
 
22794
		// Destroy HTML and SVG elements
22795
		for (key in this) {
22796
			if (this[key] && key !== 'chart') {
22797
				if (this[key].destroy) { // SVGElement
22798
					this[key].destroy();
22799
				} else if (this[key].nodeType) { // HTML element
22800
					discardElement(this[key]);
22801
				}
22802
			}
22803
			this[key] = null;
22804
		}
22805
	}
22806
};
22807
 
22808
/**
22809
 * Add logic to normalize the zoomed range in order to preserve the pressed state of range selector buttons
22810
 */
22811
Axis.prototype.toFixedRange = function (pxMin, pxMax, fixedMin, fixedMax) {
22812
	var fixedRange = this.chart && this.chart.fixedRange,
22813
		newMin = pick(fixedMin, this.translate(pxMin, true)),
22814
		newMax = pick(fixedMax, this.translate(pxMax, true)),
22815
		changeRatio = fixedRange && (newMax - newMin) / fixedRange;
22816
 
22817
	// If the difference between the fixed range and the actual requested range is
22818
	// too great, the user is dragging across an ordinal gap, and we need to release
22819
	// the range selector button.
22820
	if (changeRatio > 0.7 && changeRatio < 1.3) {
22821
		if (fixedMax) {
22822
			newMin = newMax - fixedRange;
22823
		} else {
22824
			newMax = newMin + fixedRange;
22825
		}
22826
	}
22827
 
22828
	return {
22829
		min: newMin,
22830
		max: newMax
22831
	};
22832
};
22833
 
22834
Axis.prototype.minFromRange = function () {
22835
	var rangeOptions = this.range,
22836
		type = rangeOptions.type,
22837
		timeName = { month: 'Month', year: 'FullYear'}[type],
22838
		min,
22839
		max = this.max,
22840
		date = new Date(max),
22841
		dataMin,
22842
		range;
22843
 
22844
	if (typeof rangeOptions === 'number') {
22845
		min = this.max - rangeOptions;
22846
		range = rangeOptions;
22847
	} else {
22848
		date['set' + timeName](date['get' + timeName]() - rangeOptions.count);
22849
		min = date.getTime();
22850
	}
22851
 
22852
	dataMin = pick(this.dataMin, Number.MIN_VALUE);
22853
	if (isNaN(min)) {
22854
		min = dataMin;
22855
	}
22856
	if (min <= dataMin) {
22857
		min = dataMin;
22858
		this.newMax = mathMin(min + range, this.dataMax);
22859
	}
22860
	if (isNaN(max)) {
22861
		min = undefined;
22862
	}
22863
	return min;
22864
 
22865
};
22866
 
22867
// Initialize scroller for stock charts
22868
wrap(Chart.prototype, 'init', function (proceed, options, callback) {
22869
 
22870
	addEvent(this, 'init', function () {
22871
		if (this.options.rangeSelector.enabled) {
22872
			this.rangeSelector = new RangeSelector(this);
22873
		}
22874
	});
22875
 
22876
	proceed.call(this, options, callback);
22877
 
22878
});
22879
 
22880
 
22881
Highcharts.RangeSelector = RangeSelector;
22882
 
22883
/* ****************************************************************************
22884
 * End Range Selector code													*
22885
 *****************************************************************************/
22886
 
22887
 
22888
 
22889
Chart.prototype.callbacks.push(function (chart) {
22890
	var extremes,
22891
		scroller = chart.scroller,
22892
		rangeSelector = chart.rangeSelector;
22893
 
22894
	function renderScroller() {
22895
		extremes = chart.xAxis[0].getExtremes();
22896
		scroller.render(extremes.min, extremes.max);
22897
	}
22898
 
22899
	function renderRangeSelector() {
22900
		extremes = chart.xAxis[0].getExtremes();
22901
		if (!isNaN(extremes.min)) {
22902
			rangeSelector.render(extremes.min, extremes.max);
22903
		}
22904
	}
22905
 
22906
	function afterSetExtremesHandlerScroller(e) {
22907
		if (e.triggerOp !== 'navigator-drag') {
22908
			scroller.render(e.min, e.max);
22909
		}
22910
	}
22911
 
22912
	function afterSetExtremesHandlerRangeSelector(e) {
22913
		rangeSelector.render(e.min, e.max);
22914
	}
22915
 
22916
	function destroyEvents() {
22917
		if (scroller) {
22918
			removeEvent(chart.xAxis[0], 'afterSetExtremes', afterSetExtremesHandlerScroller);
22919
		}
22920
		if (rangeSelector) {
22921
			removeEvent(chart, 'resize', renderRangeSelector);
22922
			removeEvent(chart.xAxis[0], 'afterSetExtremes', afterSetExtremesHandlerRangeSelector);
22923
		}
22924
	}
22925
 
22926
	// initiate the scroller
22927
	if (scroller) {
22928
		// redraw the scroller on setExtremes
22929
		addEvent(chart.xAxis[0], 'afterSetExtremes', afterSetExtremesHandlerScroller);
22930
 
22931
		// redraw the scroller on chart resize or box resize
22932
		wrap(chart, 'drawChartBox', function (proceed) {
22933
			var isDirtyBox = this.isDirtyBox;
22934
			proceed.call(this);
22935
			if (isDirtyBox) {
22936
				renderScroller();
22937
			}
22938
		});
22939
 
22940
		// do it now
22941
		renderScroller();
22942
	}
22943
	if (rangeSelector) {
22944
		// redraw the scroller on setExtremes
22945
		addEvent(chart.xAxis[0], 'afterSetExtremes', afterSetExtremesHandlerRangeSelector);
22946
 
22947
		// redraw the scroller chart resize
22948
		addEvent(chart, 'resize', renderRangeSelector);
22949
 
22950
		// do it now
22951
		renderRangeSelector();
22952
	}
22953
 
22954
	// Remove resize/afterSetExtremes at chart destroy
22955
	addEvent(chart, 'destroy', destroyEvents);
22956
});
22957
/**
22958
 * A wrapper for Chart with all the default values for a Stock chart
22959
 */
22960
Highcharts.StockChart = function (options, callback) {
22961
	var seriesOptions = options.series, // to increase performance, don't merge the data
22962
		opposite,
22963
 
22964
		// Always disable startOnTick:true on the main axis when the navigator is enabled (#1090)
22965
		navigatorEnabled = pick(options.navigator && options.navigator.enabled, true),
22966
		disableStartOnTick = navigatorEnabled ? {
22967
			startOnTick: false,
22968
			endOnTick: false
22969
		} : null,
22970
 
22971
		lineOptions = {
22972
 
22973
			marker: {
22974
				enabled: false,
22975
				radius: 2
22976
			}
22977
			// gapSize: 0
22978
		},
22979
		columnOptions = {
22980
			shadow: false,
22981
			borderWidth: 0
22982
		};
22983
 
22984
	// apply X axis options to both single and multi y axes
22985
	options.xAxis = map(splat(options.xAxis || {}), function (xAxisOptions) {
22986
		return merge({ // defaults
22987
				minPadding: 0,
22988
				maxPadding: 0,
22989
				ordinal: true,
22990
				title: {
22991
					text: null
22992
				},
22993
				labels: {
22994
					overflow: 'justify'
22995
				},
22996
				showLastLabel: true
22997
			}, xAxisOptions, // user options
22998
			{ // forced options
22999
				type: 'datetime',
23000
				categories: null
23001
			},
23002
			disableStartOnTick
23003
		);
23004
	});
23005
 
23006
	// apply Y axis options to both single and multi y axes
23007
	options.yAxis = map(splat(options.yAxis || {}), function (yAxisOptions) {
23008
		opposite = pick(yAxisOptions.opposite, true);
23009
		return merge({ // defaults
23010
			labels: {
23011
				y: -2
23012
			},
23013
			opposite: opposite,
23014
			showLastLabel: false,
23015
			title: {
23016
				text: null
23017
			}
23018
		}, yAxisOptions // user options
23019
		);
23020
	});
23021
 
23022
	options.series = null;
23023
 
23024
	options = merge({
23025
		chart: {
23026
			panning: true,
23027
			pinchType: 'x'
23028
		},
23029
		navigator: {
23030
			enabled: true
23031
		},
23032
		scrollbar: {
23033
			enabled: true
23034
		},
23035
		rangeSelector: {
23036
			enabled: true
23037
		},
23038
		title: {
23039
			text: null,
23040
			style: {
23041
				fontSize: '16px'
23042
			}
23043
		},
23044
		tooltip: {
23045
			shared: true,
23046
			crosshairs: true
23047
		},
23048
		legend: {
23049
			enabled: false
23050
		},
23051
 
23052
		plotOptions: {
23053
			line: lineOptions,
23054
			spline: lineOptions,
23055
			area: lineOptions,
23056
			areaspline: lineOptions,
23057
			arearange: lineOptions,
23058
			areasplinerange: lineOptions,
23059
			column: columnOptions,
23060
			columnrange: columnOptions,
23061
			candlestick: columnOptions,
23062
			ohlc: columnOptions
23063
		}
23064
 
23065
	},
23066
	options, // user's options
23067
 
23068
	{ // forced options
23069
		_stock: true, // internal flag
23070
		chart: {
23071
			inverted: false
23072
		}
23073
	});
23074
 
23075
	options.series = seriesOptions;
23076
 
23077
 
23078
	return new Chart(options, callback);
23079
};
23080
 
23081
// Implement the pinchType option
23082
wrap(Pointer.prototype, 'init', function (proceed, chart, options) {
23083
 
23084
	var pinchType = options.chart.pinchType || '';
23085
 
23086
	proceed.call(this, chart, options);
23087
 
23088
	// Pinch status
23089
	this.pinchX = this.pinchHor = pinchType.indexOf('x') !== -1;
23090
	this.pinchY = this.pinchVert = pinchType.indexOf('y') !== -1;
23091
	this.hasZoom = this.hasZoom || this.pinchHor || this.pinchVert;
23092
});
23093
 
23094
// Override the automatic label alignment so that the first Y axis' labels
23095
// are drawn on top of the grid line, and subsequent axes are drawn outside
23096
wrap(Axis.prototype, 'autoLabelAlign', function (proceed) {
23097
	var chart = this.chart,
23098
		options = this.options,
23099
		panes = chart._labelPanes = chart._labelPanes || {},
23100
		key,
23101
		labelOptions = this.options.labels;
23102
	if (this.chart.options._stock && this.coll === 'yAxis') {
23103
		key = options.top + ',' + options.height;
23104
		if (!panes[key] && labelOptions.enabled) { // do it only for the first Y axis of each pane
23105
			if (labelOptions.x === 15) { // default
23106
				labelOptions.x = 0;
23107
			}
23108
			if (labelOptions.align === undefined) {
23109
				labelOptions.align = 'right';
23110
			}
23111
			panes[key] = 1;
23112
			return 'right';
23113
		}
23114
	}
23115
	return proceed.call(this, [].slice.call(arguments, 1));
23116
});
23117
 
23118
// Override getPlotLinePath to allow for multipane charts
23119
wrap(Axis.prototype, 'getPlotLinePath', function (proceed, value, lineWidth, old, force, translatedValue) {
23120
	var axis = this,
23121
		series = (this.isLinked && !this.series ? this.linkedParent.series : this.series),
23122
		chart = axis.chart,
23123
		renderer = chart.renderer,
23124
		axisLeft = axis.left,
23125
		axisTop = axis.top,
23126
		x1,
23127
		y1,
23128
		x2,
23129
		y2,
23130
		result = [],
23131
		axes = [], //#3416 need a default array
23132
		axes2,
23133
		uniqueAxes;
23134
 
23135
	// Ignore in case of color Axis. #3360, #3524
23136
	if (axis.coll === 'colorAxis') {
23137
		return proceed.apply(this, [].slice.call(arguments, 1));
23138
	}
23139
 
23140
	// Get the related axes based on series
23141
	axes = (axis.isXAxis ?
23142
		(defined(axis.options.yAxis) ?
23143
			[chart.yAxis[axis.options.yAxis]] :
23144
			map(series, function (S) { return S.yAxis; })
23145
		) :
23146
		(defined(axis.options.xAxis) ?
23147
			[chart.xAxis[axis.options.xAxis]] :
23148
			map(series, function (S) { return S.xAxis; })
23149
		)
23150
	);
23151
 
23152
	// Get the related axes based options.*Axis setting #2810
23153
	axes2 = (axis.isXAxis ? chart.yAxis : chart.xAxis);
23154
	each(axes2, function (A) {
23155
		if (defined(A.options.id) ? A.options.id.indexOf('navigator') === -1 : true) {
23156
			var a = (A.isXAxis ? 'yAxis' : 'xAxis'),
23157
				rax = (defined(A.options[a]) ? chart[a][A.options[a]] : chart[a][0]);
23158
 
23159
			if (axis === rax) {
23160
				axes.push(A);
23161
			}
23162
		}
23163
	});
23164
 
23165
 
23166
	// Remove duplicates in the axes array. If there are no axes in the axes array,
23167
	// we are adding an axis without data, so we need to populate this with grid
23168
	// lines (#2796).
23169
	uniqueAxes = axes.length ? [] : [axis.isXAxis ? chart.yAxis[0] : chart.xAxis[0]]; //#3742
23170
	each(axes, function (axis2) {
23171
		if (inArray(axis2, uniqueAxes) === -1) {
23172
			uniqueAxes.push(axis2);
23173
		}
23174
	});
23175
 
23176
	translatedValue = pick(translatedValue, axis.translate(value, null, null, old));
23177
 
23178
	if (!isNaN(translatedValue)) {
23179
		if (axis.horiz) {
23180
			each(uniqueAxes, function (axis2) {
23181
				var skip;
23182
 
23183
				y1 = axis2.pos;
23184
				y2 = y1 + axis2.len;
23185
				x1 = x2 = mathRound(translatedValue + axis.transB);
23186
 
23187
				if (x1 < axisLeft || x1 > axisLeft + axis.width) { // outside plot area
23188
					if (force) {
23189
						x1 = x2 = mathMin(mathMax(axisLeft, x1), axisLeft + axis.width);
23190
					} else {
23191
						skip = true;
23192
					}
23193
				}
23194
				if (!skip) {
23195
					result.push('M', x1, y1, 'L', x2, y2);
23196
				}
23197
			});
23198
		} else {
23199
			each(uniqueAxes, function (axis2) {
23200
				var skip;
23201
 
23202
				x1 = axis2.pos;
23203
				x2 = x1 + axis2.len;
23204
				y1 = y2 = mathRound(axisTop + axis.height - translatedValue);
23205
 
23206
				if (y1 < axisTop || y1 > axisTop + axis.height) { // outside plot area
23207
					if (force) {
23208
						y1 = y2 = mathMin(mathMax(axisTop, y1), axis.top + axis.height);
23209
					} else {
23210
						skip = true;
23211
					}
23212
				}
23213
				if (!skip) {
23214
					result.push('M', x1, y1, 'L', x2, y2);
23215
				}
23216
			});
23217
		}
23218
	}
23219
	if (result.length > 0) {
23220
		return renderer.crispPolyLine(result, lineWidth || 1);
23221
	} else {
23222
		return null; //#3557 getPlotLinePath in regular Highcharts also returns null
23223
	}
23224
});
23225
 
23226
// Override getPlotBandPath to allow for multipane charts
23227
Axis.prototype.getPlotBandPath = function (from, to) {
23228
	var toPath = this.getPlotLinePath(to, null, null, true),
23229
		path = this.getPlotLinePath(from, null, null, true),
23230
		result = [],
23231
		i;
23232
 
23233
	if (path && toPath && path.toString() !== toPath.toString()) {
23234
		// Go over each subpath
23235
		for (i = 0; i < path.length; i += 6) {
23236
			result.push('M', path[i + 1], path[i + 2], 'L', path[i + 4], path[i + 5], toPath[i + 4], toPath[i + 5], toPath[i + 1], toPath[i + 2]);
23237
		}
23238
	} else { // outside the axis area
23239
		result = null;
23240
	}
23241
 
23242
	return result;
23243
};
23244
 
23245
// Function to crisp a line with multiple segments
23246
SVGRenderer.prototype.crispPolyLine = function (points, width) {
23247
	// points format: [M, 0, 0, L, 100, 0]
23248
	// normalize to a crisp line
23249
	var i;
23250
	for (i = 0; i < points.length; i = i + 6) {
23251
		if (points[i + 1] === points[i + 4]) {
23252
			// Substract due to #1129. Now bottom and left axis gridlines behave the same.
23253
			points[i + 1] = points[i + 4] = mathRound(points[i + 1]) - (width % 2 / 2);
23254
		}
23255
		if (points[i + 2] === points[i + 5]) {
23256
			points[i + 2] = points[i + 5] = mathRound(points[i + 2]) + (width % 2 / 2);
23257
		}
23258
	}
23259
	return points;
23260
};
23261
if (Renderer === Highcharts.VMLRenderer) {
23262
	VMLRenderer.prototype.crispPolyLine = SVGRenderer.prototype.crispPolyLine;
23263
}
23264
 
23265
 
23266
// Wrapper to hide the label
23267
wrap(Axis.prototype, 'hideCrosshair', function (proceed, i) {
23268
	proceed.call(this, i);
23269
 
23270
	if (!defined(this.crossLabelArray)) { return; }
23271
 
23272
	if (defined(i)) {
23273
		if (this.crossLabelArray[i]) { this.crossLabelArray[i].hide(); }
23274
	} else {
23275
		each(this.crossLabelArray, function (crosslabel) {
23276
			crosslabel.hide();
23277
		});
23278
	}
23279
});
23280
 
23281
// Wrapper to draw the label
23282
wrap(Axis.prototype, 'drawCrosshair', function (proceed, e, point) {
23283
	// Draw the crosshair
23284
	proceed.call(this, e, point);
23285
 
23286
	// Check if the label has to be drawn
23287
	if (!defined(this.crosshair.label) || !this.crosshair.label.enabled || !defined(point)) {
23288
		return;
23289
	}
23290
 
23291
	var chart = this.chart,
23292
		options = this.options.crosshair.label,		// the label's options
23293
		axis = this.isXAxis ? 'x' : 'y',			// axis name
23294
		horiz = this.horiz,							// axis orientation
23295
		opposite = this.opposite,					// axis position
23296
		left = this.left,							// left position
23297
		top = this.top,								// top position
23298
		crossLabel = this.crossLabel,				// reference to the svgElement
23299
		posx,
23300
		posy,
23301
		crossBox,
23302
		formatOption = options.format,
23303
		formatFormat = '',
23304
		limit;
23305
 
23306
	// If the label does not exist yet, create it.
23307
	if (!crossLabel) {
23308
		crossLabel = this.crossLabel = chart.renderer.label()
23309
		.attr({
23310
			align: options.align || (horiz ? 'center' : opposite ? (this.labelAlign === 'right' ? 'right' : 'left') : (this.labelAlign === 'left' ? 'left' : 'center')),
23311
			zIndex: 12,
23312
			height: horiz ? 16 : UNDEFINED,
23313
			fill: options.backgroundColor || (this.series[0] && this.series[0].color) || 'gray',
23314
			padding: pick(options.padding, 2),
23315
			stroke: options.borderColor || null,
23316
			'stroke-width': options.borderWidth || 0
23317
		})
23318
		.css(extend({
23319
			color: 'white',
23320
			fontWeight: 'normal',
23321
			fontSize: '11px',
23322
			textAlign: 'center'
23323
		}, options.style))
23324
		.add();
23325
	}
23326
 
23327
	if (horiz) {
23328
		posx = point.plotX + left;
23329
		posy = top + (opposite ? 0 : this.height);
23330
	} else {
23331
		posx = opposite ? this.width + left : 0;
23332
		posy = point.plotY + top;
23333
	}
23334
 
23335
	// if the crosshair goes out of view (too high or too low, hide it and hide the label)
23336
	if (posy < top || posy > top + this.height) {
23337
		this.hideCrosshair();
23338
		return;
23339
	}
23340
 
23341
	// TODO: Dynamic date formats like in Series.tooltipHeaderFormat.
23342
	if (!formatOption && !options.formatter) {
23343
		if (this.isDatetimeAxis) {
23344
			formatFormat = '%b %d, %Y';
23345
		}
23346
		formatOption = '{value' + (formatFormat ? ':' + formatFormat : '') + '}';
23347
	}
23348
 
23349
	// show the label
23350
	crossLabel.attr({
23351
		text: formatOption ? format(formatOption, {value: point[axis]}) : options.formatter.call(this, point[axis]),
23352
		x: posx,
23353
		y: posy,
23354
		visibility: VISIBLE
23355
	});
23356
	crossBox = crossLabel.getBBox();
23357
 
23358
	// now it is placed we can correct its position
23359
	if (horiz) {
23360
		if (((this.options.tickPosition === 'inside') && !opposite) ||
23361
			((this.options.tickPosition !== 'inside') && opposite)) {
23362
			posy = crossLabel.y - crossBox.height;
23363
		}
23364
	} else {
23365
		posy = crossLabel.y - (crossBox.height / 2);
23366
	}
23367
 
23368
	// check the edges
23369
	if (horiz) {
23370
		limit = {
23371
			left: left - crossBox.x,
23372
			right: left + this.width - crossBox.x
23373
		};
23374
	} else {
23375
		limit = {
23376
			left: this.labelAlign === 'left' ? left : 0,
23377
			right: this.labelAlign === 'right' ? left + this.width : chart.chartWidth
23378
		};
23379
	}
23380
 
23381
	// left edge
23382
	if (crossLabel.translateX < limit.left) {
23383
		posx += limit.left - crossLabel.translateX;
23384
	}
23385
	// right edge
23386
	if (crossLabel.translateX + crossBox.width >= limit.right) {
23387
		posx -= crossLabel.translateX + crossBox.width - limit.right;
23388
	}
23389
 
23390
	// show the crosslabel
23391
	crossLabel.attr({x: posx, y: posy, visibility: VISIBLE});
23392
});
23393
 
23394
/* ****************************************************************************
23395
 * Start value compare logic                                                  *
23396
 *****************************************************************************/
23397
 
23398
var seriesInit = seriesProto.init,
23399
	seriesProcessData = seriesProto.processData,
23400
	pointTooltipFormatter = Point.prototype.tooltipFormatter;
23401
 
23402
/**
23403
 * Extend series.init by adding a method to modify the y value used for plotting
23404
 * on the y axis. This method is called both from the axis when finding dataMin
23405
 * and dataMax, and from the series.translate method.
23406
 */
23407
seriesProto.init = function () {
23408
 
23409
	// Call base method
23410
	seriesInit.apply(this, arguments);
23411
 
23412
	// Set comparison mode
23413
	this.setCompare(this.options.compare);
23414
};
23415
 
23416
/**
23417
 * The setCompare method can be called also from the outside after render time
23418
 */
23419
seriesProto.setCompare = function (compare) {
23420
 
23421
	// Set or unset the modifyValue method
23422
	this.modifyValue = (compare === 'value' || compare === 'percent') ? function (value, point) {
23423
		var compareValue = this.compareValue;
23424
 
23425
		if (value !== UNDEFINED) { // #2601
23426
 
23427
			// get the modified value
23428
			value = compare === 'value' ?
23429
				value - compareValue : // compare value
23430
				value = 100 * (value / compareValue) - 100; // compare percent
23431
 
23432
			// record for tooltip etc.
23433
			if (point) {
23434
				point.change = value;
23435
			}
23436
 
23437
		}
23438
 
23439
		return value;
23440
	} : null;
23441
 
23442
	// Mark dirty
23443
	if (this.chart.hasRendered) {
23444
		this.isDirty = true;
23445
	}
23446
 
23447
};
23448
 
23449
/**
23450
 * Extend series.processData by finding the first y value in the plot area,
23451
 * used for comparing the following values
23452
 */
23453
seriesProto.processData = function () {
23454
	var series = this,
23455
		i = 0,
23456
		processedXData,
23457
		processedYData,
23458
		length;
23459
 
23460
	// call base method
23461
	seriesProcessData.apply(this, arguments);
23462
 
23463
	if (series.xAxis && series.processedYData) { // not pies
23464
 
23465
		// local variables
23466
		processedXData = series.processedXData;
23467
		processedYData = series.processedYData;
23468
		length = processedYData.length;
23469
 
23470
		// find the first value for comparison
23471
		for (; i < length; i++) {
23472
			if (typeof processedYData[i] === NUMBER && processedXData[i] >= series.xAxis.min) {
23473
				series.compareValue = processedYData[i];
23474
				break;
23475
			}
23476
		}
23477
	}
23478
};
23479
 
23480
/**
23481
 * Modify series extremes
23482
 */
23483
wrap(seriesProto, 'getExtremes', function (proceed) {
23484
	proceed.apply(this, [].slice.call(arguments, 1));
23485
 
23486
	if (this.modifyValue) {
23487
		this.dataMax = this.modifyValue(this.dataMax);
23488
		this.dataMin = this.modifyValue(this.dataMin);
23489
	}
23490
});
23491
 
23492
/**
23493
 * Add a utility method, setCompare, to the Y axis
23494
 */
23495
Axis.prototype.setCompare = function (compare, redraw) {
23496
	if (!this.isXAxis) {
23497
		each(this.series, function (series) {
23498
			series.setCompare(compare);
23499
		});
23500
		if (pick(redraw, true)) {
23501
			this.chart.redraw();
23502
		}
23503
	}
23504
};
23505
 
23506
/**
23507
 * Extend the tooltip formatter by adding support for the point.change variable
23508
 * as well as the changeDecimals option
23509
 */
23510
Point.prototype.tooltipFormatter = function (pointFormat) {
23511
	var point = this;
23512
 
23513
	pointFormat = pointFormat.replace(
23514
		'{point.change}',
23515
		(point.change > 0 ? '+' : '') + Highcharts.numberFormat(point.change, pick(point.series.tooltipOptions.changeDecimals, 2))
23516
	);
23517
 
23518
	return pointTooltipFormatter.apply(this, [pointFormat]);
23519
};
23520
 
23521
/* ****************************************************************************
23522
 * End value compare logic                                                    *
23523
 *****************************************************************************/
23524
 
23525
 
23526
/**
23527
 * Extend the Series prototype to create a separate series clip box. This is related
23528
 * to using multiple panes, and a future pane logic should incorporate this feature (#2754).
23529
 */
23530
wrap(Series.prototype, 'render', function (proceed) {
23531
	// Only do this on stock charts (#2939), and only if the series type handles clipping
23532
	// in the animate method (#2975).
23533
	if (this.chart.options._stock && this.xAxis) {
23534
 
23535
		// First render, initial clip box
23536
		if (!this.clipBox && this.animate) {
23537
			this.clipBox = merge(this.chart.clipBox);
23538
			this.clipBox.width = this.xAxis.len;
23539
			this.clipBox.height = this.yAxis.len;
23540
 
23541
		// On redrawing, resizing etc, update the clip rectangle
23542
		} else if (this.chart[this.sharedClipKey]) {
23543
			stop(this.chart[this.sharedClipKey]); // #2998
23544
			this.chart[this.sharedClipKey].attr({
23545
				width: this.xAxis.len,
23546
				height: this.yAxis.len
23547
			});
23548
		}
23549
	}
23550
	proceed.call(this);
23551
});
23552
 
23553
// global variables
23554
extend(Highcharts, {
23555
 
23556
	// Constructors
23557
	Color: Color,
23558
	Point: Point,
23559
	Tick: Tick,
23560
	Renderer: Renderer,
23561
	SVGElement: SVGElement,
23562
	SVGRenderer: SVGRenderer,
23563
 
23564
	// Various
23565
	arrayMin: arrayMin,
23566
	arrayMax: arrayMax,
23567
	charts: charts,
23568
	dateFormat: dateFormat,
23569
	error: error,
23570
	format: format,
23571
	pathAnim: pathAnim,
23572
	getOptions: getOptions,
23573
	hasBidiBug: hasBidiBug,
23574
	isTouchDevice: isTouchDevice,
23575
	setOptions: setOptions,
23576
	addEvent: addEvent,
23577
	removeEvent: removeEvent,
23578
	createElement: createElement,
23579
	discardElement: discardElement,
23580
	css: css,
23581
	each: each,
23582
	map: map,
23583
	merge: merge,
23584
	splat: splat,
23585
	extendClass: extendClass,
23586
	pInt: pInt,
23587
	svg: hasSVG,
23588
	canvas: useCanVG,
23589
	vml: !hasSVG && !useCanVG,
23590
	product: PRODUCT,
23591
	version: VERSION
23592
});
23593
 
23594
}());