Subversion-Projekte lars-tiefland.ci

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
776 lars 1
// ==ClosureCompiler==
2
// @compilation_level SIMPLE_OPTIMIZATIONS
3
 
4
/**
5
 * @license Highstock JS v2.1.9 (2015-10-07)
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
	isMS = /(msie|trident|edge)/i.test(userAgent) && !isOpera,
37
	docMode8 = doc.documentMode === 8,
38
	isWebKit = !isMS && /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 && !isMS && !!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.9',
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 (isMS && !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 (!isMS && 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.9/modules/canvas-tools.js',
1273
		VMLRadialGradientURL: 'http://code.highcharts.com/stock/2.1.9/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
			softThreshold: true,
1417
			states: { // states for the entire series
1418
				hover: {
1419
					//enabled: false,
1420
					lineWidthPlus: 1,
1421
					marker: {
1422
						// lineWidth: base + 1,
1423
						// radius: base + 1
1424
					},
1425
					halo: {
1426
						size: 10,
1427
						opacity: 0.25
1428
					}
1429
				},
1430
				select: {
1431
					marker: {}
1432
				}
1433
			},
1434
			stickyTracking: true,
1435
			//tooltip: {
1436
				//pointFormat: '<span style="color:{point.color}">\u25CF</span> {series.name}: <b>{point.y}</b>'
1437
				//valueDecimals: null,
1438
				//xDateFormat: '%A, %b %e, %Y',
1439
				//valuePrefix: '',
1440
				//ySuffix: ''
1441
			//}
1442
			turboThreshold: 1000
1443
			// zIndex: null
1444
		}
1445
	},
1446
	labels: {
1447
		//items: [],
1448
		style: {
1449
			//font: defaultFont,
1450
			position: ABSOLUTE,
1451
			color: '#3E576F'
1452
		}
1453
	},
1454
	legend: {
1455
		enabled: true,
1456
		align: 'center',
1457
		//floating: false,
1458
		layout: 'horizontal',
1459
		labelFormatter: function () {
1460
			return this.name;
1461
		},
1462
		//borderWidth: 0,
1463
		borderColor: '#909090',
1464
		borderRadius: 0,
1465
		navigation: {
1466
			// animation: true,
1467
			activeColor: '#274b6d',
1468
			// arrowSize: 12
1469
			inactiveColor: '#CCC'
1470
			// style: {} // text styles
1471
		},
1472
		// margin: 20,
1473
		// reversed: false,
1474
		shadow: false,
1475
		// backgroundColor: null,
1476
		/*style: {
1477
			padding: '5px'
1478
		},*/
1479
		itemStyle: {
1480
			color: '#333333',
1481
			fontSize: '12px',
1482
			fontWeight: 'bold'
1483
		},
1484
		itemHoverStyle: {
1485
			//cursor: 'pointer', removed as of #601
1486
			color: '#000'
1487
		},
1488
		itemHiddenStyle: {
1489
			color: '#CCC'
1490
		},
1491
		itemCheckboxStyle: {
1492
			position: ABSOLUTE,
1493
			width: '13px', // for IE precision
1494
			height: '13px'
1495
		},
1496
		// itemWidth: undefined,
1497
		// symbolRadius: 0,
1498
		// symbolWidth: 16,
1499
		symbolPadding: 5,
1500
		verticalAlign: 'bottom',
1501
		// width: undefined,
1502
		x: 0,
1503
		y: 0,
1504
		title: {
1505
			//text: null,
1506
			style: {
1507
				fontWeight: 'bold'
1508
			}
1509
		}
1510
	},
1511
 
1512
	loading: {
1513
		// hideDuration: 100,
1514
		labelStyle: {
1515
			fontWeight: 'bold',
1516
			position: RELATIVE,
1517
			top: '45%'
1518
		},
1519
		// showDuration: 0,
1520
		style: {
1521
			position: ABSOLUTE,
1522
			backgroundColor: 'white',
1523
			opacity: 0.5,
1524
			textAlign: 'center'
1525
		}
1526
	},
1527
 
1528
	tooltip: {
1529
		enabled: true,
1530
		animation: hasSVG,
1531
		//crosshairs: null,
1532
		backgroundColor: 'rgba(249, 249, 249, .85)',
1533
		borderWidth: 1,
1534
		borderRadius: 3,
1535
		dateTimeLabelFormats: {
1536
			millisecond: '%A, %b %e, %H:%M:%S.%L',
1537
			second: '%A, %b %e, %H:%M:%S',
1538
			minute: '%A, %b %e, %H:%M',
1539
			hour: '%A, %b %e, %H:%M',
1540
			day: '%A, %b %e, %Y',
1541
			week: 'Week from %A, %b %e, %Y',
1542
			month: '%B %Y',
1543
			year: '%Y'
1544
		},
1545
		footerFormat: '',
1546
		//formatter: defaultFormatter,
1547
		headerFormat: '<span style="font-size: 10px">{point.key}</span><br/>',
1548
		pointFormat: '<span style="color:{point.color}">\u25CF</span> {series.name}: <b>{point.y}</b><br/>',
1549
		shadow: true,
1550
		//shape: 'callout',
1551
		//shared: false,
1552
		snap: isTouchDevice ? 25 : 10,
1553
		style: {
1554
			color: '#333333',
1555
			cursor: 'default',
1556
			fontSize: '12px',
1557
			padding: '8px',
1558
			pointerEvents: 'none', // #1686 http://caniuse.com/#feat=pointer-events
1559
			whiteSpace: 'nowrap'
1560
		}
1561
		//xDateFormat: '%A, %b %e, %Y',
1562
		//valueDecimals: null,
1563
		//valuePrefix: '',
1564
		//valueSuffix: ''
1565
	},
1566
 
1567
	credits: {
1568
		enabled: true,
1569
		text: 'Highcharts.com',
1570
		href: 'http://www.highcharts.com',
1571
		position: {
1572
			align: 'right',
1573
			x: -10,
1574
			verticalAlign: 'bottom',
1575
			y: -5
1576
		},
1577
		style: {
1578
			cursor: 'pointer',
1579
			color: '#909090',
1580
			fontSize: '9px'
1581
		}
1582
	}
1583
};
1584
 
1585
 
1586
 
1587
 
1588
// Series defaults
1589
var defaultPlotOptions = defaultOptions.plotOptions,
1590
	defaultSeriesOptions = defaultPlotOptions.line;
1591
 
1592
// set the default time methods
1593
setTimeMethods();
1594
 
1595
 
1596
 
1597
/**
1598
 * Set the time methods globally based on the useUTC option. Time method can be either
1599
 * local time or UTC (default).
1600
 */
1601
function setTimeMethods() {
1602
	var globalOptions = defaultOptions.global,
1603
		useUTC = globalOptions.useUTC,
1604
		GET = useUTC ? 'getUTC' : 'get',
1605
		SET = useUTC ? 'setUTC' : 'set';
1606
 
1607
 
1608
	Date = globalOptions.Date || window.Date;
1609
	timezoneOffset = useUTC && globalOptions.timezoneOffset;
1610
	getTimezoneOffset = useUTC && globalOptions.getTimezoneOffset;
1611
	makeTime = function (year, month, date, hours, minutes, seconds) {
1612
		var d;
1613
		if (useUTC) {
1614
			d = Date.UTC.apply(0, arguments);
1615
			d += getTZOffset(d);
1616
		} else {
1617
			d = new Date(
1618
				year,
1619
				month,
1620
				pick(date, 1),
1621
				pick(hours, 0),
1622
				pick(minutes, 0),
1623
				pick(seconds, 0)
1624
			).getTime();
1625
		}
1626
		return d;
1627
	};
1628
	getMinutes =      GET + 'Minutes';
1629
	getHours =        GET + 'Hours';
1630
	getDay =          GET + 'Day';
1631
	getDate =         GET + 'Date';
1632
	getMonth =        GET + 'Month';
1633
	getFullYear =     GET + 'FullYear';
1634
	setMilliseconds = SET + 'Milliseconds';
1635
	setSeconds =      SET + 'Seconds';
1636
	setMinutes =      SET + 'Minutes';
1637
	setHours =        SET + 'Hours';
1638
	setDate =         SET + 'Date';
1639
	setMonth =        SET + 'Month';
1640
	setFullYear =     SET + 'FullYear';
1641
 
1642
}
1643
 
1644
/**
1645
 * Merge the default options with custom options and return the new options structure
1646
 * @param {Object} options The new custom options
1647
 */
1648
function setOptions(options) {
1649
 
1650
	// Copy in the default options
1651
	defaultOptions = merge(true, defaultOptions, options);
1652
 
1653
	// Apply UTC
1654
	setTimeMethods();
1655
 
1656
	return defaultOptions;
1657
}
1658
 
1659
/**
1660
 * Get the updated default options. Until 3.0.7, merely exposing defaultOptions for outside modules
1661
 * wasn't enough because the setOptions method created a new object.
1662
 */
1663
function getOptions() {
1664
	return defaultOptions;
1665
}
1666
 
1667
 
1668
/**
1669
 * Handle color operations. The object methods are chainable.
1670
 * @param {String} input The input color in either rbga or hex format
1671
 */
1672
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*\)/,
1673
	hexRegEx = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/,
1674
	rgbRegEx = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/;
1675
 
1676
var Color = function (input) {
1677
	// declare variables
1678
	var rgba = [], result, stops;
1679
 
1680
	/**
1681
	 * Parse the input color to rgba array
1682
	 * @param {String} input
1683
	 */
1684
	function init(input) {
1685
 
1686
		// Gradients
1687
		if (input && input.stops) {
1688
			stops = map(input.stops, function (stop) {
1689
				return Color(stop[1]);
1690
			});
1691
 
1692
		// Solid colors
1693
		} else {
1694
			// rgba
1695
			result = rgbaRegEx.exec(input);
1696
			if (result) {
1697
				rgba = [pInt(result[1]), pInt(result[2]), pInt(result[3]), parseFloat(result[4], 10)];
1698
			} else {
1699
				// hex
1700
				result = hexRegEx.exec(input);
1701
				if (result) {
1702
					rgba = [pInt(result[1], 16), pInt(result[2], 16), pInt(result[3], 16), 1];
1703
				} else {
1704
					// rgb
1705
					result = rgbRegEx.exec(input);
1706
					if (result) {
1707
						rgba = [pInt(result[1]), pInt(result[2]), pInt(result[3]), 1];
1708
					}
1709
				}
1710
			}
1711
		}
1712
 
1713
	}
1714
	/**
1715
	 * Return the color a specified format
1716
	 * @param {String} format
1717
	 */
1718
	function get(format) {
1719
		var ret;
1720
 
1721
		if (stops) {
1722
			ret = merge(input);
1723
			ret.stops = [].concat(ret.stops);
1724
			each(stops, function (stop, i) {
1725
				ret.stops[i] = [ret.stops[i][0], stop.get(format)];
1726
			});
1727
 
1728
		// it's NaN if gradient colors on a column chart
1729
		} else if (rgba && !isNaN(rgba[0])) {
1730
			if (format === 'rgb') {
1731
				ret = 'rgb(' + rgba[0] + ',' + rgba[1] + ',' + rgba[2] + ')';
1732
			} else if (format === 'a') {
1733
				ret = rgba[3];
1734
			} else {
1735
				ret = 'rgba(' + rgba.join(',') + ')';
1736
			}
1737
		} else {
1738
			ret = input;
1739
		}
1740
		return ret;
1741
	}
1742
 
1743
	/**
1744
	 * Brighten the color
1745
	 * @param {Number} alpha
1746
	 */
1747
	function brighten(alpha) {
1748
		if (stops) {
1749
			each(stops, function (stop) {
1750
				stop.brighten(alpha);
1751
			});
1752
 
1753
		} else if (isNumber(alpha) && alpha !== 0) {
1754
			var i;
1755
			for (i = 0; i < 3; i++) {
1756
				rgba[i] += pInt(alpha * 255);
1757
 
1758
				if (rgba[i] < 0) {
1759
					rgba[i] = 0;
1760
				}
1761
				if (rgba[i] > 255) {
1762
					rgba[i] = 255;
1763
				}
1764
			}
1765
		}
1766
		return this;
1767
	}
1768
	/**
1769
	 * Set the color's opacity to a given alpha value
1770
	 * @param {Number} alpha
1771
	 */
1772
	function setOpacity(alpha) {
1773
		rgba[3] = alpha;
1774
		return this;
1775
	}
1776
 
1777
	// initialize: parse the input
1778
	init(input);
1779
 
1780
	// public methods
1781
	return {
1782
		get: get,
1783
		brighten: brighten,
1784
		rgba: rgba,
1785
		setOpacity: setOpacity,
1786
		raw: input
1787
	};
1788
};
1789
 
1790
 
1791
/**
1792
 * A wrapper object for SVG elements
1793
 */
1794
function SVGElement() {}
1795
 
1796
SVGElement.prototype = {
1797
 
1798
	// Default base for animation
1799
	opacity: 1,
1800
	// For labels, these CSS properties are applied to the <text> node directly
1801
	textProps: ['fontSize', 'fontWeight', 'fontFamily', 'fontStyle', 'color',
1802
		'lineHeight', 'width', 'textDecoration', 'textOverflow', 'textShadow'],
1803
 
1804
	/**
1805
	 * Initialize the SVG renderer
1806
	 * @param {Object} renderer
1807
	 * @param {String} nodeName
1808
	 */
1809
	init: function (renderer, nodeName) {
1810
		var wrapper = this;
1811
		wrapper.element = nodeName === 'span' ?
1812
			createElement(nodeName) :
1813
			doc.createElementNS(SVG_NS, nodeName);
1814
		wrapper.renderer = renderer;
1815
	},
1816
 
1817
	/**
1818
	 * Animate a given attribute
1819
	 * @param {Object} params
1820
	 * @param {Number} options The same options as in jQuery animation
1821
	 * @param {Function} complete Function to perform at the end of animation
1822
	 */
1823
	animate: function (params, options, complete) {
1824
		var animOptions = pick(options, this.renderer.globalAnimation, true);
1825
		stop(this); // stop regardless of animation actually running, or reverting to .attr (#607)
1826
		if (animOptions) {
1827
			animOptions = merge(animOptions, {}); //#2625
1828
			if (complete) { // allows using a callback with the global animation without overwriting it
1829
				animOptions.complete = complete;
1830
			}
1831
			animate(this, params, animOptions);
1832
		} else {
1833
			this.attr(params, null, complete);
1834
		}
1835
		return this;
1836
	},
1837
 
1838
	/**
1839
	 * Build an SVG gradient out of a common JavaScript configuration object
1840
	 */
1841
	colorGradient: function (color, prop, elem) {
1842
		var renderer = this.renderer,
1843
			colorObject,
1844
			gradName,
1845
			gradAttr,
1846
			radAttr,
1847
			gradients,
1848
			gradientObject,
1849
			stops,
1850
			stopColor,
1851
			stopOpacity,
1852
			radialReference,
1853
			n,
1854
			id,
1855
			key = [];
1856
 
1857
		// Apply linear or radial gradients
1858
		if (color.linearGradient) {
1859
			gradName = 'linearGradient';
1860
		} else if (color.radialGradient) {
1861
			gradName = 'radialGradient';
1862
		}
1863
 
1864
		if (gradName) {
1865
			gradAttr = color[gradName];
1866
			gradients = renderer.gradients;
1867
			stops = color.stops;
1868
			radialReference = elem.radialReference;
1869
 
1870
			// Keep < 2.2 kompatibility
1871
			if (isArray(gradAttr)) {
1872
				color[gradName] = gradAttr = {
1873
					x1: gradAttr[0],
1874
					y1: gradAttr[1],
1875
					x2: gradAttr[2],
1876
					y2: gradAttr[3],
1877
					gradientUnits: 'userSpaceOnUse'
1878
				};
1879
			}
1880
 
1881
			// Correct the radial gradient for the radial reference system
1882
			if (gradName === 'radialGradient' && radialReference && !defined(gradAttr.gradientUnits)) {
1883
				radAttr = gradAttr; // Save the radial attributes for updating
1884
				gradAttr = merge(gradAttr,
1885
					renderer.getRadialAttr(radialReference, radAttr),
1886
					{ gradientUnits: 'userSpaceOnUse' }
1887
				);
1888
			}
1889
 
1890
			// Build the unique key to detect whether we need to create a new element (#1282)
1891
			for (n in gradAttr) {
1892
				if (n !== 'id') {
1893
					key.push(n, gradAttr[n]);
1894
				}
1895
			}
1896
			for (n in stops) {
1897
				key.push(stops[n]);
1898
			}
1899
			key = key.join(',');
1900
 
1901
			// Check if a gradient object with the same config object is created within this renderer
1902
			if (gradients[key]) {
1903
				id = gradients[key].attr('id');
1904
 
1905
			} else {
1906
 
1907
				// Set the id and create the element
1908
				gradAttr.id = id = PREFIX + idCounter++;
1909
				gradients[key] = gradientObject = renderer.createElement(gradName)
1910
					.attr(gradAttr)
1911
					.add(renderer.defs);
1912
 
1913
				gradientObject.radAttr = radAttr;
1914
 
1915
				// The gradient needs to keep a list of stops to be able to destroy them
1916
				gradientObject.stops = [];
1917
				each(stops, function (stop) {
1918
					var stopObject;
1919
					if (stop[1].indexOf('rgba') === 0) {
1920
						colorObject = Color(stop[1]);
1921
						stopColor = colorObject.get('rgb');
1922
						stopOpacity = colorObject.get('a');
1923
					} else {
1924
						stopColor = stop[1];
1925
						stopOpacity = 1;
1926
					}
1927
					stopObject = renderer.createElement('stop').attr({
1928
						offset: stop[0],
1929
						'stop-color': stopColor,
1930
						'stop-opacity': stopOpacity
1931
					}).add(gradientObject);
1932
 
1933
					// Add the stop element to the gradient
1934
					gradientObject.stops.push(stopObject);
1935
				});
1936
			}
1937
 
1938
			// Set the reference to the gradient object
1939
			elem.setAttribute(prop, 'url(' + renderer.url + '#' + id + ')');
1940
			elem.gradient = key;
1941
		}
1942
	},
1943
 
1944
	/**
1945
	 * Apply a polyfill to the text-stroke CSS property, by copying the text element
1946
	 * and apply strokes to the copy.
1947
	 *
1948
	 * Contrast checks at http://jsfiddle.net/highcharts/43soe9m1/2/
1949
	 *
1950
	 * docs: update default, document the polyfill and the limitations on hex colors and pixel values, document contrast pseudo-color
1951
	 */
1952
	applyTextShadow: function (textShadow) {
1953
		var elem = this.element,
1954
			tspans,
1955
			hasContrast = textShadow.indexOf('contrast') !== -1,
1956
			styles = {},
1957
			forExport = this.renderer.forExport,
1958
			// IE10 and IE11 report textShadow in elem.style even though it doesn't work. Check
1959
			// this again with new IE release. In exports, the rendering is passed to PhantomJS.
1960
			supports = forExport || (elem.style.textShadow !== UNDEFINED && !isMS);
1961
 
1962
		// When the text shadow is set to contrast, use dark stroke for light text and vice versa
1963
		if (hasContrast) {
1964
			styles.textShadow = textShadow = textShadow.replace(/contrast/g, this.renderer.getContrast(elem.style.fill));
1965
		}
1966
 
1967
		// Safari with retina displays as well as PhantomJS bug (#3974). Firefox does not tolerate this,
1968
		// it removes the text shadows.
1969
		if (isWebKit || forExport) {
1970
			styles.textRendering = 'geometricPrecision';
1971
		}
1972
 
1973
		/* Selective side-by-side testing in supported browser (http://jsfiddle.net/highcharts/73L1ptrh/)
1974
		if (elem.textContent.indexOf('2.') === 0) {
1975
			elem.style['text-shadow'] = 'none';
1976
			supports = false;
1977
		}
1978
		// */
1979
 
1980
		// No reason to polyfill, we've got native support
1981
		if (supports) {
1982
			this.css(styles); // Apply altered textShadow or textRendering workaround
1983
		} else {
1984
 
1985
			this.fakeTS = true; // Fake text shadow
1986
 
1987
			// In order to get the right y position of the clones,
1988
			// copy over the y setter
1989
			this.ySetter = this.xSetter;
1990
 
1991
			tspans = [].slice.call(elem.getElementsByTagName('tspan'));
1992
			each(textShadow.split(/\s?,\s?/g), function (textShadow) {
1993
				var firstChild = elem.firstChild,
1994
					color,
1995
					strokeWidth;
1996
 
1997
				textShadow = textShadow.split(' ');
1998
				color = textShadow[textShadow.length - 1];
1999
 
2000
				// Approximately tune the settings to the text-shadow behaviour
2001
				strokeWidth = textShadow[textShadow.length - 2];
2002
 
2003
				if (strokeWidth) {
2004
					each(tspans, function (tspan, y) {
2005
						var clone;
2006
 
2007
						// Let the first line start at the correct X position
2008
						if (y === 0) {
2009
							tspan.setAttribute('x', elem.getAttribute('x'));
2010
							y = elem.getAttribute('y');
2011
							tspan.setAttribute('y', y || 0);
2012
							if (y === null) {
2013
								elem.setAttribute('y', 0);
2014
							}
2015
						}
2016
 
2017
						// Create the clone and apply shadow properties
2018
						clone = tspan.cloneNode(1);
2019
						attr(clone, {
2020
							'class': PREFIX + 'text-shadow',
2021
							'fill': color,
2022
							'stroke': color,
2023
							'stroke-opacity': 1 / mathMax(pInt(strokeWidth), 3),
2024
							'stroke-width': strokeWidth,
2025
							'stroke-linejoin': 'round'
2026
						});
2027
						elem.insertBefore(clone, firstChild);
2028
					});
2029
				}
2030
			});
2031
		}
2032
	},
2033
 
2034
	/**
2035
	 * Set or get a given attribute
2036
	 * @param {Object|String} hash
2037
	 * @param {Mixed|Undefined} val
2038
	 */
2039
	attr: function (hash, val, complete) {
2040
		var key,
2041
			value,
2042
			element = this.element,
2043
			hasSetSymbolSize,
2044
			ret = this,
2045
			skipAttr;
2046
 
2047
		// single key-value pair
2048
		if (typeof hash === 'string' && val !== UNDEFINED) {
2049
			key = hash;
2050
			hash = {};
2051
			hash[key] = val;
2052
		}
2053
 
2054
		// used as a getter: first argument is a string, second is undefined
2055
		if (typeof hash === 'string') {
2056
			ret = (this[hash + 'Getter'] || this._defaultGetter).call(this, hash, element);
2057
 
2058
		// setter
2059
		} else {
2060
 
2061
			for (key in hash) {
2062
				value = hash[key];
2063
				skipAttr = false;
2064
 
2065
 
2066
 
2067
				if (this.symbolName && /^(x|y|width|height|r|start|end|innerR|anchorX|anchorY)/.test(key)) {
2068
					if (!hasSetSymbolSize) {
2069
						this.symbolAttr(hash);
2070
						hasSetSymbolSize = true;
2071
					}
2072
					skipAttr = true;
2073
				}
2074
 
2075
				if (this.rotation && (key === 'x' || key === 'y')) {
2076
					this.doTransform = true;
2077
				}
2078
 
2079
				if (!skipAttr) {
2080
					(this[key + 'Setter'] || this._defaultSetter).call(this, value, key, element);
2081
				}
2082
 
2083
				// Let the shadow follow the main element
2084
				if (this.shadows && /^(width|height|visibility|x|y|d|transform|cx|cy|r)$/.test(key)) {
2085
					this.updateShadows(key, value);
2086
				}
2087
			}
2088
 
2089
			// Update transform. Do this outside the loop to prevent redundant updating for batch setting
2090
			// of attributes.
2091
			if (this.doTransform) {
2092
				this.updateTransform();
2093
				this.doTransform = false;
2094
			}
2095
 
2096
		}
2097
 
2098
		// In accordance with animate, run a complete callback
2099
		if (complete) {
2100
			complete();
2101
		}
2102
 
2103
		return ret;
2104
	},
2105
 
2106
	updateShadows: function (key, value) {
2107
		var shadows = this.shadows,
2108
			i = shadows.length;
2109
		while (i--) {
2110
			shadows[i].setAttribute(
2111
				key,
2112
				key === 'height' ?
2113
					mathMax(value - (shadows[i].cutHeight || 0), 0) :
2114
					key === 'd' ? this.d : value
2115
			);
2116
		}
2117
	},
2118
 
2119
	/**
2120
	 * Add a class name to an element
2121
	 */
2122
	addClass: function (className) {
2123
		var element = this.element,
2124
			currentClassName = attr(element, 'class') || '';
2125
 
2126
		if (currentClassName.indexOf(className) === -1) {
2127
			attr(element, 'class', currentClassName + ' ' + className);
2128
		}
2129
		return this;
2130
	},
2131
	/* hasClass and removeClass are not (yet) needed
2132
	hasClass: function (className) {
2133
		return attr(this.element, 'class').indexOf(className) !== -1;
2134
	},
2135
	removeClass: function (className) {
2136
		attr(this.element, 'class', attr(this.element, 'class').replace(className, ''));
2137
		return this;
2138
	},
2139
	*/
2140
 
2141
	/**
2142
	 * If one of the symbol size affecting parameters are changed,
2143
	 * check all the others only once for each call to an element's
2144
	 * .attr() method
2145
	 * @param {Object} hash
2146
	 */
2147
	symbolAttr: function (hash) {
2148
		var wrapper = this;
2149
 
2150
		each(['x', 'y', 'r', 'start', 'end', 'width', 'height', 'innerR', 'anchorX', 'anchorY'], function (key) {
2151
			wrapper[key] = pick(hash[key], wrapper[key]);
2152
		});
2153
 
2154
		wrapper.attr({
2155
			d: wrapper.renderer.symbols[wrapper.symbolName](
2156
				wrapper.x,
2157
				wrapper.y,
2158
				wrapper.width,
2159
				wrapper.height,
2160
				wrapper
2161
			)
2162
		});
2163
	},
2164
 
2165
	/**
2166
	 * Apply a clipping path to this object
2167
	 * @param {String} id
2168
	 */
2169
	clip: function (clipRect) {
2170
		return this.attr('clip-path', clipRect ? 'url(' + this.renderer.url + '#' + clipRect.id + ')' : NONE);
2171
	},
2172
 
2173
	/**
2174
	 * Calculate the coordinates needed for drawing a rectangle crisply and return the
2175
	 * calculated attributes
2176
	 * @param {Number} strokeWidth
2177
	 * @param {Number} x
2178
	 * @param {Number} y
2179
	 * @param {Number} width
2180
	 * @param {Number} height
2181
	 */
2182
	crisp: function (rect) {
2183
 
2184
		var wrapper = this,
2185
			key,
2186
			attribs = {},
2187
			normalizer,
2188
			strokeWidth = rect.strokeWidth || wrapper.strokeWidth || 0;
2189
 
2190
		normalizer = mathRound(strokeWidth) % 2 / 2; // mathRound because strokeWidth can sometimes have roundoff errors
2191
 
2192
		// normalize for crisp edges
2193
		rect.x = mathFloor(rect.x || wrapper.x || 0) + normalizer;
2194
		rect.y = mathFloor(rect.y || wrapper.y || 0) + normalizer;
2195
		rect.width = mathFloor((rect.width || wrapper.width || 0) - 2 * normalizer);
2196
		rect.height = mathFloor((rect.height || wrapper.height || 0) - 2 * normalizer);
2197
		rect.strokeWidth = strokeWidth;
2198
 
2199
		for (key in rect) {
2200
			if (wrapper[key] !== rect[key]) { // only set attribute if changed
2201
				wrapper[key] = attribs[key] = rect[key];
2202
			}
2203
		}
2204
 
2205
		return attribs;
2206
	},
2207
 
2208
	/**
2209
	 * Set styles for the element
2210
	 * @param {Object} styles
2211
	 */
2212
	css: function (styles) {
2213
		var elemWrapper = this,
2214
			oldStyles = elemWrapper.styles,
2215
			newStyles = {},
2216
			elem = elemWrapper.element,
2217
			textWidth,
2218
			n,
2219
			serializedCss = '',
2220
			hyphenate,
2221
			hasNew = !oldStyles;
2222
 
2223
		// convert legacy
2224
		if (styles && styles.color) {
2225
			styles.fill = styles.color;
2226
		}
2227
 
2228
		// Filter out existing styles to increase performance (#2640)
2229
		if (oldStyles) {
2230
			for (n in styles) {
2231
				if (styles[n] !== oldStyles[n]) {
2232
					newStyles[n] = styles[n];
2233
					hasNew = true;
2234
				}
2235
			}
2236
		}
2237
		if (hasNew) {
2238
			textWidth = elemWrapper.textWidth =
2239
				(styles && styles.width && elem.nodeName.toLowerCase() === 'text' && pInt(styles.width)) ||
2240
				elemWrapper.textWidth; // #3501
2241
 
2242
			// Merge the new styles with the old ones
2243
			if (oldStyles) {
2244
				styles = extend(
2245
					oldStyles,
2246
					newStyles
2247
				);
2248
			}
2249
 
2250
			// store object
2251
			elemWrapper.styles = styles;
2252
 
2253
			if (textWidth && (useCanVG || (!hasSVG && elemWrapper.renderer.forExport))) {
2254
				delete styles.width;
2255
			}
2256
 
2257
			// serialize and set style attribute
2258
			if (isMS && !hasSVG) {
2259
				css(elemWrapper.element, styles);
2260
			} else {
2261
				/*jslint unparam: true*/
2262
				hyphenate = function (a, b) { return '-' + b.toLowerCase(); };
2263
				/*jslint unparam: false*/
2264
				for (n in styles) {
2265
					serializedCss += n.replace(/([A-Z])/g, hyphenate) + ':' + styles[n] + ';';
2266
				}
2267
				attr(elem, 'style', serializedCss); // #1881
2268
			}
2269
 
2270
 
2271
			// re-build text
2272
			if (textWidth && elemWrapper.added) {
2273
				elemWrapper.renderer.buildText(elemWrapper);
2274
			}
2275
		}
2276
 
2277
		return elemWrapper;
2278
	},
2279
 
2280
	/**
2281
	 * Add an event listener
2282
	 * @param {String} eventType
2283
	 * @param {Function} handler
2284
	 */
2285
	on: function (eventType, handler) {
2286
		var svgElement = this,
2287
			element = svgElement.element;
2288
 
2289
		// touch
2290
		if (hasTouch && eventType === 'click') {
2291
			element.ontouchstart = function (e) {
2292
				svgElement.touchEventFired = Date.now();
2293
				e.preventDefault();
2294
				handler.call(element, e);
2295
			};
2296
			element.onclick = function (e) {
2297
				if (userAgent.indexOf('Android') === -1 || Date.now() - (svgElement.touchEventFired || 0) > 1100) { // #2269
2298
					handler.call(element, e);
2299
				}
2300
			};
2301
		} else {
2302
			// simplest possible event model for internal use
2303
			element['on' + eventType] = handler;
2304
		}
2305
		return this;
2306
	},
2307
 
2308
	/**
2309
	 * Set the coordinates needed to draw a consistent radial gradient across
2310
	 * pie slices regardless of positioning inside the chart. The format is
2311
	 * [centerX, centerY, diameter] in pixels.
2312
	 */
2313
	setRadialReference: function (coordinates) {
2314
		var existingGradient = this.renderer.gradients[this.element.gradient];
2315
 
2316
		this.element.radialReference = coordinates;
2317
 
2318
		// On redrawing objects with an existing gradient, the gradient needs
2319
		// to be repositioned (#3801)
2320
		if (existingGradient && existingGradient.radAttr) {
2321
			existingGradient.animate(
2322
				this.renderer.getRadialAttr(
2323
					coordinates,
2324
					existingGradient.radAttr
2325
				)
2326
			);
2327
		}
2328
 
2329
		return this;
2330
	},
2331
 
2332
	/**
2333
	 * Move an object and its children by x and y values
2334
	 * @param {Number} x
2335
	 * @param {Number} y
2336
	 */
2337
	translate: function (x, y) {
2338
		return this.attr({
2339
			translateX: x,
2340
			translateY: y
2341
		});
2342
	},
2343
 
2344
	/**
2345
	 * Invert a group, rotate and flip
2346
	 */
2347
	invert: function () {
2348
		var wrapper = this;
2349
		wrapper.inverted = true;
2350
		wrapper.updateTransform();
2351
		return wrapper;
2352
	},
2353
 
2354
	/**
2355
	 * Private method to update the transform attribute based on internal
2356
	 * properties
2357
	 */
2358
	updateTransform: function () {
2359
		var wrapper = this,
2360
			translateX = wrapper.translateX || 0,
2361
			translateY = wrapper.translateY || 0,
2362
			scaleX = wrapper.scaleX,
2363
			scaleY = wrapper.scaleY,
2364
			inverted = wrapper.inverted,
2365
			rotation = wrapper.rotation,
2366
			element = wrapper.element,
2367
			transform;
2368
 
2369
		// flipping affects translate as adjustment for flipping around the group's axis
2370
		if (inverted) {
2371
			translateX += wrapper.attr('width');
2372
			translateY += wrapper.attr('height');
2373
		}
2374
 
2375
		// Apply translate. Nearly all transformed elements have translation, so instead
2376
		// of checking for translate = 0, do it always (#1767, #1846).
2377
		transform = ['translate(' + translateX + ',' + translateY + ')'];
2378
 
2379
		// apply rotation
2380
		if (inverted) {
2381
			transform.push('rotate(90) scale(-1,1)');
2382
		} else if (rotation) { // text rotation
2383
			transform.push('rotate(' + rotation + ' ' + (element.getAttribute('x') || 0) + ' ' + (element.getAttribute('y') || 0) + ')');
2384
 
2385
			// Delete bBox memo when the rotation changes
2386
			//delete wrapper.bBox;
2387
		}
2388
 
2389
		// apply scale
2390
		if (defined(scaleX) || defined(scaleY)) {
2391
			transform.push('scale(' + pick(scaleX, 1) + ' ' + pick(scaleY, 1) + ')');
2392
		}
2393
 
2394
		if (transform.length) {
2395
			element.setAttribute('transform', transform.join(' '));
2396
		}
2397
	},
2398
	/**
2399
	 * Bring the element to the front
2400
	 */
2401
	toFront: function () {
2402
		var element = this.element;
2403
		element.parentNode.appendChild(element);
2404
		return this;
2405
	},
2406
 
2407
 
2408
	/**
2409
	 * Break down alignment options like align, verticalAlign, x and y
2410
	 * to x and y relative to the chart.
2411
	 *
2412
	 * @param {Object} alignOptions
2413
	 * @param {Boolean} alignByTranslate
2414
	 * @param {String[Object} box The box to align to, needs a width and height. When the
2415
	 *		box is a string, it refers to an object in the Renderer. For example, when
2416
	 *		box is 'spacingBox', it refers to Renderer.spacingBox which holds width, height
2417
	 *		x and y properties.
2418
	 *
2419
	 */
2420
	align: function (alignOptions, alignByTranslate, box) {
2421
		var align,
2422
			vAlign,
2423
			x,
2424
			y,
2425
			attribs = {},
2426
			alignTo,
2427
			renderer = this.renderer,
2428
			alignedObjects = renderer.alignedObjects;
2429
 
2430
		// First call on instanciate
2431
		if (alignOptions) {
2432
			this.alignOptions = alignOptions;
2433
			this.alignByTranslate = alignByTranslate;
2434
			if (!box || isString(box)) { // boxes other than renderer handle this internally
2435
				this.alignTo = alignTo = box || 'renderer';
2436
				erase(alignedObjects, this); // prevent duplicates, like legendGroup after resize
2437
				alignedObjects.push(this);
2438
				box = null; // reassign it below
2439
			}
2440
 
2441
		// When called on resize, no arguments are supplied
2442
		} else {
2443
			alignOptions = this.alignOptions;
2444
			alignByTranslate = this.alignByTranslate;
2445
			alignTo = this.alignTo;
2446
		}
2447
 
2448
		box = pick(box, renderer[alignTo], renderer);
2449
 
2450
		// Assign variables
2451
		align = alignOptions.align;
2452
		vAlign = alignOptions.verticalAlign;
2453
		x = (box.x || 0) + (alignOptions.x || 0); // default: left align
2454
		y = (box.y || 0) + (alignOptions.y || 0); // default: top align
2455
 
2456
		// Align
2457
		if (align === 'right' || align === 'center') {
2458
			x += (box.width - (alignOptions.width || 0)) /
2459
					{ right: 1, center: 2 }[align];
2460
		}
2461
		attribs[alignByTranslate ? 'translateX' : 'x'] = mathRound(x);
2462
 
2463
 
2464
		// Vertical align
2465
		if (vAlign === 'bottom' || vAlign === 'middle') {
2466
			y += (box.height - (alignOptions.height || 0)) /
2467
					({ bottom: 1, middle: 2 }[vAlign] || 1);
2468
 
2469
		}
2470
		attribs[alignByTranslate ? 'translateY' : 'y'] = mathRound(y);
2471
 
2472
		// Animate only if already placed
2473
		this[this.placed ? 'animate' : 'attr'](attribs);
2474
		this.placed = true;
2475
		this.alignAttr = attribs;
2476
 
2477
		return this;
2478
	},
2479
 
2480
	/**
2481
	 * Get the bounding box (width, height, x and y) for the element
2482
	 */
2483
	getBBox: function (reload) {
2484
		var wrapper = this,
2485
			bBox,// = wrapper.bBox,
2486
			renderer = wrapper.renderer,
2487
			width,
2488
			height,
2489
			rotation = wrapper.rotation,
2490
			element = wrapper.element,
2491
			styles = wrapper.styles,
2492
			rad = rotation * deg2rad,
2493
			textStr = wrapper.textStr,
2494
			textShadow,
2495
			elemStyle = element.style,
2496
			toggleTextShadowShim,
2497
			cacheKey;
2498
 
2499
		if (textStr !== UNDEFINED) {
2500
 
2501
			// Properties that affect bounding box
2502
			cacheKey = ['', rotation || 0, styles && styles.fontSize, element.style.width].join(',');
2503
 
2504
			// Since numbers are monospaced, and numerical labels appear a lot in a chart,
2505
			// we assume that a label of n characters has the same bounding box as others
2506
			// of the same length.
2507
			if (textStr === '' || numRegex.test(textStr)) {
2508
				cacheKey = 'num:' + textStr.toString().length + cacheKey;
2509
 
2510
			// Caching all strings reduces rendering time by 4-5%.
2511
			} else {
2512
				cacheKey = textStr + cacheKey;
2513
			}
2514
		}
2515
 
2516
		if (cacheKey && !reload) {
2517
			bBox = renderer.cache[cacheKey];
2518
		}
2519
 
2520
		// No cache found
2521
		if (!bBox) {
2522
 
2523
			// SVG elements
2524
			if (element.namespaceURI === SVG_NS || renderer.forExport) {
2525
				try { // Fails in Firefox if the container has display: none.
2526
 
2527
					// When the text shadow shim is used, we need to hide the fake shadows
2528
					// to get the correct bounding box (#3872)
2529
					toggleTextShadowShim = this.fakeTS && function (display) {
2530
						each(element.querySelectorAll('.' + PREFIX + 'text-shadow'), function (tspan) {
2531
							tspan.style.display = display;
2532
						});
2533
					};
2534
 
2535
					// Workaround for #3842, Firefox reporting wrong bounding box for shadows
2536
					if (isFirefox && elemStyle.textShadow) {
2537
						textShadow = elemStyle.textShadow;
2538
						elemStyle.textShadow = '';
2539
					} else if (toggleTextShadowShim) {
2540
						toggleTextShadowShim(NONE);
2541
					}
2542
 
2543
					bBox = element.getBBox ?
2544
						// SVG: use extend because IE9 is not allowed to change width and height in case
2545
						// of rotation (below)
2546
						extend({}, element.getBBox()) :
2547
						// Canvas renderer and legacy IE in export mode
2548
						{
2549
							width: element.offsetWidth,
2550
							height: element.offsetHeight
2551
						};
2552
 
2553
					// #3842
2554
					if (textShadow) {
2555
						elemStyle.textShadow = textShadow;
2556
					} else if (toggleTextShadowShim) {
2557
						toggleTextShadowShim('');
2558
					}
2559
				} catch (e) {}
2560
 
2561
				// If the bBox is not set, the try-catch block above failed. The other condition
2562
				// is for Opera that returns a width of -Infinity on hidden elements.
2563
				if (!bBox || bBox.width < 0) {
2564
					bBox = { width: 0, height: 0 };
2565
				}
2566
 
2567
 
2568
			// VML Renderer or useHTML within SVG
2569
			} else {
2570
 
2571
				bBox = wrapper.htmlGetBBox();
2572
 
2573
			}
2574
 
2575
			// True SVG elements as well as HTML elements in modern browsers using the .useHTML option
2576
			// need to compensated for rotation
2577
			if (renderer.isSVG) {
2578
				width = bBox.width;
2579
				height = bBox.height;
2580
 
2581
				// Workaround for wrong bounding box in IE9 and IE10 (#1101, #1505, #1669, #2568)
2582
				if (isMS && styles && styles.fontSize === '11px' && height.toPrecision(3) === '16.9') {
2583
					bBox.height = height = 14;
2584
				}
2585
 
2586
				// Adjust for rotated text
2587
				if (rotation) {
2588
					bBox.width = mathAbs(height * mathSin(rad)) + mathAbs(width * mathCos(rad));
2589
					bBox.height = mathAbs(height * mathCos(rad)) + mathAbs(width * mathSin(rad));
2590
				}
2591
			}
2592
 
2593
			// Cache it
2594
			if (cacheKey) {
2595
				renderer.cache[cacheKey] = bBox;
2596
			}
2597
		}
2598
		return bBox;
2599
	},
2600
 
2601
	/**
2602
	 * Show the element
2603
	 */
2604
	show: function (inherit) {
2605
		return this.attr({ visibility: inherit ? 'inherit' : VISIBLE });
2606
	},
2607
 
2608
	/**
2609
	 * Hide the element
2610
	 */
2611
	hide: function () {
2612
		return this.attr({ visibility: HIDDEN });
2613
	},
2614
 
2615
	fadeOut: function (duration) {
2616
		var elemWrapper = this;
2617
		elemWrapper.animate({
2618
			opacity: 0
2619
		}, {
2620
			duration: duration || 150,
2621
			complete: function () {
2622
				elemWrapper.attr({ y: -9999 }); // #3088, assuming we're only using this for tooltips
2623
			}
2624
		});
2625
	},
2626
 
2627
	/**
2628
	 * Add the element
2629
	 * @param {Object|Undefined} parent Can be an element, an element wrapper or undefined
2630
	 *	to append the element to the renderer.box.
2631
	 */
2632
	add: function (parent) {
2633
 
2634
		var renderer = this.renderer,
2635
			element = this.element,
2636
			inserted;
2637
 
2638
		if (parent) {
2639
			this.parentGroup = parent;
2640
		}
2641
 
2642
		// mark as inverted
2643
		this.parentInverted = parent && parent.inverted;
2644
 
2645
		// build formatted text
2646
		if (this.textStr !== undefined) {
2647
			renderer.buildText(this);
2648
		}
2649
 
2650
		// Mark as added
2651
		this.added = true;
2652
 
2653
		// If we're adding to renderer root, or other elements in the group
2654
		// have a z index, we need to handle it
2655
		if (!parent || parent.handleZ || this.zIndex) {
2656
			inserted = this.zIndexSetter();
2657
		}
2658
 
2659
		// If zIndex is not handled, append at the end
2660
		if (!inserted) {
2661
			(parent ? parent.element : renderer.box).appendChild(element);
2662
		}
2663
 
2664
		// fire an event for internal hooks
2665
		if (this.onAdd) {
2666
			this.onAdd();
2667
		}
2668
 
2669
		return this;
2670
	},
2671
 
2672
	/**
2673
	 * Removes a child either by removeChild or move to garbageBin.
2674
	 * Issue 490; in VML removeChild results in Orphaned nodes according to sIEve, discardElement does not.
2675
	 */
2676
	safeRemoveChild: function (element) {
2677
		var parentNode = element.parentNode;
2678
		if (parentNode) {
2679
			parentNode.removeChild(element);
2680
		}
2681
	},
2682
 
2683
	/**
2684
	 * Destroy the element and element wrapper
2685
	 */
2686
	destroy: function () {
2687
		var wrapper = this,
2688
			element = wrapper.element || {},
2689
			shadows = wrapper.shadows,
2690
			parentToClean = wrapper.renderer.isSVG && element.nodeName === 'SPAN' && wrapper.parentGroup,
2691
			grandParent,
2692
			key,
2693
			i;
2694
 
2695
		// remove events
2696
		element.onclick = element.onmouseout = element.onmouseover = element.onmousemove = element.point = null;
2697
		stop(wrapper); // stop running animations
2698
 
2699
		if (wrapper.clipPath) {
2700
			wrapper.clipPath = wrapper.clipPath.destroy();
2701
		}
2702
 
2703
		// Destroy stops in case this is a gradient object
2704
		if (wrapper.stops) {
2705
			for (i = 0; i < wrapper.stops.length; i++) {
2706
				wrapper.stops[i] = wrapper.stops[i].destroy();
2707
			}
2708
			wrapper.stops = null;
2709
		}
2710
 
2711
		// remove element
2712
		wrapper.safeRemoveChild(element);
2713
 
2714
		// destroy shadows
2715
		if (shadows) {
2716
			each(shadows, function (shadow) {
2717
				wrapper.safeRemoveChild(shadow);
2718
			});
2719
		}
2720
 
2721
		// In case of useHTML, clean up empty containers emulating SVG groups (#1960, #2393, #2697).
2722
		while (parentToClean && parentToClean.div && parentToClean.div.childNodes.length === 0) {
2723
			grandParent = parentToClean.parentGroup;
2724
			wrapper.safeRemoveChild(parentToClean.div);
2725
			delete parentToClean.div;
2726
			parentToClean = grandParent;
2727
		}
2728
 
2729
		// remove from alignObjects
2730
		if (wrapper.alignTo) {
2731
			erase(wrapper.renderer.alignedObjects, wrapper);
2732
		}
2733
 
2734
		for (key in wrapper) {
2735
			delete wrapper[key];
2736
		}
2737
 
2738
		return null;
2739
	},
2740
 
2741
	/**
2742
	 * Add a shadow to the element. Must be done after the element is added to the DOM
2743
	 * @param {Boolean|Object} shadowOptions
2744
	 */
2745
	shadow: function (shadowOptions, group, cutOff) {
2746
		var shadows = [],
2747
			i,
2748
			shadow,
2749
			element = this.element,
2750
			strokeWidth,
2751
			shadowWidth,
2752
			shadowElementOpacity,
2753
 
2754
			// compensate for inverted plot area
2755
			transform;
2756
 
2757
 
2758
		if (shadowOptions) {
2759
			shadowWidth = pick(shadowOptions.width, 3);
2760
			shadowElementOpacity = (shadowOptions.opacity || 0.15) / shadowWidth;
2761
			transform = this.parentInverted ?
2762
				'(-1,-1)' :
2763
				'(' + pick(shadowOptions.offsetX, 1) + ', ' + pick(shadowOptions.offsetY, 1) + ')';
2764
			for (i = 1; i <= shadowWidth; i++) {
2765
				shadow = element.cloneNode(0);
2766
				strokeWidth = (shadowWidth * 2) + 1 - (2 * i);
2767
				attr(shadow, {
2768
					'isShadow': 'true',
2769
					'stroke': shadowOptions.color || 'black',
2770
					'stroke-opacity': shadowElementOpacity * i,
2771
					'stroke-width': strokeWidth,
2772
					'transform': 'translate' + transform,
2773
					'fill': NONE
2774
				});
2775
				if (cutOff) {
2776
					attr(shadow, 'height', mathMax(attr(shadow, 'height') - strokeWidth, 0));
2777
					shadow.cutHeight = strokeWidth;
2778
				}
2779
 
2780
				if (group) {
2781
					group.element.appendChild(shadow);
2782
				} else {
2783
					element.parentNode.insertBefore(shadow, element);
2784
				}
2785
 
2786
				shadows.push(shadow);
2787
			}
2788
 
2789
			this.shadows = shadows;
2790
		}
2791
		return this;
2792
 
2793
	},
2794
 
2795
	xGetter: function (key) {
2796
		if (this.element.nodeName === 'circle') {
2797
			key = { x: 'cx', y: 'cy' }[key] || key;
2798
		}
2799
		return this._defaultGetter(key);
2800
	},
2801
 
2802
	/**
2803
	 * Get the current value of an attribute or pseudo attribute, used mainly
2804
	 * for animation.
2805
	 */
2806
	_defaultGetter: function (key) {
2807
		var ret = pick(this[key], this.element ? this.element.getAttribute(key) : null, 0);
2808
 
2809
		if (/^[\-0-9\.]+$/.test(ret)) { // is numerical
2810
			ret = parseFloat(ret);
2811
		}
2812
		return ret;
2813
	},
2814
 
2815
 
2816
	dSetter: function (value, key, element) {
2817
		if (value && value.join) { // join path
2818
			value = value.join(' ');
2819
		}
2820
		if (/(NaN| {2}|^$)/.test(value)) {
2821
			value = 'M 0 0';
2822
		}
2823
		element.setAttribute(key, value);
2824
 
2825
		this[key] = value;
2826
	},
2827
	dashstyleSetter: function (value) {
2828
		var i;
2829
		value = value && value.toLowerCase();
2830
		if (value) {
2831
			value = value
2832
				.replace('shortdashdotdot', '3,1,1,1,1,1,')
2833
				.replace('shortdashdot', '3,1,1,1')
2834
				.replace('shortdot', '1,1,')
2835
				.replace('shortdash', '3,1,')
2836
				.replace('longdash', '8,3,')
2837
				.replace(/dot/g, '1,3,')
2838
				.replace('dash', '4,3,')
2839
				.replace(/,$/, '')
2840
				.split(','); // ending comma
2841
 
2842
			i = value.length;
2843
			while (i--) {
2844
				value[i] = pInt(value[i]) * this['stroke-width'];
2845
			}
2846
			value = value.join(',')
2847
				.replace('NaN', 'none'); // #3226
2848
			this.element.setAttribute('stroke-dasharray', value);
2849
		}
2850
	},
2851
	alignSetter: function (value) {
2852
		this.element.setAttribute('text-anchor', { left: 'start', center: 'middle', right: 'end' }[value]);
2853
	},
2854
	opacitySetter: function (value, key, element) {
2855
		this[key] = value;
2856
		element.setAttribute(key, value);
2857
	},
2858
	titleSetter: function (value) {
2859
		var titleNode = this.element.getElementsByTagName('title')[0];
2860
		if (!titleNode) {
2861
			titleNode = doc.createElementNS(SVG_NS, 'title');
2862
			this.element.appendChild(titleNode);
2863
		}
2864
		titleNode.appendChild(
2865
			doc.createTextNode(
2866
				(String(pick(value), '')).replace(/<[^>]*>/g, '') // #3276, #3895
2867
			)
2868
		);
2869
	},
2870
	textSetter: function (value) {
2871
		if (value !== this.textStr) {
2872
			// Delete bBox memo when the text changes
2873
			delete this.bBox;
2874
 
2875
			this.textStr = value;
2876
			if (this.added) {
2877
				this.renderer.buildText(this);
2878
			}
2879
		}
2880
	},
2881
	fillSetter: function (value, key, element) {
2882
		if (typeof value === 'string') {
2883
			element.setAttribute(key, value);
2884
		} else if (value) {
2885
			this.colorGradient(value, key, element);
2886
		}
2887
	},
2888
	visibilitySetter: function (value, key, element) {
2889
		// IE9-11 doesn't handle visibilty:inherit well, so we remove the attribute instead (#2881, #3909)
2890
		if (value === 'inherit') {
2891
			element.removeAttribute(key);
2892
		} else {
2893
			element.setAttribute(key, value);
2894
		}
2895
	},
2896
	zIndexSetter: function (value, key) {
2897
		var renderer = this.renderer,
2898
			parentGroup = this.parentGroup,
2899
			parentWrapper = parentGroup || renderer,
2900
			parentNode = parentWrapper.element || renderer.box,
2901
			childNodes,
2902
			otherElement,
2903
			otherZIndex,
2904
			element = this.element,
2905
			inserted,
2906
			run = this.added,
2907
			i;
2908
 
2909
		if (defined(value)) {
2910
			element.setAttribute(key, value); // So we can read it for other elements in the group
2911
			value = +value;
2912
			if (this[key] === value) { // Only update when needed (#3865)
2913
				run = false;
2914
			}
2915
			this[key] = value;
2916
		}
2917
 
2918
		// Insert according to this and other elements' zIndex. Before .add() is called,
2919
		// nothing is done. Then on add, or by later calls to zIndexSetter, the node
2920
		// is placed on the right place in the DOM.
2921
		if (run) {
2922
			value = this.zIndex;
2923
 
2924
			if (value && parentGroup) {
2925
				parentGroup.handleZ = true;
2926
			}
2927
 
2928
			childNodes = parentNode.childNodes;
2929
			for (i = 0; i < childNodes.length && !inserted; i++) {
2930
				otherElement = childNodes[i];
2931
				otherZIndex = attr(otherElement, 'zIndex');
2932
				if (otherElement !== element && (
2933
						// Insert before the first element with a higher zIndex
2934
						pInt(otherZIndex) > value ||
2935
						// If no zIndex given, insert before the first element with a zIndex
2936
						(!defined(value) && defined(otherZIndex))
2937
 
2938
						)) {
2939
					parentNode.insertBefore(element, otherElement);
2940
					inserted = true;
2941
				}
2942
			}
2943
			if (!inserted) {
2944
				parentNode.appendChild(element);
2945
			}
2946
		}
2947
		return inserted;
2948
	},
2949
	_defaultSetter: function (value, key, element) {
2950
		element.setAttribute(key, value);
2951
	}
2952
};
2953
 
2954
// Some shared setters and getters
2955
SVGElement.prototype.yGetter = SVGElement.prototype.xGetter;
2956
SVGElement.prototype.translateXSetter = SVGElement.prototype.translateYSetter =
2957
		SVGElement.prototype.rotationSetter = SVGElement.prototype.verticalAlignSetter =
2958
		SVGElement.prototype.scaleXSetter = SVGElement.prototype.scaleYSetter = function (value, key) {
2959
	this[key] = value;
2960
	this.doTransform = true;
2961
};
2962
 
2963
// WebKit and Batik have problems with a stroke-width of zero, so in this case we remove the
2964
// stroke attribute altogether. #1270, #1369, #3065, #3072.
2965
SVGElement.prototype['stroke-widthSetter'] = SVGElement.prototype.strokeSetter = function (value, key, element) {
2966
	this[key] = value;
2967
	// Only apply the stroke attribute if the stroke width is defined and larger than 0
2968
	if (this.stroke && this['stroke-width']) {
2969
		this.strokeWidth = this['stroke-width'];
2970
		SVGElement.prototype.fillSetter.call(this, this.stroke, 'stroke', element); // use prototype as instance may be overridden
2971
		element.setAttribute('stroke-width', this['stroke-width']);
2972
		this.hasStroke = true;
2973
	} else if (key === 'stroke-width' && value === 0 && this.hasStroke) {
2974
		element.removeAttribute('stroke');
2975
		this.hasStroke = false;
2976
	}
2977
};
2978
 
2979
 
2980
/**
2981
 * The default SVG renderer
2982
 */
2983
var SVGRenderer = function () {
2984
	this.init.apply(this, arguments);
2985
};
2986
SVGRenderer.prototype = {
2987
	Element: SVGElement,
2988
 
2989
	/**
2990
	 * Initialize the SVGRenderer
2991
	 * @param {Object} container
2992
	 * @param {Number} width
2993
	 * @param {Number} height
2994
	 * @param {Boolean} forExport
2995
	 */
2996
	init: function (container, width, height, style, forExport, allowHTML) {
2997
		var renderer = this,
2998
			loc = location,
2999
			boxWrapper,
3000
			element,
3001
			desc;
3002
 
3003
		boxWrapper = renderer.createElement('svg')
3004
			.attr({
3005
				version: '1.1'
3006
			})
3007
			.css(this.getStyle(style));
3008
		element = boxWrapper.element;
3009
		container.appendChild(element);
3010
 
3011
		// For browsers other than IE, add the namespace attribute (#1978)
3012
		if (container.innerHTML.indexOf('xmlns') === -1) {
3013
			attr(element, 'xmlns', SVG_NS);
3014
		}
3015
 
3016
		// object properties
3017
		renderer.isSVG = true;
3018
		renderer.box = element;
3019
		renderer.boxWrapper = boxWrapper;
3020
		renderer.alignedObjects = [];
3021
 
3022
		// Page url used for internal references. #24, #672, #1070
3023
		renderer.url = (isFirefox || isWebKit) && doc.getElementsByTagName('base').length ?
3024
			loc.href
3025
				.replace(/#.*?$/, '') // remove the hash
3026
				.replace(/([\('\)])/g, '\\$1') // escape parantheses and quotes
3027
				.replace(/ /g, '%20') : // replace spaces (needed for Safari only)
3028
			'';
3029
 
3030
		// Add description
3031
		desc = this.createElement('desc').add();
3032
		desc.element.appendChild(doc.createTextNode('Created with ' + PRODUCT + ' ' + VERSION));
3033
 
3034
 
3035
		renderer.defs = this.createElement('defs').add();
3036
		renderer.allowHTML = allowHTML;
3037
		renderer.forExport = forExport;
3038
		renderer.gradients = {}; // Object where gradient SvgElements are stored
3039
		renderer.cache = {}; // Cache for numerical bounding boxes
3040
 
3041
		renderer.setSize(width, height, false);
3042
 
3043
 
3044
 
3045
		// Issue 110 workaround:
3046
		// In Firefox, if a div is positioned by percentage, its pixel position may land
3047
		// between pixels. The container itself doesn't display this, but an SVG element
3048
		// inside this container will be drawn at subpixel precision. In order to draw
3049
		// sharp lines, this must be compensated for. This doesn't seem to work inside
3050
		// iframes though (like in jsFiddle).
3051
		var subPixelFix, rect;
3052
		if (isFirefox && container.getBoundingClientRect) {
3053
			renderer.subPixelFix = subPixelFix = function () {
3054
				css(container, { left: 0, top: 0 });
3055
				rect = container.getBoundingClientRect();
3056
				css(container, {
3057
					left: (mathCeil(rect.left) - rect.left) + PX,
3058
					top: (mathCeil(rect.top) - rect.top) + PX
3059
				});
3060
			};
3061
 
3062
			// run the fix now
3063
			subPixelFix();
3064
 
3065
			// run it on resize
3066
			addEvent(win, 'resize', subPixelFix);
3067
		}
3068
	},
3069
 
3070
	getStyle: function (style) {
3071
		return (this.style = extend({
3072
			fontFamily: '"Lucida Grande", "Lucida Sans Unicode", Arial, Helvetica, sans-serif', // default font
3073
			fontSize: '12px'
3074
		}, style));
3075
	},
3076
 
3077
	/**
3078
	 * Detect whether the renderer is hidden. This happens when one of the parent elements
3079
	 * has display: none. #608.
3080
	 */
3081
	isHidden: function () {
3082
		return !this.boxWrapper.getBBox().width;
3083
	},
3084
 
3085
	/**
3086
	 * Destroys the renderer and its allocated members.
3087
	 */
3088
	destroy: function () {
3089
		var renderer = this,
3090
			rendererDefs = renderer.defs;
3091
		renderer.box = null;
3092
		renderer.boxWrapper = renderer.boxWrapper.destroy();
3093
 
3094
		// Call destroy on all gradient elements
3095
		destroyObjectProperties(renderer.gradients || {});
3096
		renderer.gradients = null;
3097
 
3098
		// Defs are null in VMLRenderer
3099
		// Otherwise, destroy them here.
3100
		if (rendererDefs) {
3101
			renderer.defs = rendererDefs.destroy();
3102
		}
3103
 
3104
		// Remove sub pixel fix handler
3105
		// We need to check that there is a handler, otherwise all functions that are registered for event 'resize' are removed
3106
		// See issue #982
3107
		if (renderer.subPixelFix) {
3108
			removeEvent(win, 'resize', renderer.subPixelFix);
3109
		}
3110
 
3111
		renderer.alignedObjects = null;
3112
 
3113
		return null;
3114
	},
3115
 
3116
	/**
3117
	 * Create a wrapper for an SVG element
3118
	 * @param {Object} nodeName
3119
	 */
3120
	createElement: function (nodeName) {
3121
		var wrapper = new this.Element();
3122
		wrapper.init(this, nodeName);
3123
		return wrapper;
3124
	},
3125
 
3126
	/**
3127
	 * Dummy function for use in canvas renderer
3128
	 */
3129
	draw: function () {},
3130
 
3131
	/**
3132
	 * Get converted radial gradient attributes
3133
	 */
3134
	getRadialAttr: function (radialReference, gradAttr) {
3135
		return {
3136
			cx: (radialReference[0] - radialReference[2] / 2) + gradAttr.cx * radialReference[2],
3137
			cy: (radialReference[1] - radialReference[2] / 2) + gradAttr.cy * radialReference[2],
3138
			r: gradAttr.r * radialReference[2]
3139
		};
3140
	},
3141
 
3142
	/**
3143
	 * Parse a simple HTML string into SVG tspans
3144
	 *
3145
	 * @param {Object} textNode The parent text SVG node
3146
	 */
3147
	buildText: function (wrapper) {
3148
		var textNode = wrapper.element,
3149
			renderer = this,
3150
			forExport = renderer.forExport,
3151
			textStr = pick(wrapper.textStr, '').toString(),
3152
			hasMarkup = textStr.indexOf('<') !== -1,
3153
			lines,
3154
			childNodes = textNode.childNodes,
3155
			styleRegex,
3156
			hrefRegex,
3157
			parentX = attr(textNode, 'x'),
3158
			textStyles = wrapper.styles,
3159
			width = wrapper.textWidth,
3160
			textLineHeight = textStyles && textStyles.lineHeight,
3161
			textShadow = textStyles && textStyles.textShadow,
3162
			ellipsis = textStyles && textStyles.textOverflow === 'ellipsis',
3163
			i = childNodes.length,
3164
			tempParent = width && !wrapper.added && this.box,
3165
			getLineHeight = function (tspan) {
3166
				return textLineHeight ?
3167
					pInt(textLineHeight) :
3168
					renderer.fontMetrics(
3169
						/(px|em)$/.test(tspan && tspan.style.fontSize) ?
3170
							tspan.style.fontSize :
3171
							((textStyles && textStyles.fontSize) || renderer.style.fontSize || 12),
3172
						tspan
3173
					).h;
3174
			},
3175
			unescapeAngleBrackets = function (inputStr) {
3176
				return inputStr.replace(/&lt;/g, '<').replace(/&gt;/g, '>');
3177
			};
3178
 
3179
		/// remove old text
3180
		while (i--) {
3181
			textNode.removeChild(childNodes[i]);
3182
		}
3183
 
3184
		// Skip tspans, add text directly to text node. The forceTSpan is a hook
3185
		// used in text outline hack.
3186
		if (!hasMarkup && !textShadow && !ellipsis && textStr.indexOf(' ') === -1) {
3187
			textNode.appendChild(doc.createTextNode(unescapeAngleBrackets(textStr)));
3188
			return;
3189
 
3190
		// Complex strings, add more logic
3191
		} else {
3192
 
3193
			styleRegex = /<.*style="([^"]+)".*>/;
3194
			hrefRegex = /<.*href="(http[^"]+)".*>/;
3195
 
3196
			if (tempParent) {
3197
				tempParent.appendChild(textNode); // attach it to the DOM to read offset width
3198
			}
3199
 
3200
			if (hasMarkup) {
3201
				lines = textStr
3202
					.replace(/<(b|strong)>/g, '<span style="font-weight:bold">')
3203
					.replace(/<(i|em)>/g, '<span style="font-style:italic">')
3204
					.replace(/<a/g, '<span')
3205
					.replace(/<\/(b|strong|i|em|a)>/g, '</span>')
3206
					.split(/<br.*?>/g);
3207
 
3208
			} else {
3209
				lines = [textStr];
3210
			}
3211
 
3212
 
3213
			// remove empty line at end
3214
			if (lines[lines.length - 1] === '') {
3215
				lines.pop();
3216
			}
3217
 
3218
 
3219
			// build the lines
3220
			each(lines, function (line, lineNo) {
3221
				var spans, spanNo = 0;
3222
 
3223
				line = line.replace(/<span/g, '|||<span').replace(/<\/span>/g, '</span>|||');
3224
				spans = line.split('|||');
3225
 
3226
				each(spans, function (span) {
3227
					if (span !== '' || spans.length === 1) {
3228
						var attributes = {},
3229
							tspan = doc.createElementNS(SVG_NS, 'tspan'),
3230
							spanStyle; // #390
3231
						if (styleRegex.test(span)) {
3232
							spanStyle = span.match(styleRegex)[1].replace(/(;| |^)color([ :])/, '$1fill$2');
3233
							attr(tspan, 'style', spanStyle);
3234
						}
3235
						if (hrefRegex.test(span) && !forExport) { // Not for export - #1529
3236
							attr(tspan, 'onclick', 'location.href=\"' + span.match(hrefRegex)[1] + '\"');
3237
							css(tspan, { cursor: 'pointer' });
3238
						}
3239
 
3240
						span = unescapeAngleBrackets(span.replace(/<(.|\n)*?>/g, '') || ' ');
3241
 
3242
						// Nested tags aren't supported, and cause crash in Safari (#1596)
3243
						if (span !== ' ') {
3244
 
3245
							// add the text node
3246
							tspan.appendChild(doc.createTextNode(span));
3247
 
3248
							if (!spanNo) { // first span in a line, align it to the left
3249
								if (lineNo && parentX !== null) {
3250
									attributes.x = parentX;
3251
								}
3252
							} else {
3253
								attributes.dx = 0; // #16
3254
							}
3255
 
3256
							// add attributes
3257
							attr(tspan, attributes);
3258
 
3259
							// Append it
3260
							textNode.appendChild(tspan);
3261
 
3262
							// first span on subsequent line, add the line height
3263
							if (!spanNo && lineNo) {
3264
 
3265
								// allow getting the right offset height in exporting in IE
3266
								if (!hasSVG && forExport) {
3267
									css(tspan, { display: 'block' });
3268
								}
3269
 
3270
								// Set the line height based on the font size of either
3271
								// the text element or the tspan element
3272
								attr(
3273
									tspan,
3274
									'dy',
3275
									getLineHeight(tspan)
3276
								);
3277
							}
3278
 
3279
							/*if (width) {
3280
								renderer.breakText(wrapper, width);
3281
							}*/
3282
 
3283
							// Check width and apply soft breaks or ellipsis
3284
							if (width) {
3285
								var words = span.replace(/([^\^])-/g, '$1- ').split(' '), // #1273
3286
									hasWhiteSpace = spans.length > 1 || lineNo || (words.length > 1 && textStyles.whiteSpace !== 'nowrap'),
3287
									tooLong,
3288
									wasTooLong,
3289
									actualWidth,
3290
									rest = [],
3291
									dy = getLineHeight(tspan),
3292
									softLineNo = 1,
3293
									rotation = wrapper.rotation,
3294
									wordStr = span, // for ellipsis
3295
									cursor = wordStr.length, // binary search cursor
3296
									bBox;
3297
 
3298
								while ((hasWhiteSpace || ellipsis) && (words.length || rest.length)) {
3299
									wrapper.rotation = 0; // discard rotation when computing box
3300
									bBox = wrapper.getBBox(true);
3301
									actualWidth = bBox.width;
3302
 
3303
									// Old IE cannot measure the actualWidth for SVG elements (#2314)
3304
									if (!hasSVG && renderer.forExport) {
3305
										actualWidth = renderer.measureSpanWidth(tspan.firstChild.data, wrapper.styles);
3306
									}
3307
 
3308
									tooLong = actualWidth > width;
3309
 
3310
									// For ellipsis, do a binary search for the correct string length
3311
									if (wasTooLong === undefined) {
3312
										wasTooLong = tooLong; // First time
3313
									}
3314
									if (ellipsis && wasTooLong) {
3315
										cursor /= 2;
3316
 
3317
										if (wordStr === '' || (!tooLong && cursor < 0.5)) {
3318
											words = []; // All ok, break out
3319
										} else {
3320
											if (tooLong) {
3321
												wasTooLong = true;
3322
											}
3323
											wordStr = span.substring(0, wordStr.length + (tooLong ? -1 : 1) * mathCeil(cursor));
3324
											words = [wordStr + (width > 3 ? '\u2026' : '')];
3325
											tspan.removeChild(tspan.firstChild);
3326
										}
3327
 
3328
									// Looping down, this is the first word sequence that is not too long,
3329
									// so we can move on to build the next line.
3330
									} else if (!tooLong || words.length === 1) {
3331
										words = rest;
3332
										rest = [];
3333
 
3334
										if (words.length) {
3335
											softLineNo++;
3336
 
3337
											tspan = doc.createElementNS(SVG_NS, 'tspan');
3338
											attr(tspan, {
3339
												dy: dy,
3340
												x: parentX
3341
											});
3342
											if (spanStyle) { // #390
3343
												attr(tspan, 'style', spanStyle);
3344
											}
3345
											textNode.appendChild(tspan);
3346
										}
3347
										if (actualWidth > width) { // a single word is pressing it out
3348
											width = actualWidth;
3349
										}
3350
									} else { // append to existing line tspan
3351
										tspan.removeChild(tspan.firstChild);
3352
										rest.unshift(words.pop());
3353
									}
3354
									if (words.length) {
3355
										tspan.appendChild(doc.createTextNode(words.join(' ').replace(/- /g, '-')));
3356
									}
3357
								}
3358
								if (wasTooLong) {
3359
									wrapper.attr('title', wrapper.textStr);
3360
								}
3361
								wrapper.rotation = rotation;
3362
							}
3363
 
3364
							spanNo++;
3365
						}
3366
					}
3367
				});
3368
			});
3369
			if (tempParent) {
3370
				tempParent.removeChild(textNode); // attach it to the DOM to read offset width
3371
			}
3372
 
3373
			// Apply the text shadow
3374
			if (textShadow && wrapper.applyTextShadow) {
3375
				wrapper.applyTextShadow(textShadow);
3376
			}
3377
		}
3378
	},
3379
 
3380
 
3381
 
3382
	/*
3383
	breakText: function (wrapper, width) {
3384
		var bBox = wrapper.getBBox(),
3385
			node = wrapper.element,
3386
			textLength = node.textContent.length,
3387
			pos = mathRound(width * textLength / bBox.width), // try this position first, based on average character width
3388
			increment = 0,
3389
			finalPos;
3390
 
3391
		if (bBox.width > width) {
3392
			while (finalPos === undefined) {
3393
				textLength = node.getSubStringLength(0, pos);
3394
 
3395
				if (textLength <= width) {
3396
					if (increment === -1) {
3397
						finalPos = pos;
3398
					} else {
3399
						increment = 1;
3400
					}
3401
				} else {
3402
					if (increment === 1) {
3403
						finalPos = pos - 1;
3404
					} else {
3405
						increment = -1;
3406
					}
3407
				}
3408
				pos += increment;
3409
			}
3410
		}
3411
		console.log(finalPos, node.getSubStringLength(0, finalPos))
3412
	},
3413
	*/
3414
 
3415
	/**
3416
	 * Returns white for dark colors and black for bright colors
3417
	 */
3418
	getContrast: function (color) {
3419
		color = Color(color).rgba;
3420
		return color[0] + color[1] + color[2] > 384 ? '#000000' : '#FFFFFF';
3421
	},
3422
 
3423
	/**
3424
	 * Create a button with preset states
3425
	 * @param {String} text
3426
	 * @param {Number} x
3427
	 * @param {Number} y
3428
	 * @param {Function} callback
3429
	 * @param {Object} normalState
3430
	 * @param {Object} hoverState
3431
	 * @param {Object} pressedState
3432
	 */
3433
	button: function (text, x, y, callback, normalState, hoverState, pressedState, disabledState, shape) {
3434
		var label = this.label(text, x, y, shape, null, null, null, null, 'button'),
3435
			curState = 0,
3436
			stateOptions,
3437
			stateStyle,
3438
			normalStyle,
3439
			hoverStyle,
3440
			pressedStyle,
3441
			disabledStyle,
3442
			verticalGradient = { x1: 0, y1: 0, x2: 0, y2: 1 };
3443
 
3444
		// Normal state - prepare the attributes
3445
		normalState = merge({
3446
			'stroke-width': 1,
3447
			stroke: '#CCCCCC',
3448
			fill: {
3449
				linearGradient: verticalGradient,
3450
				stops: [
3451
					[0, '#FEFEFE'],
3452
					[1, '#F6F6F6']
3453
				]
3454
			},
3455
			r: 2,
3456
			padding: 5,
3457
			style: {
3458
				color: 'black'
3459
			}
3460
		}, normalState);
3461
		normalStyle = normalState.style;
3462
		delete normalState.style;
3463
 
3464
		// Hover state
3465
		hoverState = merge(normalState, {
3466
			stroke: '#68A',
3467
			fill: {
3468
				linearGradient: verticalGradient,
3469
				stops: [
3470
					[0, '#FFF'],
3471
					[1, '#ACF']
3472
				]
3473
			}
3474
		}, hoverState);
3475
		hoverStyle = hoverState.style;
3476
		delete hoverState.style;
3477
 
3478
		// Pressed state
3479
		pressedState = merge(normalState, {
3480
			stroke: '#68A',
3481
			fill: {
3482
				linearGradient: verticalGradient,
3483
				stops: [
3484
					[0, '#9BD'],
3485
					[1, '#CDF']
3486
				]
3487
			}
3488
		}, pressedState);
3489
		pressedStyle = pressedState.style;
3490
		delete pressedState.style;
3491
 
3492
		// Disabled state
3493
		disabledState = merge(normalState, {
3494
			style: {
3495
				color: '#CCC'
3496
			}
3497
		}, disabledState);
3498
		disabledStyle = disabledState.style;
3499
		delete disabledState.style;
3500
 
3501
		// Add the events. IE9 and IE10 need mouseover and mouseout to funciton (#667).
3502
		addEvent(label.element, isMS ? 'mouseover' : 'mouseenter', function () {
3503
			if (curState !== 3) {
3504
				label.attr(hoverState)
3505
					.css(hoverStyle);
3506
			}
3507
		});
3508
		addEvent(label.element, isMS ? 'mouseout' : 'mouseleave', function () {
3509
			if (curState !== 3) {
3510
				stateOptions = [normalState, hoverState, pressedState][curState];
3511
				stateStyle = [normalStyle, hoverStyle, pressedStyle][curState];
3512
				label.attr(stateOptions)
3513
					.css(stateStyle);
3514
			}
3515
		});
3516
 
3517
		label.setState = function (state) {
3518
			label.state = curState = state;
3519
			if (!state) {
3520
				label.attr(normalState)
3521
					.css(normalStyle);
3522
			} else if (state === 2) {
3523
				label.attr(pressedState)
3524
					.css(pressedStyle);
3525
			} else if (state === 3) {
3526
				label.attr(disabledState)
3527
					.css(disabledStyle);
3528
			}
3529
		};
3530
 
3531
		return label
3532
			.on('click', function (e) {
3533
				if (curState !== 3) {
3534
					callback.call(label, e);
3535
				}
3536
			})
3537
			.attr(normalState)
3538
			.css(extend({ cursor: 'default' }, normalStyle));
3539
	},
3540
 
3541
	/**
3542
	 * Make a straight line crisper by not spilling out to neighbour pixels
3543
	 * @param {Array} points
3544
	 * @param {Number} width
3545
	 */
3546
	crispLine: function (points, width) {
3547
		// points format: [M, 0, 0, L, 100, 0]
3548
		// normalize to a crisp line
3549
		if (points[1] === points[4]) {
3550
			// Substract due to #1129. Now bottom and left axis gridlines behave the same.
3551
			points[1] = points[4] = mathRound(points[1]) - (width % 2 / 2);
3552
		}
3553
		if (points[2] === points[5]) {
3554
			points[2] = points[5] = mathRound(points[2]) + (width % 2 / 2);
3555
		}
3556
		return points;
3557
	},
3558
 
3559
 
3560
	/**
3561
	 * Draw a path
3562
	 * @param {Array} path An SVG path in array form
3563
	 */
3564
	path: function (path) {
3565
		var attr = {
3566
			fill: NONE
3567
		};
3568
		if (isArray(path)) {
3569
			attr.d = path;
3570
		} else if (isObject(path)) { // attributes
3571
			extend(attr, path);
3572
		}
3573
		return this.createElement('path').attr(attr);
3574
	},
3575
 
3576
	/**
3577
	 * Draw and return an SVG circle
3578
	 * @param {Number} x The x position
3579
	 * @param {Number} y The y position
3580
	 * @param {Number} r The radius
3581
	 */
3582
	circle: function (x, y, r) {
3583
		var attr = isObject(x) ?
3584
			x :
3585
			{
3586
				x: x,
3587
				y: y,
3588
				r: r
3589
			},
3590
			wrapper = this.createElement('circle');
3591
 
3592
		wrapper.xSetter = function (value) {
3593
			this.element.setAttribute('cx', value);
3594
		};
3595
		wrapper.ySetter = function (value) {
3596
			this.element.setAttribute('cy', value);
3597
		};
3598
		return wrapper.attr(attr);
3599
	},
3600
 
3601
	/**
3602
	 * Draw and return an arc
3603
	 * @param {Number} x X position
3604
	 * @param {Number} y Y position
3605
	 * @param {Number} r Radius
3606
	 * @param {Number} innerR Inner radius like used in donut charts
3607
	 * @param {Number} start Starting angle
3608
	 * @param {Number} end Ending angle
3609
	 */
3610
	arc: function (x, y, r, innerR, start, end) {
3611
		var arc;
3612
 
3613
		if (isObject(x)) {
3614
			y = x.y;
3615
			r = x.r;
3616
			innerR = x.innerR;
3617
			start = x.start;
3618
			end = x.end;
3619
			x = x.x;
3620
		}
3621
 
3622
		// Arcs are defined as symbols for the ability to set
3623
		// attributes in attr and animate
3624
		arc = this.symbol('arc', x || 0, y || 0, r || 0, r || 0, {
3625
			innerR: innerR || 0,
3626
			start: start || 0,
3627
			end: end || 0
3628
		});
3629
		arc.r = r; // #959
3630
		return arc;
3631
	},
3632
 
3633
	/**
3634
	 * Draw and return a rectangle
3635
	 * @param {Number} x Left position
3636
	 * @param {Number} y Top position
3637
	 * @param {Number} width
3638
	 * @param {Number} height
3639
	 * @param {Number} r Border corner radius
3640
	 * @param {Number} strokeWidth A stroke width can be supplied to allow crisp drawing
3641
	 */
3642
	rect: function (x, y, width, height, r, strokeWidth) {
3643
 
3644
		r = isObject(x) ? x.r : r;
3645
 
3646
		var wrapper = this.createElement('rect'),
3647
			attribs = isObject(x) ? x : x === UNDEFINED ? {} : {
3648
				x: x,
3649
				y: y,
3650
				width: mathMax(width, 0),
3651
				height: mathMax(height, 0)
3652
			};
3653
 
3654
		if (strokeWidth !== UNDEFINED) {
3655
			attribs.strokeWidth = strokeWidth;
3656
			attribs = wrapper.crisp(attribs);
3657
		}
3658
 
3659
		if (r) {
3660
			attribs.r = r;
3661
		}
3662
 
3663
		wrapper.rSetter = function (value) {
3664
			attr(this.element, {
3665
				rx: value,
3666
				ry: value
3667
			});
3668
		};
3669
 
3670
		return wrapper.attr(attribs);
3671
	},
3672
 
3673
	/**
3674
	 * Resize the box and re-align all aligned elements
3675
	 * @param {Object} width
3676
	 * @param {Object} height
3677
	 * @param {Boolean} animate
3678
	 *
3679
	 */
3680
	setSize: function (width, height, animate) {
3681
		var renderer = this,
3682
			alignedObjects = renderer.alignedObjects,
3683
			i = alignedObjects.length;
3684
 
3685
		renderer.width = width;
3686
		renderer.height = height;
3687
 
3688
		renderer.boxWrapper[pick(animate, true) ? 'animate' : 'attr']({
3689
			width: width,
3690
			height: height
3691
		});
3692
 
3693
		while (i--) {
3694
			alignedObjects[i].align();
3695
		}
3696
	},
3697
 
3698
	/**
3699
	 * Create a group
3700
	 * @param {String} name The group will be given a class name of 'highcharts-{name}'.
3701
	 *	 This can be used for styling and scripting.
3702
	 */
3703
	g: function (name) {
3704
		var elem = this.createElement('g');
3705
		return defined(name) ? elem.attr({ 'class': PREFIX + name }) : elem;
3706
	},
3707
 
3708
	/**
3709
	 * Display an image
3710
	 * @param {String} src
3711
	 * @param {Number} x
3712
	 * @param {Number} y
3713
	 * @param {Number} width
3714
	 * @param {Number} height
3715
	 */
3716
	image: function (src, x, y, width, height) {
3717
		var attribs = {
3718
				preserveAspectRatio: NONE
3719
			},
3720
			elemWrapper;
3721
 
3722
		// optional properties
3723
		if (arguments.length > 1) {
3724
			extend(attribs, {
3725
				x: x,
3726
				y: y,
3727
				width: width,
3728
				height: height
3729
			});
3730
		}
3731
 
3732
		elemWrapper = this.createElement('image').attr(attribs);
3733
 
3734
		// set the href in the xlink namespace
3735
		if (elemWrapper.element.setAttributeNS) {
3736
			elemWrapper.element.setAttributeNS('http://www.w3.org/1999/xlink',
3737
				'href', src);
3738
		} else {
3739
			// could be exporting in IE
3740
			// using href throws "not supported" in ie7 and under, requries regex shim to fix later
3741
			elemWrapper.element.setAttribute('hc-svg-href', src);
3742
		}
3743
		return elemWrapper;
3744
	},
3745
 
3746
	/**
3747
	 * Draw a symbol out of pre-defined shape paths from the namespace 'symbol' object.
3748
	 *
3749
	 * @param {Object} symbol
3750
	 * @param {Object} x
3751
	 * @param {Object} y
3752
	 * @param {Object} radius
3753
	 * @param {Object} options
3754
	 */
3755
	symbol: function (symbol, x, y, width, height, options) {
3756
 
3757
		var obj,
3758
 
3759
			// get the symbol definition function
3760
			symbolFn = this.symbols[symbol],
3761
 
3762
			// check if there's a path defined for this symbol
3763
			path = symbolFn && symbolFn(
3764
				mathRound(x),
3765
				mathRound(y),
3766
				width,
3767
				height,
3768
				options
3769
			),
3770
 
3771
			imageElement,
3772
			imageRegex = /^url\((.*?)\)$/,
3773
			imageSrc,
3774
			imageSize,
3775
			centerImage;
3776
 
3777
		if (path) {
3778
 
3779
			obj = this.path(path);
3780
			// expando properties for use in animate and attr
3781
			extend(obj, {
3782
				symbolName: symbol,
3783
				x: x,
3784
				y: y,
3785
				width: width,
3786
				height: height
3787
			});
3788
			if (options) {
3789
				extend(obj, options);
3790
			}
3791
 
3792
 
3793
		// image symbols
3794
		} else if (imageRegex.test(symbol)) {
3795
 
3796
			// On image load, set the size and position
3797
			centerImage = function (img, size) {
3798
				if (img.element) { // it may be destroyed in the meantime (#1390)
3799
					img.attr({
3800
						width: size[0],
3801
						height: size[1]
3802
					});
3803
 
3804
					if (!img.alignByTranslate) { // #185
3805
						img.translate(
3806
							mathRound((width - size[0]) / 2), // #1378
3807
							mathRound((height - size[1]) / 2)
3808
						);
3809
					}
3810
				}
3811
			};
3812
 
3813
			imageSrc = symbol.match(imageRegex)[1];
3814
			imageSize = symbolSizes[imageSrc] || (options && options.width && options.height && [options.width, options.height]);
3815
 
3816
			// Ireate the image synchronously, add attribs async
3817
			obj = this.image(imageSrc)
3818
				.attr({
3819
					x: x,
3820
					y: y
3821
				});
3822
			obj.isImg = true;
3823
 
3824
			if (imageSize) {
3825
				centerImage(obj, imageSize);
3826
			} else {
3827
				// Initialize image to be 0 size so export will still function if there's no cached sizes.
3828
				obj.attr({ width: 0, height: 0 });
3829
 
3830
				// Create a dummy JavaScript image to get the width and height. Due to a bug in IE < 8,
3831
				// the created element must be assigned to a variable in order to load (#292).
3832
				imageElement = createElement('img', {
3833
					onload: function () {
3834
 
3835
						// Special case for SVGs on IE11, the width is not accessible until the image is
3836
						// part of the DOM (#2854).
3837
						if (this.width === 0) {
3838
							css(this, {
3839
								position: ABSOLUTE,
3840
								top: '-999em'
3841
							});
3842
							document.body.appendChild(this);
3843
						}
3844
 
3845
						// Center the image
3846
						centerImage(obj, symbolSizes[imageSrc] = [this.width, this.height]);
3847
 
3848
						// Clean up after #2854 workaround.
3849
						if (this.parentNode) {
3850
							this.parentNode.removeChild(this);
3851
						}
3852
					},
3853
					src: imageSrc
3854
				});
3855
			}
3856
		}
3857
 
3858
		return obj;
3859
	},
3860
 
3861
	/**
3862
	 * An extendable collection of functions for defining symbol paths.
3863
	 */
3864
	symbols: {
3865
		'circle': function (x, y, w, h) {
3866
			var cpw = 0.166 * w;
3867
			return [
3868
				M, x + w / 2, y,
3869
				'C', x + w + cpw, y, x + w + cpw, y + h, x + w / 2, y + h,
3870
				'C', x - cpw, y + h, x - cpw, y, x + w / 2, y,
3871
				'Z'
3872
			];
3873
		},
3874
 
3875
		'square': function (x, y, w, h) {
3876
			return [
3877
				M, x, y,
3878
				L, x + w, y,
3879
				x + w, y + h,
3880
				x, y + h,
3881
				'Z'
3882
			];
3883
		},
3884
 
3885
		'triangle': function (x, y, w, h) {
3886
			return [
3887
				M, x + w / 2, y,
3888
				L, x + w, y + h,
3889
				x, y + h,
3890
				'Z'
3891
			];
3892
		},
3893
 
3894
		'triangle-down': function (x, y, w, h) {
3895
			return [
3896
				M, x, y,
3897
				L, x + w, y,
3898
				x + w / 2, y + h,
3899
				'Z'
3900
			];
3901
		},
3902
		'diamond': function (x, y, w, h) {
3903
			return [
3904
				M, x + w / 2, y,
3905
				L, x + w, y + h / 2,
3906
				x + w / 2, y + h,
3907
				x, y + h / 2,
3908
				'Z'
3909
			];
3910
		},
3911
		'arc': function (x, y, w, h, options) {
3912
			var start = options.start,
3913
				radius = options.r || w || h,
3914
				end = options.end - 0.001, // to prevent cos and sin of start and end from becoming equal on 360 arcs (related: #1561)
3915
				innerRadius = options.innerR,
3916
				open = options.open,
3917
				cosStart = mathCos(start),
3918
				sinStart = mathSin(start),
3919
				cosEnd = mathCos(end),
3920
				sinEnd = mathSin(end),
3921
				longArc = options.end - start < mathPI ? 0 : 1;
3922
 
3923
			return [
3924
				M,
3925
				x + radius * cosStart,
3926
				y + radius * sinStart,
3927
				'A', // arcTo
3928
				radius, // x radius
3929
				radius, // y radius
3930
				0, // slanting
3931
				longArc, // long or short arc
3932
				1, // clockwise
3933
				x + radius * cosEnd,
3934
				y + radius * sinEnd,
3935
				open ? M : L,
3936
				x + innerRadius * cosEnd,
3937
				y + innerRadius * sinEnd,
3938
				'A', // arcTo
3939
				innerRadius, // x radius
3940
				innerRadius, // y radius
3941
				0, // slanting
3942
				longArc, // long or short arc
3943
				0, // clockwise
3944
				x + innerRadius * cosStart,
3945
				y + innerRadius * sinStart,
3946
 
3947
				open ? '' : 'Z' // close
3948
			];
3949
		},
3950
 
3951
		/**
3952
		 * Callout shape used for default tooltips, also used for rounded rectangles in VML
3953
		 */
3954
		callout: function (x, y, w, h, options) {
3955
			var arrowLength = 6,
3956
				halfDistance = 6,
3957
				r = mathMin((options && options.r) || 0, w, h),
3958
				safeDistance = r + halfDistance,
3959
				anchorX = options && options.anchorX,
3960
				anchorY = options && options.anchorY,
3961
				path;
3962
 
3963
			path = [
3964
				'M', x + r, y,
3965
				'L', x + w - r, y, // top side
3966
				'C', x + w, y, x + w, y, x + w, y + r, // top-right corner
3967
				'L', x + w, y + h - r, // right side
3968
				'C', x + w, y + h, x + w, y + h, x + w - r, y + h, // bottom-right corner
3969
				'L', x + r, y + h, // bottom side
3970
				'C', x, y + h, x, y + h, x, y + h - r, // bottom-left corner
3971
				'L', x, y + r, // left side
3972
				'C', x, y, x, y, x + r, y // top-right corner
3973
			];
3974
 
3975
			if (anchorX && anchorX > w && anchorY > y + safeDistance && anchorY < y + h - safeDistance) { // replace right side
3976
				path.splice(13, 3,
3977
					'L', x + w, anchorY - halfDistance,
3978
					x + w + arrowLength, anchorY,
3979
					x + w, anchorY + halfDistance,
3980
					x + w, y + h - r
3981
				);
3982
			} else if (anchorX && anchorX < 0 && anchorY > y + safeDistance && anchorY < y + h - safeDistance) { // replace left side
3983
				path.splice(33, 3,
3984
					'L', x, anchorY + halfDistance,
3985
					x - arrowLength, anchorY,
3986
					x, anchorY - halfDistance,
3987
					x, y + r
3988
				);
3989
			} else if (anchorY && anchorY > h && anchorX > x + safeDistance && anchorX < x + w - safeDistance) { // replace bottom
3990
				path.splice(23, 3,
3991
					'L', anchorX + halfDistance, y + h,
3992
					anchorX, y + h + arrowLength,
3993
					anchorX - halfDistance, y + h,
3994
					x + r, y + h
3995
				);
3996
			} else if (anchorY && anchorY < 0 && anchorX > x + safeDistance && anchorX < x + w - safeDistance) { // replace top
3997
				path.splice(3, 3,
3998
					'L', anchorX - halfDistance, y,
3999
					anchorX, y - arrowLength,
4000
					anchorX + halfDistance, y,
4001
					w - r, y
4002
				);
4003
			}
4004
			return path;
4005
		}
4006
	},
4007
 
4008
	/**
4009
	 * Define a clipping rectangle
4010
	 * @param {String} id
4011
	 * @param {Number} x
4012
	 * @param {Number} y
4013
	 * @param {Number} width
4014
	 * @param {Number} height
4015
	 */
4016
	clipRect: function (x, y, width, height) {
4017
		var wrapper,
4018
			id = PREFIX + idCounter++,
4019
 
4020
			clipPath = this.createElement('clipPath').attr({
4021
				id: id
4022
			}).add(this.defs);
4023
 
4024
		wrapper = this.rect(x, y, width, height, 0).add(clipPath);
4025
		wrapper.id = id;
4026
		wrapper.clipPath = clipPath;
4027
		wrapper.count = 0;
4028
 
4029
		return wrapper;
4030
	},
4031
 
4032
 
4033
 
4034
 
4035
 
4036
	/**
4037
	 * Add text to the SVG object
4038
	 * @param {String} str
4039
	 * @param {Number} x Left position
4040
	 * @param {Number} y Top position
4041
	 * @param {Boolean} useHTML Use HTML to render the text
4042
	 */
4043
	text: function (str, x, y, useHTML) {
4044
 
4045
		// declare variables
4046
		var renderer = this,
4047
			fakeSVG = useCanVG || (!hasSVG && renderer.forExport),
4048
			wrapper,
4049
			attr = {};
4050
 
4051
		if (useHTML && (renderer.allowHTML || !renderer.forExport)) {
4052
			return renderer.html(str, x, y);
4053
		}
4054
 
4055
		attr.x = Math.round(x || 0); // X is always needed for line-wrap logic
4056
		if (y) {
4057
			attr.y = Math.round(y);
4058
		}
4059
		if (str || str === 0) {
4060
			attr.text = str;
4061
		}
4062
 
4063
		wrapper = renderer.createElement('text')
4064
			.attr(attr);
4065
 
4066
		// Prevent wrapping from creating false offsetWidths in export in legacy IE (#1079, #1063)
4067
		if (fakeSVG) {
4068
			wrapper.css({
4069
				position: ABSOLUTE
4070
			});
4071
		}
4072
 
4073
		if (!useHTML) {
4074
			wrapper.xSetter = function (value, key, element) {
4075
				var tspans = element.getElementsByTagName('tspan'),
4076
					tspan,
4077
					parentVal = element.getAttribute(key),
4078
					i;
4079
				for (i = 0; i < tspans.length; i++) {
4080
					tspan = tspans[i];
4081
					// If the x values are equal, the tspan represents a linebreak
4082
					if (tspan.getAttribute(key) === parentVal) {
4083
						tspan.setAttribute(key, value);
4084
					}
4085
				}
4086
				element.setAttribute(key, value);
4087
			};
4088
		}
4089
 
4090
		return wrapper;
4091
	},
4092
 
4093
	/**
4094
	 * Utility to return the baseline offset and total line height from the font size
4095
	 */
4096
	fontMetrics: function (fontSize, elem) {
4097
		var lineHeight,
4098
			baseline,
4099
			style;
4100
 
4101
		fontSize = fontSize || this.style.fontSize;
4102
		if (!fontSize && elem && win.getComputedStyle) {
4103
			elem = elem.element || elem; // SVGElement
4104
			style = win.getComputedStyle(elem, "");
4105
			fontSize = style && style.fontSize; // #4309, the style doesn't exist inside a hidden iframe in Firefox
4106
		}
4107
		fontSize = /px/.test(fontSize) ? pInt(fontSize) : /em/.test(fontSize) ? parseFloat(fontSize) * 12 : 12;
4108
 
4109
		// Empirical values found by comparing font size and bounding box height.
4110
		// Applies to the default font family. http://jsfiddle.net/highcharts/7xvn7/
4111
		lineHeight = fontSize < 24 ? fontSize + 3 : mathRound(fontSize * 1.2);
4112
		baseline = mathRound(lineHeight * 0.8);
4113
 
4114
		return {
4115
			h: lineHeight,
4116
			b: baseline,
4117
			f: fontSize
4118
		};
4119
	},
4120
 
4121
	/**
4122
	 * Correct X and Y positioning of a label for rotation (#1764)
4123
	 */
4124
	rotCorr: function (baseline, rotation, alterY) {
4125
		var y = baseline;
4126
		if (rotation && alterY) {
4127
			y = mathMax(y * mathCos(rotation * deg2rad), 4);
4128
		}
4129
		return {
4130
			x: (-baseline / 3) * mathSin(rotation * deg2rad),
4131
			y: y
4132
		};
4133
	},
4134
 
4135
	/**
4136
	 * Add a label, a text item that can hold a colored or gradient background
4137
	 * as well as a border and shadow.
4138
	 * @param {string} str
4139
	 * @param {Number} x
4140
	 * @param {Number} y
4141
	 * @param {String} shape
4142
	 * @param {Number} anchorX In case the shape has a pointer, like a flag, this is the
4143
	 *	coordinates it should be pinned to
4144
	 * @param {Number} anchorY
4145
	 * @param {Boolean} baseline Whether to position the label relative to the text baseline,
4146
	 *	like renderer.text, or to the upper border of the rectangle.
4147
	 * @param {String} className Class name for the group
4148
	 */
4149
	label: function (str, x, y, shape, anchorX, anchorY, useHTML, baseline, className) {
4150
 
4151
		var renderer = this,
4152
			wrapper = renderer.g(className),
4153
			text = renderer.text('', 0, 0, useHTML)
4154
				.attr({
4155
					zIndex: 1
4156
				}),
4157
				//.add(wrapper),
4158
			box,
4159
			bBox,
4160
			alignFactor = 0,
4161
			padding = 3,
4162
			paddingLeft = 0,
4163
			width,
4164
			height,
4165
			wrapperX,
4166
			wrapperY,
4167
			crispAdjust = 0,
4168
			deferredAttr = {},
4169
			baselineOffset,
4170
			needsBox;
4171
 
4172
		/**
4173
		 * This function runs after the label is added to the DOM (when the bounding box is
4174
		 * available), and after the text of the label is updated to detect the new bounding
4175
		 * box and reflect it in the border box.
4176
		 */
4177
		function updateBoxSize() {
4178
			var boxX,
4179
				boxY,
4180
				style = text.element.style;
4181
 
4182
			bBox = (width === undefined || height === undefined || wrapper.styles.textAlign) && defined(text.textStr) &&
4183
				text.getBBox(); //#3295 && 3514 box failure when string equals 0
4184
			wrapper.width = (width || bBox.width || 0) + 2 * padding + paddingLeft;
4185
			wrapper.height = (height || bBox.height || 0) + 2 * padding;
4186
 
4187
			// update the label-scoped y offset
4188
			baselineOffset = padding + renderer.fontMetrics(style && style.fontSize, text).b;
4189
 
4190
 
4191
			if (needsBox) {
4192
 
4193
				// create the border box if it is not already present
4194
				if (!box) {
4195
					boxX = mathRound(-alignFactor * padding) + crispAdjust;
4196
					boxY = (baseline ? -baselineOffset : 0) + crispAdjust;
4197
 
4198
					wrapper.box = box = shape ?
4199
						renderer.symbol(shape, boxX, boxY, wrapper.width, wrapper.height, deferredAttr) :
4200
						renderer.rect(boxX, boxY, wrapper.width, wrapper.height, 0, deferredAttr[STROKE_WIDTH]);
4201
 
4202
					if (!box.isImg) { // #4324, fill "none" causes it to be ignored by mouse events in IE
4203
						box.attr('fill', NONE);
4204
					}
4205
					box.add(wrapper);
4206
				}
4207
 
4208
				// apply the box attributes
4209
				if (!box.isImg) { // #1630
4210
					box.attr(extend({
4211
						width: mathRound(wrapper.width),
4212
						height: mathRound(wrapper.height)
4213
					}, deferredAttr));
4214
				}
4215
				deferredAttr = null;
4216
			}
4217
		}
4218
 
4219
		/**
4220
		 * This function runs after setting text or padding, but only if padding is changed
4221
		 */
4222
		function updateTextPadding() {
4223
			var styles = wrapper.styles,
4224
				textAlign = styles && styles.textAlign,
4225
				x = paddingLeft + padding * (1 - alignFactor),
4226
				y;
4227
 
4228
			// determin y based on the baseline
4229
			y = baseline ? 0 : baselineOffset;
4230
 
4231
			// compensate for alignment
4232
			if (defined(width) && bBox && (textAlign === 'center' || textAlign === 'right')) {
4233
				x += { center: 0.5, right: 1 }[textAlign] * (width - bBox.width);
4234
			}
4235
 
4236
			// update if anything changed
4237
			if (x !== text.x || y !== text.y) {
4238
				text.attr('x', x);
4239
				if (y !== UNDEFINED) {
4240
					text.attr('y', y);
4241
				}
4242
			}
4243
 
4244
			// record current values
4245
			text.x = x;
4246
			text.y = y;
4247
		}
4248
 
4249
		/**
4250
		 * Set a box attribute, or defer it if the box is not yet created
4251
		 * @param {Object} key
4252
		 * @param {Object} value
4253
		 */
4254
		function boxAttr(key, value) {
4255
			if (box) {
4256
				box.attr(key, value);
4257
			} else {
4258
				deferredAttr[key] = value;
4259
			}
4260
		}
4261
 
4262
		/**
4263
		 * After the text element is added, get the desired size of the border box
4264
		 * and add it before the text in the DOM.
4265
		 */
4266
		wrapper.onAdd = function () {
4267
			text.add(wrapper);
4268
			wrapper.attr({
4269
				text: (str || str === 0) ? str : '', // alignment is available now // #3295: 0 not rendered if given as a value
4270
				x: x,
4271
				y: y
4272
			});
4273
 
4274
			if (box && defined(anchorX)) {
4275
				wrapper.attr({
4276
					anchorX: anchorX,
4277
					anchorY: anchorY
4278
				});
4279
			}
4280
		};
4281
 
4282
		/*
4283
		 * Add specific attribute setters.
4284
		 */
4285
 
4286
		// only change local variables
4287
		wrapper.widthSetter = function (value) {
4288
			width = value;
4289
		};
4290
		wrapper.heightSetter = function (value) {
4291
			height = value;
4292
		};
4293
		wrapper.paddingSetter =  function (value) {
4294
			if (defined(value) && value !== padding) {
4295
				padding = wrapper.padding = value;
4296
				updateTextPadding();
4297
			}
4298
		};
4299
		wrapper.paddingLeftSetter =  function (value) {
4300
			if (defined(value) && value !== paddingLeft) {
4301
				paddingLeft = value;
4302
				updateTextPadding();
4303
			}
4304
		};
4305
 
4306
 
4307
		// change local variable and prevent setting attribute on the group
4308
		wrapper.alignSetter = function (value) {
4309
			alignFactor = { left: 0, center: 0.5, right: 1 }[value];
4310
		};
4311
 
4312
		// apply these to the box and the text alike
4313
		wrapper.textSetter = function (value) {
4314
			if (value !== UNDEFINED) {
4315
				text.textSetter(value);
4316
			}
4317
			updateBoxSize();
4318
			updateTextPadding();
4319
		};
4320
 
4321
		// apply these to the box but not to the text
4322
		wrapper['stroke-widthSetter'] = function (value, key) {
4323
			if (value) {
4324
				needsBox = true;
4325
			}
4326
			crispAdjust = value % 2 / 2;
4327
			boxAttr(key, value);
4328
		};
4329
		wrapper.strokeSetter = wrapper.fillSetter = wrapper.rSetter = function (value, key) {
4330
			if (key === 'fill' && value) {
4331
				needsBox = true;
4332
			}
4333
			boxAttr(key, value);
4334
		};
4335
		wrapper.anchorXSetter = function (value, key) {
4336
			anchorX = value;
4337
			boxAttr(key, mathRound(value) - crispAdjust - wrapperX);
4338
		};
4339
		wrapper.anchorYSetter = function (value, key) {
4340
			anchorY = value;
4341
			boxAttr(key, value - wrapperY);
4342
		};
4343
 
4344
		// rename attributes
4345
		wrapper.xSetter = function (value) {
4346
			wrapper.x = value; // for animation getter
4347
			if (alignFactor) {
4348
				value -= alignFactor * ((width || bBox.width) + padding);
4349
			}
4350
			wrapperX = mathRound(value);
4351
			wrapper.attr('translateX', wrapperX);
4352
		};
4353
		wrapper.ySetter = function (value) {
4354
			wrapperY = wrapper.y = mathRound(value);
4355
			wrapper.attr('translateY', wrapperY);
4356
		};
4357
 
4358
		// Redirect certain methods to either the box or the text
4359
		var baseCss = wrapper.css;
4360
		return extend(wrapper, {
4361
			/**
4362
			 * Pick up some properties and apply them to the text instead of the wrapper
4363
			 */
4364
			css: function (styles) {
4365
				if (styles) {
4366
					var textStyles = {};
4367
					styles = merge(styles); // create a copy to avoid altering the original object (#537)
4368
					each(wrapper.textProps, function (prop) {
4369
						if (styles[prop] !== UNDEFINED) {
4370
							textStyles[prop] = styles[prop];
4371
							delete styles[prop];
4372
						}
4373
					});
4374
					text.css(textStyles);
4375
				}
4376
				return baseCss.call(wrapper, styles);
4377
			},
4378
			/**
4379
			 * Return the bounding box of the box, not the group
4380
			 */
4381
			getBBox: function () {
4382
				return {
4383
					width: bBox.width + 2 * padding,
4384
					height: bBox.height + 2 * padding,
4385
					x: bBox.x - padding,
4386
					y: bBox.y - padding
4387
				};
4388
			},
4389
			/**
4390
			 * Apply the shadow to the box
4391
			 */
4392
			shadow: function (b) {
4393
				if (box) {
4394
					box.shadow(b);
4395
				}
4396
				return wrapper;
4397
			},
4398
			/**
4399
			 * Destroy and release memory.
4400
			 */
4401
			destroy: function () {
4402
 
4403
				// Added by button implementation
4404
				removeEvent(wrapper.element, 'mouseenter');
4405
				removeEvent(wrapper.element, 'mouseleave');
4406
 
4407
				if (text) {
4408
					text = text.destroy();
4409
				}
4410
				if (box) {
4411
					box = box.destroy();
4412
				}
4413
				// Call base implementation to destroy the rest
4414
				SVGElement.prototype.destroy.call(wrapper);
4415
 
4416
				// Release local pointers (#1298)
4417
				wrapper = renderer = updateBoxSize = updateTextPadding = boxAttr = null;
4418
			}
4419
		});
4420
	}
4421
}; // end SVGRenderer
4422
 
4423
 
4424
// general renderer
4425
Renderer = SVGRenderer;
4426
// extend SvgElement for useHTML option
4427
extend(SVGElement.prototype, {
4428
	/**
4429
	 * Apply CSS to HTML elements. This is used in text within SVG rendering and
4430
	 * by the VML renderer
4431
	 */
4432
	htmlCss: function (styles) {
4433
		var wrapper = this,
4434
			element = wrapper.element,
4435
			textWidth = styles && element.tagName === 'SPAN' && styles.width;
4436
 
4437
		if (textWidth) {
4438
			delete styles.width;
4439
			wrapper.textWidth = textWidth;
4440
			wrapper.updateTransform();
4441
		}
4442
		if (styles && styles.textOverflow === 'ellipsis') {
4443
			styles.whiteSpace = 'nowrap';
4444
			styles.overflow = 'hidden';
4445
		}
4446
		wrapper.styles = extend(wrapper.styles, styles);
4447
		css(wrapper.element, styles);
4448
 
4449
		return wrapper;
4450
	},
4451
 
4452
	/**
4453
	 * VML and useHTML method for calculating the bounding box based on offsets
4454
	 * @param {Boolean} refresh Whether to force a fresh value from the DOM or to
4455
	 * use the cached value
4456
	 *
4457
	 * @return {Object} A hash containing values for x, y, width and height
4458
	 */
4459
 
4460
	htmlGetBBox: function () {
4461
		var wrapper = this,
4462
			element = wrapper.element;
4463
 
4464
		// faking getBBox in exported SVG in legacy IE
4465
		// faking getBBox in exported SVG in legacy IE (is this a duplicate of the fix for #1079?)
4466
		if (element.nodeName === 'text') {
4467
			element.style.position = ABSOLUTE;
4468
		}
4469
 
4470
		return {
4471
			x: element.offsetLeft,
4472
			y: element.offsetTop,
4473
			width: element.offsetWidth,
4474
			height: element.offsetHeight
4475
		};
4476
	},
4477
 
4478
	/**
4479
	 * VML override private method to update elements based on internal
4480
	 * properties based on SVG transform
4481
	 */
4482
	htmlUpdateTransform: function () {
4483
		// aligning non added elements is expensive
4484
		if (!this.added) {
4485
			this.alignOnAdd = true;
4486
			return;
4487
		}
4488
 
4489
		var wrapper = this,
4490
			renderer = wrapper.renderer,
4491
			elem = wrapper.element,
4492
			translateX = wrapper.translateX || 0,
4493
			translateY = wrapper.translateY || 0,
4494
			x = wrapper.x || 0,
4495
			y = wrapper.y || 0,
4496
			align = wrapper.textAlign || 'left',
4497
			alignCorrection = { left: 0, center: 0.5, right: 1 }[align],
4498
			shadows = wrapper.shadows,
4499
			styles = wrapper.styles;
4500
 
4501
		// apply translate
4502
		css(elem, {
4503
			marginLeft: translateX,
4504
			marginTop: translateY
4505
		});
4506
		if (shadows) { // used in labels/tooltip
4507
			each(shadows, function (shadow) {
4508
				css(shadow, {
4509
					marginLeft: translateX + 1,
4510
					marginTop: translateY + 1
4511
				});
4512
			});
4513
		}
4514
 
4515
		// apply inversion
4516
		if (wrapper.inverted) { // wrapper is a group
4517
			each(elem.childNodes, function (child) {
4518
				renderer.invertChild(child, elem);
4519
			});
4520
		}
4521
 
4522
		if (elem.tagName === 'SPAN') {
4523
 
4524
			var width,
4525
				rotation = wrapper.rotation,
4526
				baseline,
4527
				textWidth = pInt(wrapper.textWidth),
4528
				currentTextTransform = [rotation, align, elem.innerHTML, wrapper.textWidth, wrapper.textAlign].join(',');
4529
 
4530
			if (currentTextTransform !== wrapper.cTT) { // do the calculations and DOM access only if properties changed
4531
 
4532
 
4533
				baseline = renderer.fontMetrics(elem.style.fontSize).b;
4534
 
4535
				// Renderer specific handling of span rotation
4536
				if (defined(rotation)) {
4537
					wrapper.setSpanRotation(rotation, alignCorrection, baseline);
4538
				}
4539
 
4540
				width = pick(wrapper.elemWidth, elem.offsetWidth);
4541
 
4542
				// Update textWidth
4543
				if (width > textWidth && /[ \-]/.test(elem.textContent || elem.innerText)) { // #983, #1254
4544
					css(elem, {
4545
						width: textWidth + PX,
4546
						display: 'block',
4547
						whiteSpace: (styles && styles.whiteSpace) || 'normal' // #3331
4548
					});
4549
					width = textWidth;
4550
				}
4551
 
4552
				wrapper.getSpanCorrection(width, baseline, alignCorrection, rotation, align);
4553
			}
4554
 
4555
			// apply position with correction
4556
			css(elem, {
4557
				left: (x + (wrapper.xCorr || 0)) + PX,
4558
				top: (y + (wrapper.yCorr || 0)) + PX
4559
			});
4560
 
4561
			// force reflow in webkit to apply the left and top on useHTML element (#1249)
4562
			if (isWebKit) {
4563
				baseline = elem.offsetHeight; // assigned to baseline for JSLint purpose
4564
			}
4565
 
4566
			// record current text transform
4567
			wrapper.cTT = currentTextTransform;
4568
		}
4569
	},
4570
 
4571
	/**
4572
	 * Set the rotation of an individual HTML span
4573
	 */
4574
	setSpanRotation: function (rotation, alignCorrection, baseline) {
4575
		var rotationStyle = {},
4576
			cssTransformKey = isMS ? '-ms-transform' : isWebKit ? '-webkit-transform' : isFirefox ? 'MozTransform' : isOpera ? '-o-transform' : '';
4577
 
4578
		rotationStyle[cssTransformKey] = rotationStyle.transform = 'rotate(' + rotation + 'deg)';
4579
		rotationStyle[cssTransformKey + (isFirefox ? 'Origin' : '-origin')] = rotationStyle.transformOrigin = (alignCorrection * 100) + '% ' + baseline + 'px';
4580
		css(this.element, rotationStyle);
4581
	},
4582
 
4583
	/**
4584
	 * Get the correction in X and Y positioning as the element is rotated.
4585
	 */
4586
	getSpanCorrection: function (width, baseline, alignCorrection) {
4587
		this.xCorr = -width * alignCorrection;
4588
		this.yCorr = -baseline;
4589
	}
4590
});
4591
 
4592
// Extend SvgRenderer for useHTML option.
4593
extend(SVGRenderer.prototype, {
4594
	/**
4595
	 * Create HTML text node. This is used by the VML renderer as well as the SVG
4596
	 * renderer through the useHTML option.
4597
	 *
4598
	 * @param {String} str
4599
	 * @param {Number} x
4600
	 * @param {Number} y
4601
	 */
4602
	html: function (str, x, y) {
4603
		var wrapper = this.createElement('span'),
4604
			element = wrapper.element,
4605
			renderer = wrapper.renderer;
4606
 
4607
		// Text setter
4608
		wrapper.textSetter = function (value) {
4609
			if (value !== element.innerHTML) {
4610
				delete this.bBox;
4611
			}
4612
			element.innerHTML = this.textStr = value;
4613
			wrapper.htmlUpdateTransform();
4614
		};
4615
 
4616
		// Various setters which rely on update transform
4617
		wrapper.xSetter = wrapper.ySetter = wrapper.alignSetter = wrapper.rotationSetter = function (value, key) {
4618
			if (key === 'align') {
4619
				key = 'textAlign'; // Do not overwrite the SVGElement.align method. Same as VML.
4620
			}
4621
			wrapper[key] = value;
4622
			wrapper.htmlUpdateTransform();
4623
		};
4624
 
4625
		// Set the default attributes
4626
		wrapper.attr({
4627
				text: str,
4628
				x: mathRound(x),
4629
				y: mathRound(y)
4630
			})
4631
			.css({
4632
				position: ABSOLUTE,
4633
				fontFamily: this.style.fontFamily,
4634
				fontSize: this.style.fontSize
4635
			});
4636
 
4637
		// Keep the whiteSpace style outside the wrapper.styles collection
4638
		element.style.whiteSpace = 'nowrap';
4639
 
4640
		// Use the HTML specific .css method
4641
		wrapper.css = wrapper.htmlCss;
4642
 
4643
		// This is specific for HTML within SVG
4644
		if (renderer.isSVG) {
4645
			wrapper.add = function (svgGroupWrapper) {
4646
 
4647
				var htmlGroup,
4648
					container = renderer.box.parentNode,
4649
					parentGroup,
4650
					parents = [];
4651
 
4652
				this.parentGroup = svgGroupWrapper;
4653
 
4654
				// Create a mock group to hold the HTML elements
4655
				if (svgGroupWrapper) {
4656
					htmlGroup = svgGroupWrapper.div;
4657
					if (!htmlGroup) {
4658
 
4659
						// Read the parent chain into an array and read from top down
4660
						parentGroup = svgGroupWrapper;
4661
						while (parentGroup) {
4662
 
4663
							parents.push(parentGroup);
4664
 
4665
							// Move up to the next parent group
4666
							parentGroup = parentGroup.parentGroup;
4667
						}
4668
 
4669
						// Ensure dynamically updating position when any parent is translated
4670
						each(parents.reverse(), function (parentGroup) {
4671
							var htmlGroupStyle,
4672
								cls = attr(parentGroup.element, 'class');
4673
 
4674
							if (cls) {
4675
								cls = { className: cls };
4676
							} // else null
4677
 
4678
							// Create a HTML div and append it to the parent div to emulate
4679
							// the SVG group structure
4680
							htmlGroup = parentGroup.div = parentGroup.div || createElement(DIV, cls, {
4681
								position: ABSOLUTE,
4682
								left: (parentGroup.translateX || 0) + PX,
4683
								top: (parentGroup.translateY || 0) + PX
4684
							}, htmlGroup || container); // the top group is appended to container
4685
 
4686
							// Shortcut
4687
							htmlGroupStyle = htmlGroup.style;
4688
 
4689
							// Set listeners to update the HTML div's position whenever the SVG group
4690
							// position is changed
4691
							extend(parentGroup, {
4692
								translateXSetter: function (value, key) {
4693
									htmlGroupStyle.left = value + PX;
4694
									parentGroup[key] = value;
4695
									parentGroup.doTransform = true;
4696
								},
4697
								translateYSetter: function (value, key) {
4698
									htmlGroupStyle.top = value + PX;
4699
									parentGroup[key] = value;
4700
									parentGroup.doTransform = true;
4701
								}
4702
							});
4703
 
4704
							// These properties are set as attributes on the SVG group, and as
4705
							// identical CSS properties on the div. (#3542)
4706
							each(['opacity', 'visibility'], function (prop) {
4707
								wrap(parentGroup, prop + 'Setter', function (proceed, value, key, elem) {
4708
									proceed.call(this, value, key, elem);
4709
									htmlGroupStyle[key] = value;
4710
								});
4711
							});
4712
						});
4713
 
4714
					}
4715
				} else {
4716
					htmlGroup = container;
4717
				}
4718
 
4719
				htmlGroup.appendChild(element);
4720
 
4721
				// Shared with VML:
4722
				wrapper.added = true;
4723
				if (wrapper.alignOnAdd) {
4724
					wrapper.htmlUpdateTransform();
4725
				}
4726
 
4727
				return wrapper;
4728
			};
4729
		}
4730
		return wrapper;
4731
	}
4732
});
4733
 
4734
/* ****************************************************************************
4735
 *                                                                            *
4736
 * START OF INTERNET EXPLORER <= 8 SPECIFIC CODE                              *
4737
 *                                                                            *
4738
 * For applications and websites that don't need IE support, like platform    *
4739
 * targeted mobile apps and web apps, this code can be removed.               *
4740
 *                                                                            *
4741
 *****************************************************************************/
4742
 
4743
/**
4744
 * @constructor
4745
 */
4746
var VMLRenderer, VMLElement;
4747
if (!hasSVG && !useCanVG) {
4748
 
4749
/**
4750
 * The VML element wrapper.
4751
 */
4752
VMLElement = {
4753
 
4754
	/**
4755
	 * Initialize a new VML element wrapper. It builds the markup as a string
4756
	 * to minimize DOM traffic.
4757
	 * @param {Object} renderer
4758
	 * @param {Object} nodeName
4759
	 */
4760
	init: function (renderer, nodeName) {
4761
		var wrapper = this,
4762
			markup =  ['<', nodeName, ' filled="f" stroked="f"'],
4763
			style = ['position: ', ABSOLUTE, ';'],
4764
			isDiv = nodeName === DIV;
4765
 
4766
		// divs and shapes need size
4767
		if (nodeName === 'shape' || isDiv) {
4768
			style.push('left:0;top:0;width:1px;height:1px;');
4769
		}
4770
		style.push('visibility: ', isDiv ? HIDDEN : VISIBLE);
4771
 
4772
		markup.push(' style="', style.join(''), '"/>');
4773
 
4774
		// create element with default attributes and style
4775
		if (nodeName) {
4776
			markup = isDiv || nodeName === 'span' || nodeName === 'img' ?
4777
				markup.join('')
4778
				: renderer.prepVML(markup);
4779
			wrapper.element = createElement(markup);
4780
		}
4781
 
4782
		wrapper.renderer = renderer;
4783
	},
4784
 
4785
	/**
4786
	 * Add the node to the given parent
4787
	 * @param {Object} parent
4788
	 */
4789
	add: function (parent) {
4790
		var wrapper = this,
4791
			renderer = wrapper.renderer,
4792
			element = wrapper.element,
4793
			box = renderer.box,
4794
			inverted = parent && parent.inverted,
4795
 
4796
			// get the parent node
4797
			parentNode = parent ?
4798
				parent.element || parent :
4799
				box;
4800
 
4801
 
4802
		// if the parent group is inverted, apply inversion on all children
4803
		if (inverted) { // only on groups
4804
			renderer.invertChild(element, parentNode);
4805
		}
4806
 
4807
		// append it
4808
		parentNode.appendChild(element);
4809
 
4810
		// align text after adding to be able to read offset
4811
		wrapper.added = true;
4812
		if (wrapper.alignOnAdd && !wrapper.deferUpdateTransform) {
4813
			wrapper.updateTransform();
4814
		}
4815
 
4816
		// fire an event for internal hooks
4817
		if (wrapper.onAdd) {
4818
			wrapper.onAdd();
4819
		}
4820
 
4821
		return wrapper;
4822
	},
4823
 
4824
	/**
4825
	 * VML always uses htmlUpdateTransform
4826
	 */
4827
	updateTransform: SVGElement.prototype.htmlUpdateTransform,
4828
 
4829
	/**
4830
	 * Set the rotation of a span with oldIE's filter
4831
	 */
4832
	setSpanRotation: function () {
4833
		// Adjust for alignment and rotation. Rotation of useHTML content is not yet implemented
4834
		// but it can probably be implemented for Firefox 3.5+ on user request. FF3.5+
4835
		// has support for CSS3 transform. The getBBox method also needs to be updated
4836
		// to compensate for the rotation, like it currently does for SVG.
4837
		// Test case: http://jsfiddle.net/highcharts/Ybt44/
4838
 
4839
		var rotation = this.rotation,
4840
			costheta = mathCos(rotation * deg2rad),
4841
			sintheta = mathSin(rotation * deg2rad);
4842
 
4843
		css(this.element, {
4844
			filter: rotation ? ['progid:DXImageTransform.Microsoft.Matrix(M11=', costheta,
4845
				', M12=', -sintheta, ', M21=', sintheta, ', M22=', costheta,
4846
				', sizingMethod=\'auto expand\')'].join('') : NONE
4847
		});
4848
	},
4849
 
4850
	/**
4851
	 * Get the positioning correction for the span after rotating.
4852
	 */
4853
	getSpanCorrection: function (width, baseline, alignCorrection, rotation, align) {
4854
 
4855
		var costheta = rotation ? mathCos(rotation * deg2rad) : 1,
4856
			sintheta = rotation ? mathSin(rotation * deg2rad) : 0,
4857
			height = pick(this.elemHeight, this.element.offsetHeight),
4858
			quad,
4859
			nonLeft = align && align !== 'left';
4860
 
4861
		// correct x and y
4862
		this.xCorr = costheta < 0 && -width;
4863
		this.yCorr = sintheta < 0 && -height;
4864
 
4865
		// correct for baseline and corners spilling out after rotation
4866
		quad = costheta * sintheta < 0;
4867
		this.xCorr += sintheta * baseline * (quad ? 1 - alignCorrection : alignCorrection);
4868
		this.yCorr -= costheta * baseline * (rotation ? (quad ? alignCorrection : 1 - alignCorrection) : 1);
4869
		// correct for the length/height of the text
4870
		if (nonLeft) {
4871
			this.xCorr -= width * alignCorrection * (costheta < 0 ? -1 : 1);
4872
			if (rotation) {
4873
				this.yCorr -= height * alignCorrection * (sintheta < 0 ? -1 : 1);
4874
			}
4875
			css(this.element, {
4876
				textAlign: align
4877
			});
4878
		}
4879
	},
4880
 
4881
	/**
4882
	 * Converts a subset of an SVG path definition to its VML counterpart. Takes an array
4883
	 * as the parameter and returns a string.
4884
	 */
4885
	pathToVML: function (value) {
4886
		// convert paths
4887
		var i = value.length,
4888
			path = [];
4889
 
4890
		while (i--) {
4891
 
4892
			// Multiply by 10 to allow subpixel precision.
4893
			// Substracting half a pixel seems to make the coordinates
4894
			// align with SVG, but this hasn't been tested thoroughly
4895
			if (isNumber(value[i])) {
4896
				path[i] = mathRound(value[i] * 10) - 5;
4897
			} else if (value[i] === 'Z') { // close the path
4898
				path[i] = 'x';
4899
			} else {
4900
				path[i] = value[i];
4901
 
4902
				// When the start X and end X coordinates of an arc are too close,
4903
				// they are rounded to the same value above. In this case, substract or
4904
				// add 1 from the end X and Y positions. #186, #760, #1371, #1410.
4905
				if (value.isArc && (value[i] === 'wa' || value[i] === 'at')) {
4906
					// Start and end X
4907
					if (path[i + 5] === path[i + 7]) {
4908
						path[i + 7] += value[i + 7] > value[i + 5] ? 1 : -1;
4909
					}
4910
					// Start and end Y
4911
					if (path[i + 6] === path[i + 8]) {
4912
						path[i + 8] += value[i + 8] > value[i + 6] ? 1 : -1;
4913
					}
4914
				}
4915
			}
4916
		}
4917
 
4918
 
4919
		// Loop up again to handle path shortcuts (#2132)
4920
		/*while (i++ < path.length) {
4921
			if (path[i] === 'H') { // horizontal line to
4922
				path[i] = 'L';
4923
				path.splice(i + 2, 0, path[i - 1]);
4924
			} else if (path[i] === 'V') { // vertical line to
4925
				path[i] = 'L';
4926
				path.splice(i + 1, 0, path[i - 2]);
4927
			}
4928
		}*/
4929
		return path.join(' ') || 'x';
4930
	},
4931
 
4932
	/**
4933
	 * Set the element's clipping to a predefined rectangle
4934
	 *
4935
	 * @param {String} id The id of the clip rectangle
4936
	 */
4937
	clip: function (clipRect) {
4938
		var wrapper = this,
4939
			clipMembers,
4940
			cssRet;
4941
 
4942
		if (clipRect) {
4943
			clipMembers = clipRect.members;
4944
			erase(clipMembers, wrapper); // Ensure unique list of elements (#1258)
4945
			clipMembers.push(wrapper);
4946
			wrapper.destroyClip = function () {
4947
				erase(clipMembers, wrapper);
4948
			};
4949
			cssRet = clipRect.getCSS(wrapper);
4950
 
4951
		} else {
4952
			if (wrapper.destroyClip) {
4953
				wrapper.destroyClip();
4954
			}
4955
			cssRet = { clip: docMode8 ? 'inherit' : 'rect(auto)' }; // #1214
4956
		}
4957
 
4958
		return wrapper.css(cssRet);
4959
 
4960
	},
4961
 
4962
	/**
4963
	 * Set styles for the element
4964
	 * @param {Object} styles
4965
	 */
4966
	css: SVGElement.prototype.htmlCss,
4967
 
4968
	/**
4969
	 * Removes a child either by removeChild or move to garbageBin.
4970
	 * Issue 490; in VML removeChild results in Orphaned nodes according to sIEve, discardElement does not.
4971
	 */
4972
	safeRemoveChild: function (element) {
4973
		// discardElement will detach the node from its parent before attaching it
4974
		// to the garbage bin. Therefore it is important that the node is attached and have parent.
4975
		if (element.parentNode) {
4976
			discardElement(element);
4977
		}
4978
	},
4979
 
4980
	/**
4981
	 * Extend element.destroy by removing it from the clip members array
4982
	 */
4983
	destroy: function () {
4984
		if (this.destroyClip) {
4985
			this.destroyClip();
4986
		}
4987
 
4988
		return SVGElement.prototype.destroy.apply(this);
4989
	},
4990
 
4991
	/**
4992
	 * Add an event listener. VML override for normalizing event parameters.
4993
	 * @param {String} eventType
4994
	 * @param {Function} handler
4995
	 */
4996
	on: function (eventType, handler) {
4997
		// simplest possible event model for internal use
4998
		this.element['on' + eventType] = function () {
4999
			var evt = win.event;
5000
			evt.target = evt.srcElement;
5001
			handler(evt);
5002
		};
5003
		return this;
5004
	},
5005
 
5006
	/**
5007
	 * In stacked columns, cut off the shadows so that they don't overlap
5008
	 */
5009
	cutOffPath: function (path, length) {
5010
 
5011
		var len;
5012
 
5013
		path = path.split(/[ ,]/);
5014
		len = path.length;
5015
 
5016
		if (len === 9 || len === 11) {
5017
			path[len - 4] = path[len - 2] = pInt(path[len - 2]) - 10 * length;
5018
		}
5019
		return path.join(' ');
5020
	},
5021
 
5022
	/**
5023
	 * Apply a drop shadow by copying elements and giving them different strokes
5024
	 * @param {Boolean|Object} shadowOptions
5025
	 */
5026
	shadow: function (shadowOptions, group, cutOff) {
5027
		var shadows = [],
5028
			i,
5029
			element = this.element,
5030
			renderer = this.renderer,
5031
			shadow,
5032
			elemStyle = element.style,
5033
			markup,
5034
			path = element.path,
5035
			strokeWidth,
5036
			modifiedPath,
5037
			shadowWidth,
5038
			shadowElementOpacity;
5039
 
5040
		// some times empty paths are not strings
5041
		if (path && typeof path.value !== 'string') {
5042
			path = 'x';
5043
		}
5044
		modifiedPath = path;
5045
 
5046
		if (shadowOptions) {
5047
			shadowWidth = pick(shadowOptions.width, 3);
5048
			shadowElementOpacity = (shadowOptions.opacity || 0.15) / shadowWidth;
5049
			for (i = 1; i <= 3; i++) {
5050
 
5051
				strokeWidth = (shadowWidth * 2) + 1 - (2 * i);
5052
 
5053
				// Cut off shadows for stacked column items
5054
				if (cutOff) {
5055
					modifiedPath = this.cutOffPath(path.value, strokeWidth + 0.5);
5056
				}
5057
 
5058
				markup = ['<shape isShadow="true" strokeweight="', strokeWidth,
5059
					'" filled="false" path="', modifiedPath,
5060
					'" coordsize="10 10" style="', element.style.cssText, '" />'];
5061
 
5062
				shadow = createElement(renderer.prepVML(markup),
5063
					null, {
5064
						left: pInt(elemStyle.left) + pick(shadowOptions.offsetX, 1),
5065
						top: pInt(elemStyle.top) + pick(shadowOptions.offsetY, 1)
5066
					}
5067
				);
5068
				if (cutOff) {
5069
					shadow.cutOff = strokeWidth + 1;
5070
				}
5071
 
5072
				// apply the opacity
5073
				markup = ['<stroke color="', shadowOptions.color || 'black', '" opacity="', shadowElementOpacity * i, '"/>'];
5074
				createElement(renderer.prepVML(markup), null, null, shadow);
5075
 
5076
 
5077
				// insert it
5078
				if (group) {
5079
					group.element.appendChild(shadow);
5080
				} else {
5081
					element.parentNode.insertBefore(shadow, element);
5082
				}
5083
 
5084
				// record it
5085
				shadows.push(shadow);
5086
 
5087
			}
5088
 
5089
			this.shadows = shadows;
5090
		}
5091
		return this;
5092
	},
5093
	updateShadows: noop, // Used in SVG only
5094
 
5095
	setAttr: function (key, value) {
5096
		if (docMode8) { // IE8 setAttribute bug
5097
			this.element[key] = value;
5098
		} else {
5099
			this.element.setAttribute(key, value);
5100
		}
5101
	},
5102
	classSetter: function (value) {
5103
		// IE8 Standards mode has problems retrieving the className unless set like this
5104
		this.element.className = value;
5105
	},
5106
	dashstyleSetter: function (value, key, element) {
5107
		var strokeElem = element.getElementsByTagName('stroke')[0] ||
5108
			createElement(this.renderer.prepVML(['<stroke/>']), null, null, element);
5109
		strokeElem[key] = value || 'solid';
5110
		this[key] = value; /* because changing stroke-width will change the dash length
5111
			and cause an epileptic effect */
5112
	},
5113
	dSetter: function (value, key, element) {
5114
		var i,
5115
			shadows = this.shadows;
5116
		value = value || [];
5117
		this.d = value.join && value.join(' '); // used in getter for animation
5118
 
5119
		element.path = value = this.pathToVML(value);
5120
 
5121
		// update shadows
5122
		if (shadows) {
5123
			i = shadows.length;
5124
			while (i--) {
5125
				shadows[i].path = shadows[i].cutOff ? this.cutOffPath(value, shadows[i].cutOff) : value;
5126
			}
5127
		}
5128
		this.setAttr(key, value);
5129
	},
5130
	fillSetter: function (value, key, element) {
5131
		var nodeName = element.nodeName;
5132
		if (nodeName === 'SPAN') { // text color
5133
			element.style.color = value;
5134
		} else if (nodeName !== 'IMG') { // #1336
5135
			element.filled = value !== NONE;
5136
			this.setAttr('fillcolor', this.renderer.color(value, element, key, this));
5137
		}
5138
	},
5139
	opacitySetter: noop, // Don't bother - animation is too slow and filters introduce artifacts
5140
	rotationSetter: function (value, key, element) {
5141
		var style = element.style;
5142
		this[key] = style[key] = value; // style is for #1873
5143
 
5144
		// Correction for the 1x1 size of the shape container. Used in gauge needles.
5145
		style.left = -mathRound(mathSin(value * deg2rad) + 1) + PX;
5146
		style.top = mathRound(mathCos(value * deg2rad)) + PX;
5147
	},
5148
	strokeSetter: function (value, key, element) {
5149
		this.setAttr('strokecolor', this.renderer.color(value, element, key));
5150
	},
5151
	'stroke-widthSetter': function (value, key, element) {
5152
		element.stroked = !!value; // VML "stroked" attribute
5153
		this[key] = value; // used in getter, issue #113
5154
		if (isNumber(value)) {
5155
			value += PX;
5156
		}
5157
		this.setAttr('strokeweight', value);
5158
	},
5159
	titleSetter: function (value, key) {
5160
		this.setAttr(key, value);
5161
	},
5162
	visibilitySetter: function (value, key, element) {
5163
 
5164
		// Handle inherited visibility
5165
		if (value === 'inherit') {
5166
			value = VISIBLE;
5167
		}
5168
 
5169
		// Let the shadow follow the main element
5170
		if (this.shadows) {
5171
			each(this.shadows, function (shadow) {
5172
				shadow.style[key] = value;
5173
			});
5174
		}
5175
 
5176
		// Instead of toggling the visibility CSS property, move the div out of the viewport.
5177
		// This works around #61 and #586
5178
		if (element.nodeName === 'DIV') {
5179
			value = value === HIDDEN ? '-999em' : 0;
5180
 
5181
			// In order to redraw, IE7 needs the div to be visible when tucked away
5182
			// outside the viewport. So the visibility is actually opposite of
5183
			// the expected value. This applies to the tooltip only.
5184
			if (!docMode8) {
5185
				element.style[key] = value ? VISIBLE : HIDDEN;
5186
			}
5187
			key = 'top';
5188
		}
5189
		element.style[key] = value;
5190
	},
5191
	xSetter: function (value, key, element) {
5192
		this[key] = value; // used in getter
5193
 
5194
		if (key === 'x') {
5195
			key = 'left';
5196
		} else if (key === 'y') {
5197
			key = 'top';
5198
		}/* else {
5199
			value = mathMax(0, value); // don't set width or height below zero (#311)
5200
		}*/
5201
 
5202
		// clipping rectangle special
5203
		if (this.updateClipping) {
5204
			this[key] = value; // the key is now 'left' or 'top' for 'x' and 'y'
5205
			this.updateClipping();
5206
		} else {
5207
			// normal
5208
			element.style[key] = value;
5209
		}
5210
	},
5211
	zIndexSetter: function (value, key, element) {
5212
		element.style[key] = value;
5213
	}
5214
};
5215
Highcharts.VMLElement = VMLElement = extendClass(SVGElement, VMLElement);
5216
 
5217
// Some shared setters
5218
VMLElement.prototype.ySetter =
5219
	VMLElement.prototype.widthSetter =
5220
	VMLElement.prototype.heightSetter =
5221
	VMLElement.prototype.xSetter;
5222
 
5223
 
5224
/**
5225
 * The VML renderer
5226
 */
5227
var VMLRendererExtension = { // inherit SVGRenderer
5228
 
5229
	Element: VMLElement,
5230
	isIE8: userAgent.indexOf('MSIE 8.0') > -1,
5231
 
5232
 
5233
	/**
5234
	 * Initialize the VMLRenderer
5235
	 * @param {Object} container
5236
	 * @param {Number} width
5237
	 * @param {Number} height
5238
	 */
5239
	init: function (container, width, height, style) {
5240
		var renderer = this,
5241
			boxWrapper,
5242
			box,
5243
			css;
5244
 
5245
		renderer.alignedObjects = [];
5246
 
5247
		boxWrapper = renderer.createElement(DIV)
5248
			.css(extend(this.getStyle(style), { position: RELATIVE}));
5249
		box = boxWrapper.element;
5250
		container.appendChild(boxWrapper.element);
5251
 
5252
 
5253
		// generate the containing box
5254
		renderer.isVML = true;
5255
		renderer.box = box;
5256
		renderer.boxWrapper = boxWrapper;
5257
		renderer.cache = {};
5258
 
5259
 
5260
		renderer.setSize(width, height, false);
5261
 
5262
		// The only way to make IE6 and IE7 print is to use a global namespace. However,
5263
		// with IE8 the only way to make the dynamic shapes visible in screen and print mode
5264
		// seems to be to add the xmlns attribute and the behaviour style inline.
5265
		if (!doc.namespaces.hcv) {
5266
 
5267
			doc.namespaces.add('hcv', 'urn:schemas-microsoft-com:vml');
5268
 
5269
			// Setup default CSS (#2153, #2368, #2384)
5270
			css = 'hcv\\:fill, hcv\\:path, hcv\\:shape, hcv\\:stroke' +
5271
				'{ behavior:url(#default#VML); display: inline-block; } ';
5272
			try {
5273
				doc.createStyleSheet().cssText = css;
5274
			} catch (e) {
5275
				doc.styleSheets[0].cssText += css;
5276
			}
5277
 
5278
		}
5279
	},
5280
 
5281
 
5282
	/**
5283
	 * Detect whether the renderer is hidden. This happens when one of the parent elements
5284
	 * has display: none
5285
	 */
5286
	isHidden: function () {
5287
		return !this.box.offsetWidth;
5288
	},
5289
 
5290
	/**
5291
	 * Define a clipping rectangle. In VML it is accomplished by storing the values
5292
	 * for setting the CSS style to all associated members.
5293
	 *
5294
	 * @param {Number} x
5295
	 * @param {Number} y
5296
	 * @param {Number} width
5297
	 * @param {Number} height
5298
	 */
5299
	clipRect: function (x, y, width, height) {
5300
 
5301
		// create a dummy element
5302
		var clipRect = this.createElement(),
5303
			isObj = isObject(x);
5304
 
5305
		// mimic a rectangle with its style object for automatic updating in attr
5306
		return extend(clipRect, {
5307
			members: [],
5308
			count: 0,
5309
			left: (isObj ? x.x : x) + 1,
5310
			top: (isObj ? x.y : y) + 1,
5311
			width: (isObj ? x.width : width) - 1,
5312
			height: (isObj ? x.height : height) - 1,
5313
			getCSS: function (wrapper) {
5314
				var element = wrapper.element,
5315
					nodeName = element.nodeName,
5316
					isShape = nodeName === 'shape',
5317
					inverted = wrapper.inverted,
5318
					rect = this,
5319
					top = rect.top - (isShape ? element.offsetTop : 0),
5320
					left = rect.left,
5321
					right = left + rect.width,
5322
					bottom = top + rect.height,
5323
					ret = {
5324
						clip: 'rect(' +
5325
							mathRound(inverted ? left : top) + 'px,' +
5326
							mathRound(inverted ? bottom : right) + 'px,' +
5327
							mathRound(inverted ? right : bottom) + 'px,' +
5328
							mathRound(inverted ? top : left) + 'px)'
5329
					};
5330
 
5331
				// issue 74 workaround
5332
				if (!inverted && docMode8 && nodeName === 'DIV') {
5333
					extend(ret, {
5334
						width: right + PX,
5335
						height: bottom + PX
5336
					});
5337
				}
5338
				return ret;
5339
			},
5340
 
5341
			// used in attr and animation to update the clipping of all members
5342
			updateClipping: function () {
5343
				each(clipRect.members, function (member) {
5344
					if (member.element) { // Deleted series, like in stock/members/series-remove demo. Should be removed from members, but this will do.
5345
						member.css(clipRect.getCSS(member));
5346
					}
5347
				});
5348
			}
5349
		});
5350
 
5351
	},
5352
 
5353
 
5354
	/**
5355
	 * Take a color and return it if it's a string, make it a gradient if it's a
5356
	 * gradient configuration object, and apply opacity.
5357
	 *
5358
	 * @param {Object} color The color or config object
5359
	 */
5360
	color: function (color, elem, prop, wrapper) {
5361
		var renderer = this,
5362
			colorObject,
5363
			regexRgba = /^rgba/,
5364
			markup,
5365
			fillType,
5366
			ret = NONE;
5367
 
5368
		// Check for linear or radial gradient
5369
		if (color && color.linearGradient) {
5370
			fillType = 'gradient';
5371
		} else if (color && color.radialGradient) {
5372
			fillType = 'pattern';
5373
		}
5374
 
5375
 
5376
		if (fillType) {
5377
 
5378
			var stopColor,
5379
				stopOpacity,
5380
				gradient = color.linearGradient || color.radialGradient,
5381
				x1,
5382
				y1,
5383
				x2,
5384
				y2,
5385
				opacity1,
5386
				opacity2,
5387
				color1,
5388
				color2,
5389
				fillAttr = '',
5390
				stops = color.stops,
5391
				firstStop,
5392
				lastStop,
5393
				colors = [],
5394
				addFillNode = function () {
5395
					// Add the fill subnode. When colors attribute is used, the meanings of opacity and o:opacity2
5396
					// are reversed.
5397
					markup = ['<fill colors="' + colors.join(',') + '" opacity="', opacity2, '" o:opacity2="', opacity1,
5398
						'" type="', fillType, '" ', fillAttr, 'focus="100%" method="any" />'];
5399
					createElement(renderer.prepVML(markup), null, null, elem);
5400
				};
5401
 
5402
			// Extend from 0 to 1
5403
			firstStop = stops[0];
5404
			lastStop = stops[stops.length - 1];
5405
			if (firstStop[0] > 0) {
5406
				stops.unshift([
5407
					0,
5408
					firstStop[1]
5409
				]);
5410
			}
5411
			if (lastStop[0] < 1) {
5412
				stops.push([
5413
					1,
5414
					lastStop[1]
5415
				]);
5416
			}
5417
 
5418
			// Compute the stops
5419
			each(stops, function (stop, i) {
5420
				if (regexRgba.test(stop[1])) {
5421
					colorObject = Color(stop[1]);
5422
					stopColor = colorObject.get('rgb');
5423
					stopOpacity = colorObject.get('a');
5424
				} else {
5425
					stopColor = stop[1];
5426
					stopOpacity = 1;
5427
				}
5428
 
5429
				// Build the color attribute
5430
				colors.push((stop[0] * 100) + '% ' + stopColor);
5431
 
5432
				// Only start and end opacities are allowed, so we use the first and the last
5433
				if (!i) {
5434
					opacity1 = stopOpacity;
5435
					color2 = stopColor;
5436
				} else {
5437
					opacity2 = stopOpacity;
5438
					color1 = stopColor;
5439
				}
5440
			});
5441
 
5442
			// Apply the gradient to fills only.
5443
			if (prop === 'fill') {
5444
 
5445
				// Handle linear gradient angle
5446
				if (fillType === 'gradient') {
5447
					x1 = gradient.x1 || gradient[0] || 0;
5448
					y1 = gradient.y1 || gradient[1] || 0;
5449
					x2 = gradient.x2 || gradient[2] || 0;
5450
					y2 = gradient.y2 || gradient[3] || 0;
5451
					fillAttr = 'angle="' + (90  - math.atan(
5452
						(y2 - y1) / // y vector
5453
						(x2 - x1) // x vector
5454
						) * 180 / mathPI) + '"';
5455
 
5456
					addFillNode();
5457
 
5458
				// Radial (circular) gradient
5459
				} else {
5460
 
5461
					var r = gradient.r,
5462
						sizex = r * 2,
5463
						sizey = r * 2,
5464
						cx = gradient.cx,
5465
						cy = gradient.cy,
5466
						radialReference = elem.radialReference,
5467
						bBox,
5468
						applyRadialGradient = function () {
5469
							if (radialReference) {
5470
								bBox = wrapper.getBBox();
5471
								cx += (radialReference[0] - bBox.x) / bBox.width - 0.5;
5472
								cy += (radialReference[1] - bBox.y) / bBox.height - 0.5;
5473
								sizex *= radialReference[2] / bBox.width;
5474
								sizey *= radialReference[2] / bBox.height;
5475
							}
5476
							fillAttr = 'src="' + defaultOptions.global.VMLRadialGradientURL + '" ' +
5477
								'size="' + sizex + ',' + sizey + '" ' +
5478
								'origin="0.5,0.5" ' +
5479
								'position="' + cx + ',' + cy + '" ' +
5480
								'color2="' + color2 + '" ';
5481
 
5482
							addFillNode();
5483
						};
5484
 
5485
					// Apply radial gradient
5486
					if (wrapper.added) {
5487
						applyRadialGradient();
5488
					} else {
5489
						// We need to know the bounding box to get the size and position right
5490
						wrapper.onAdd = applyRadialGradient;
5491
					}
5492
 
5493
					// The fill element's color attribute is broken in IE8 standards mode, so we
5494
					// need to set the parent shape's fillcolor attribute instead.
5495
					ret = color1;
5496
				}
5497
 
5498
			// Gradients are not supported for VML stroke, return the first color. #722.
5499
			} else {
5500
				ret = stopColor;
5501
			}
5502
 
5503
		// if the color is an rgba color, split it and add a fill node
5504
		// to hold the opacity component
5505
		} else if (regexRgba.test(color) && elem.tagName !== 'IMG') {
5506
 
5507
			colorObject = Color(color);
5508
 
5509
			markup = ['<', prop, ' opacity="', colorObject.get('a'), '"/>'];
5510
			createElement(this.prepVML(markup), null, null, elem);
5511
 
5512
			ret = colorObject.get('rgb');
5513
 
5514
 
5515
		} else {
5516
			var propNodes = elem.getElementsByTagName(prop); // 'stroke' or 'fill' node
5517
			if (propNodes.length) {
5518
				propNodes[0].opacity = 1;
5519
				propNodes[0].type = 'solid';
5520
			}
5521
			ret = color;
5522
		}
5523
 
5524
		return ret;
5525
	},
5526
 
5527
	/**
5528
	 * Take a VML string and prepare it for either IE8 or IE6/IE7.
5529
	 * @param {Array} markup A string array of the VML markup to prepare
5530
	 */
5531
	prepVML: function (markup) {
5532
		var vmlStyle = 'display:inline-block;behavior:url(#default#VML);',
5533
			isIE8 = this.isIE8;
5534
 
5535
		markup = markup.join('');
5536
 
5537
		if (isIE8) { // add xmlns and style inline
5538
			markup = markup.replace('/>', ' xmlns="urn:schemas-microsoft-com:vml" />');
5539
			if (markup.indexOf('style="') === -1) {
5540
				markup = markup.replace('/>', ' style="' + vmlStyle + '" />');
5541
			} else {
5542
				markup = markup.replace('style="', 'style="' + vmlStyle);
5543
			}
5544
 
5545
		} else { // add namespace
5546
			markup = markup.replace('<', '<hcv:');
5547
		}
5548
 
5549
		return markup;
5550
	},
5551
 
5552
	/**
5553
	 * Create rotated and aligned text
5554
	 * @param {String} str
5555
	 * @param {Number} x
5556
	 * @param {Number} y
5557
	 */
5558
	text: SVGRenderer.prototype.html,
5559
 
5560
	/**
5561
	 * Create and return a path element
5562
	 * @param {Array} path
5563
	 */
5564
	path: function (path) {
5565
		var attr = {
5566
			// subpixel precision down to 0.1 (width and height = 1px)
5567
			coordsize: '10 10'
5568
		};
5569
		if (isArray(path)) {
5570
			attr.d = path;
5571
		} else if (isObject(path)) { // attributes
5572
			extend(attr, path);
5573
		}
5574
		// create the shape
5575
		return this.createElement('shape').attr(attr);
5576
	},
5577
 
5578
	/**
5579
	 * Create and return a circle element. In VML circles are implemented as
5580
	 * shapes, which is faster than v:oval
5581
	 * @param {Number} x
5582
	 * @param {Number} y
5583
	 * @param {Number} r
5584
	 */
5585
	circle: function (x, y, r) {
5586
		var circle = this.symbol('circle');
5587
		if (isObject(x)) {
5588
			r = x.r;
5589
			y = x.y;
5590
			x = x.x;
5591
		}
5592
		circle.isCircle = true; // Causes x and y to mean center (#1682)
5593
		circle.r = r;
5594
		return circle.attr({ x: x, y: y });
5595
	},
5596
 
5597
	/**
5598
	 * Create a group using an outer div and an inner v:group to allow rotating
5599
	 * and flipping. A simple v:group would have problems with positioning
5600
	 * child HTML elements and CSS clip.
5601
	 *
5602
	 * @param {String} name The name of the group
5603
	 */
5604
	g: function (name) {
5605
		var wrapper,
5606
			attribs;
5607
 
5608
		// set the class name
5609
		if (name) {
5610
			attribs = { 'className': PREFIX + name, 'class': PREFIX + name };
5611
		}
5612
 
5613
		// the div to hold HTML and clipping
5614
		wrapper = this.createElement(DIV).attr(attribs);
5615
 
5616
		return wrapper;
5617
	},
5618
 
5619
	/**
5620
	 * VML override to create a regular HTML image
5621
	 * @param {String} src
5622
	 * @param {Number} x
5623
	 * @param {Number} y
5624
	 * @param {Number} width
5625
	 * @param {Number} height
5626
	 */
5627
	image: function (src, x, y, width, height) {
5628
		var obj = this.createElement('img')
5629
			.attr({ src: src });
5630
 
5631
		if (arguments.length > 1) {
5632
			obj.attr({
5633
				x: x,
5634
				y: y,
5635
				width: width,
5636
				height: height
5637
			});
5638
		}
5639
		return obj;
5640
	},
5641
 
5642
	/**
5643
	 * For rectangles, VML uses a shape for rect to overcome bugs and rotation problems
5644
	 */
5645
	createElement: function (nodeName) {
5646
		return nodeName === 'rect' ? this.symbol(nodeName) : SVGRenderer.prototype.createElement.call(this, nodeName);
5647
	},
5648
 
5649
	/**
5650
	 * In the VML renderer, each child of an inverted div (group) is inverted
5651
	 * @param {Object} element
5652
	 * @param {Object} parentNode
5653
	 */
5654
	invertChild: function (element, parentNode) {
5655
		var ren = this,
5656
			parentStyle = parentNode.style,
5657
			imgStyle = element.tagName === 'IMG' && element.style; // #1111
5658
 
5659
		css(element, {
5660
			flip: 'x',
5661
			left: pInt(parentStyle.width) - (imgStyle ? pInt(imgStyle.top) : 1),
5662
			top: pInt(parentStyle.height) - (imgStyle ? pInt(imgStyle.left) : 1),
5663
			rotation: -90
5664
		});
5665
 
5666
		// Recursively invert child elements, needed for nested composite shapes like box plots and error bars. #1680, #1806.
5667
		each(element.childNodes, function (child) {
5668
			ren.invertChild(child, element);
5669
		});
5670
	},
5671
 
5672
	/**
5673
	 * Symbol definitions that override the parent SVG renderer's symbols
5674
	 *
5675
	 */
5676
	symbols: {
5677
		// VML specific arc function
5678
		arc: function (x, y, w, h, options) {
5679
			var start = options.start,
5680
				end = options.end,
5681
				radius = options.r || w || h,
5682
				innerRadius = options.innerR,
5683
				cosStart = mathCos(start),
5684
				sinStart = mathSin(start),
5685
				cosEnd = mathCos(end),
5686
				sinEnd = mathSin(end),
5687
				ret;
5688
 
5689
			if (end - start === 0) { // no angle, don't show it.
5690
				return ['x'];
5691
			}
5692
 
5693
			ret = [
5694
				'wa', // clockwise arc to
5695
				x - radius, // left
5696
				y - radius, // top
5697
				x + radius, // right
5698
				y + radius, // bottom
5699
				x + radius * cosStart, // start x
5700
				y + radius * sinStart, // start y
5701
				x + radius * cosEnd, // end x
5702
				y + radius * sinEnd  // end y
5703
			];
5704
 
5705
			if (options.open && !innerRadius) {
5706
				ret.push(
5707
					'e',
5708
					M,
5709
					x,// - innerRadius,
5710
					y// - innerRadius
5711
				);
5712
			}
5713
 
5714
			ret.push(
5715
				'at', // anti clockwise arc to
5716
				x - innerRadius, // left
5717
				y - innerRadius, // top
5718
				x + innerRadius, // right
5719
				y + innerRadius, // bottom
5720
				x + innerRadius * cosEnd, // start x
5721
				y + innerRadius * sinEnd, // start y
5722
				x + innerRadius * cosStart, // end x
5723
				y + innerRadius * sinStart, // end y
5724
				'x', // finish path
5725
				'e' // close
5726
			);
5727
 
5728
			ret.isArc = true;
5729
			return ret;
5730
 
5731
		},
5732
		// Add circle symbol path. This performs significantly faster than v:oval.
5733
		circle: function (x, y, w, h, wrapper) {
5734
 
5735
			if (wrapper) {
5736
				w = h = 2 * wrapper.r;
5737
			}
5738
 
5739
			// Center correction, #1682
5740
			if (wrapper && wrapper.isCircle) {
5741
				x -= w / 2;
5742
				y -= h / 2;
5743
			}
5744
 
5745
			// Return the path
5746
			return [
5747
				'wa', // clockwisearcto
5748
				x, // left
5749
				y, // top
5750
				x + w, // right
5751
				y + h, // bottom
5752
				x + w, // start x
5753
				y + h / 2,     // start y
5754
				x + w, // end x
5755
				y + h / 2,     // end y
5756
				//'x', // finish path
5757
				'e' // close
5758
			];
5759
		},
5760
		/**
5761
		 * Add rectangle symbol path which eases rotation and omits arcsize problems
5762
		 * compared to the built-in VML roundrect shape. When borders are not rounded,
5763
		 * use the simpler square path, else use the callout path without the arrow.
5764
		 */
5765
		rect: function (x, y, w, h, options) {
5766
			return SVGRenderer.prototype.symbols[
5767
				!defined(options) || !options.r ? 'square' : 'callout'
5768
			].call(0, x, y, w, h, options);
5769
		}
5770
	}
5771
};
5772
Highcharts.VMLRenderer = VMLRenderer = function () {
5773
	this.init.apply(this, arguments);
5774
};
5775
VMLRenderer.prototype = merge(SVGRenderer.prototype, VMLRendererExtension);
5776
 
5777
	// general renderer
5778
	Renderer = VMLRenderer;
5779
}
5780
 
5781
// This method is used with exporting in old IE, when emulating SVG (see #2314)
5782
SVGRenderer.prototype.measureSpanWidth = function (text, styles) {
5783
	var measuringSpan = doc.createElement('span'),
5784
		offsetWidth,
5785
	textNode = doc.createTextNode(text);
5786
 
5787
	measuringSpan.appendChild(textNode);
5788
	css(measuringSpan, styles);
5789
	this.box.appendChild(measuringSpan);
5790
	offsetWidth = measuringSpan.offsetWidth;
5791
	discardElement(measuringSpan); // #2463
5792
	return offsetWidth;
5793
};
5794
 
5795
 
5796
/* ****************************************************************************
5797
 *                                                                            *
5798
 * END OF INTERNET EXPLORER <= 8 SPECIFIC CODE                                *
5799
 *                                                                            *
5800
 *****************************************************************************/
5801
/* ****************************************************************************
5802
 *                                                                            *
5803
 * START OF ANDROID < 3 SPECIFIC CODE. THIS CAN BE REMOVED IF YOU'RE NOT      *
5804
 * TARGETING THAT SYSTEM.                                                     *
5805
 *                                                                            *
5806
 *****************************************************************************/
5807
var CanVGRenderer,
5808
	CanVGController;
5809
 
5810
if (useCanVG) {
5811
	/**
5812
	 * The CanVGRenderer is empty from start to keep the source footprint small.
5813
	 * When requested, the CanVGController downloads the rest of the source packaged
5814
	 * together with the canvg library.
5815
	 */
5816
	Highcharts.CanVGRenderer = CanVGRenderer = function () {
5817
		// Override the global SVG namespace to fake SVG/HTML that accepts CSS
5818
		SVG_NS = 'http://www.w3.org/1999/xhtml';
5819
	};
5820
 
5821
	/**
5822
	 * Start with an empty symbols object. This is needed when exporting is used (exporting.src.js will add a few symbols), but
5823
	 * the implementation from SvgRenderer will not be merged in until first render.
5824
	 */
5825
	CanVGRenderer.prototype.symbols = {};
5826
 
5827
	/**
5828
	 * Handles on demand download of canvg rendering support.
5829
	 */
5830
	CanVGController = (function () {
5831
		// List of renderering calls
5832
		var deferredRenderCalls = [];
5833
 
5834
		/**
5835
		 * When downloaded, we are ready to draw deferred charts.
5836
		 */
5837
		function drawDeferred() {
5838
			var callLength = deferredRenderCalls.length,
5839
				callIndex;
5840
 
5841
			// Draw all pending render calls
5842
			for (callIndex = 0; callIndex < callLength; callIndex++) {
5843
				deferredRenderCalls[callIndex]();
5844
			}
5845
			// Clear the list
5846
			deferredRenderCalls = [];
5847
		}
5848
 
5849
		return {
5850
			push: function (func, scriptLocation) {
5851
				// Only get the script once
5852
				if (deferredRenderCalls.length === 0) {
5853
					getScript(scriptLocation, drawDeferred);
5854
				}
5855
				// Register render call
5856
				deferredRenderCalls.push(func);
5857
			}
5858
		};
5859
	}());
5860
 
5861
	Renderer = CanVGRenderer;
5862
} // end CanVGRenderer
5863
 
5864
/* ****************************************************************************
5865
 *                                                                            *
5866
 * END OF ANDROID < 3 SPECIFIC CODE                                           *
5867
 *                                                                            *
5868
 *****************************************************************************/
5869
 
5870
/**
5871
 * The Tick class
5872
 */
5873
function Tick(axis, pos, type, noLabel) {
5874
	this.axis = axis;
5875
	this.pos = pos;
5876
	this.type = type || '';
5877
	this.isNew = true;
5878
 
5879
	if (!type && !noLabel) {
5880
		this.addLabel();
5881
	}
5882
}
5883
 
5884
Tick.prototype = {
5885
	/**
5886
	 * Write the tick label
5887
	 */
5888
	addLabel: function () {
5889
		var tick = this,
5890
			axis = tick.axis,
5891
			options = axis.options,
5892
			chart = axis.chart,
5893
			categories = axis.categories,
5894
			names = axis.names,
5895
			pos = tick.pos,
5896
			labelOptions = options.labels,
5897
			str,
5898
			tickPositions = axis.tickPositions,
5899
			isFirst = pos === tickPositions[0],
5900
			isLast = pos === tickPositions[tickPositions.length - 1],
5901
			value = categories ?
5902
				pick(categories[pos], names[pos], pos) :
5903
				pos,
5904
			label = tick.label,
5905
			tickPositionInfo = tickPositions.info,
5906
			dateTimeLabelFormat;
5907
 
5908
		// Set the datetime label format. If a higher rank is set for this position, use that. If not,
5909
		// use the general format.
5910
		if (axis.isDatetimeAxis && tickPositionInfo) {
5911
			dateTimeLabelFormat = options.dateTimeLabelFormats[tickPositionInfo.higherRanks[pos] || tickPositionInfo.unitName];
5912
		}
5913
		// set properties for access in render method
5914
		tick.isFirst = isFirst;
5915
		tick.isLast = isLast;
5916
 
5917
		// get the string
5918
		str = axis.labelFormatter.call({
5919
			axis: axis,
5920
			chart: chart,
5921
			isFirst: isFirst,
5922
			isLast: isLast,
5923
			dateTimeLabelFormat: dateTimeLabelFormat,
5924
			value: axis.isLog ? correctFloat(lin2log(value)) : value
5925
		});
5926
 
5927
		// prepare CSS
5928
		//css = width && { width: mathMax(1, mathRound(width - 2 * (labelOptions.padding || 10))) + PX };
5929
 
5930
		// first call
5931
		if (!defined(label)) {
5932
 
5933
			tick.label = label =
5934
				defined(str) && labelOptions.enabled ?
5935
					chart.renderer.text(
5936
							str,
5937
							0,
5938
							0,
5939
							labelOptions.useHTML
5940
						)
5941
						//.attr(attr)
5942
						// without position absolute, IE export sometimes is wrong
5943
						.css(merge(labelOptions.style))
5944
						.add(axis.labelGroup) :
5945
					null;
5946
			tick.labelLength = label && label.getBBox().width; // Un-rotated length
5947
			tick.rotation = 0; // Base value to detect change for new calls to getBBox
5948
 
5949
		// update
5950
		} else if (label) {
5951
			label.attr({ text: str });
5952
		}
5953
	},
5954
 
5955
	/**
5956
	 * Get the offset height or width of the label
5957
	 */
5958
	getLabelSize: function () {
5959
		return this.label ?
5960
			this.label.getBBox()[this.axis.horiz ? 'height' : 'width'] :
5961
			0;
5962
	},
5963
 
5964
	/**
5965
	 * Handle the label overflow by adjusting the labels to the left and right edge, or
5966
	 * hide them if they collide into the neighbour label.
5967
	 */
5968
	handleOverflow: function (xy) {
5969
		var axis = this.axis,
5970
			pxPos = xy.x,
5971
			chartWidth = axis.chart.chartWidth,
5972
			spacing = axis.chart.spacing,
5973
			leftBound = pick(axis.labelLeft, mathMin(axis.pos, spacing[3])),
5974
			rightBound = pick(axis.labelRight, mathMax(axis.pos + axis.len, chartWidth - spacing[1])),
5975
			label = this.label,
5976
			rotation = this.rotation,
5977
			factor = { left: 0, center: 0.5, right: 1 }[axis.labelAlign],
5978
			labelWidth = label.getBBox().width,
5979
			slotWidth = axis.slotWidth,
5980
			xCorrection = factor,
5981
			goRight = 1,
5982
			leftPos,
5983
			rightPos,
5984
			textWidth,
5985
			css = {};
5986
 
5987
		// Check if the label overshoots the chart spacing box. If it does, move it.
5988
		// If it now overshoots the slotWidth, add ellipsis.
5989
		if (!rotation) {
5990
			leftPos = pxPos - factor * labelWidth;
5991
			rightPos = pxPos + (1 - factor) * labelWidth;
5992
 
5993
			if (leftPos < leftBound) {
5994
				slotWidth = xy.x + slotWidth * (1 - factor) - leftBound;
5995
			} else if (rightPos > rightBound) {
5996
				slotWidth = rightBound - xy.x + slotWidth * factor;
5997
				goRight = -1;
5998
			}
5999
 
6000
			slotWidth = mathMin(axis.slotWidth, slotWidth); // #4177
6001
			if (slotWidth < axis.slotWidth && axis.labelAlign === 'center') {
6002
				xy.x += goRight * (axis.slotWidth - slotWidth - xCorrection * (axis.slotWidth - mathMin(labelWidth, slotWidth)));
6003
			}
6004
			// If the label width exceeds the available space, set a text width to be
6005
			// picked up below. Also, if a width has been set before, we need to set a new
6006
			// one because the reported labelWidth will be limited by the box (#3938).
6007
			if (labelWidth > slotWidth || (axis.autoRotation && label.styles.width)) {
6008
				textWidth = slotWidth;
6009
			}
6010
 
6011
		// Add ellipsis to prevent rotated labels to be clipped against the edge of the chart
6012
		} else if (rotation < 0 && pxPos - factor * labelWidth < leftBound) {
6013
			textWidth = mathRound(pxPos / mathCos(rotation * deg2rad) - leftBound);
6014
		} else if (rotation > 0 && pxPos + factor * labelWidth > rightBound) {
6015
			textWidth = mathRound((chartWidth - pxPos) / mathCos(rotation * deg2rad));
6016
		}
6017
 
6018
		if (textWidth) {
6019
			css.width = textWidth;
6020
			if (!axis.options.labels.style.textOverflow) {
6021
				css.textOverflow = 'ellipsis';
6022
			}
6023
			label.css(css);
6024
		}
6025
	},
6026
 
6027
	/**
6028
	 * Get the x and y position for ticks and labels
6029
	 */
6030
	getPosition: function (horiz, pos, tickmarkOffset, old) {
6031
		var axis = this.axis,
6032
			chart = axis.chart,
6033
			cHeight = (old && chart.oldChartHeight) || chart.chartHeight;
6034
 
6035
		return {
6036
			x: horiz ?
6037
				axis.translate(pos + tickmarkOffset, null, null, old) + axis.transB :
6038
				axis.left + axis.offset + (axis.opposite ? ((old && chart.oldChartWidth) || chart.chartWidth) - axis.right - axis.left : 0),
6039
 
6040
			y: horiz ?
6041
				cHeight - axis.bottom + axis.offset - (axis.opposite ? axis.height : 0) :
6042
				cHeight - axis.translate(pos + tickmarkOffset, null, null, old) - axis.transB
6043
		};
6044
 
6045
	},
6046
 
6047
	/**
6048
	 * Get the x, y position of the tick label
6049
	 */
6050
	getLabelPosition: function (x, y, label, horiz, labelOptions, tickmarkOffset, index, step) {
6051
		var axis = this.axis,
6052
			transA = axis.transA,
6053
			reversed = axis.reversed,
6054
			staggerLines = axis.staggerLines,
6055
			rotCorr = axis.tickRotCorr || { x: 0, y: 0 },
6056
			yOffset = pick(labelOptions.y, rotCorr.y + (axis.side === 2 ? 8 : -(label.getBBox().height / 2))),
6057
			line;
6058
 
6059
		x = x + labelOptions.x + rotCorr.x - (tickmarkOffset && horiz ?
6060
			tickmarkOffset * transA * (reversed ? -1 : 1) : 0);
6061
		y = y + yOffset - (tickmarkOffset && !horiz ?
6062
			tickmarkOffset * transA * (reversed ? 1 : -1) : 0);
6063
 
6064
		// Correct for staggered labels
6065
		if (staggerLines) {
6066
			line = (index / (step || 1) % staggerLines);
6067
			y += line * (axis.labelOffset / staggerLines);
6068
		}
6069
 
6070
		return {
6071
			x: x,
6072
			y: mathRound(y)
6073
		};
6074
	},
6075
 
6076
	/**
6077
	 * Extendible method to return the path of the marker
6078
	 */
6079
	getMarkPath: function (x, y, tickLength, tickWidth, horiz, renderer) {
6080
		return renderer.crispLine([
6081
				M,
6082
				x,
6083
				y,
6084
				L,
6085
				x + (horiz ? 0 : -tickLength),
6086
				y + (horiz ? tickLength : 0)
6087
			], tickWidth);
6088
	},
6089
 
6090
	/**
6091
	 * Put everything in place
6092
	 *
6093
	 * @param index {Number}
6094
	 * @param old {Boolean} Use old coordinates to prepare an animation into new position
6095
	 */
6096
	render: function (index, old, opacity) {
6097
		var tick = this,
6098
			axis = tick.axis,
6099
			options = axis.options,
6100
			chart = axis.chart,
6101
			renderer = chart.renderer,
6102
			horiz = axis.horiz,
6103
			type = tick.type,
6104
			label = tick.label,
6105
			pos = tick.pos,
6106
			labelOptions = options.labels,
6107
			gridLine = tick.gridLine,
6108
			gridPrefix = type ? type + 'Grid' : 'grid',
6109
			tickPrefix = type ? type + 'Tick' : 'tick',
6110
			gridLineWidth = options[gridPrefix + 'LineWidth'],
6111
			gridLineColor = options[gridPrefix + 'LineColor'],
6112
			dashStyle = options[gridPrefix + 'LineDashStyle'],
6113
			tickLength = options[tickPrefix + 'Length'],
6114
			tickWidth = pick(options[tickPrefix + 'Width'], !type && axis.isXAxis ? 1 : 0), // X axis defaults to 1
6115
			tickColor = options[tickPrefix + 'Color'],
6116
			tickPosition = options[tickPrefix + 'Position'],
6117
			gridLinePath,
6118
			mark = tick.mark,
6119
			markPath,
6120
			step = /*axis.labelStep || */labelOptions.step,
6121
			attribs,
6122
			show = true,
6123
			tickmarkOffset = axis.tickmarkOffset,
6124
			xy = tick.getPosition(horiz, pos, tickmarkOffset, old),
6125
			x = xy.x,
6126
			y = xy.y,
6127
			reverseCrisp = ((horiz && x === axis.pos + axis.len) || (!horiz && y === axis.pos)) ? -1 : 1; // #1480, #1687
6128
 
6129
		opacity = pick(opacity, 1);
6130
		this.isActive = true;
6131
 
6132
		// create the grid line
6133
		if (gridLineWidth) {
6134
			gridLinePath = axis.getPlotLinePath(pos + tickmarkOffset, gridLineWidth * reverseCrisp, old, true);
6135
 
6136
			if (gridLine === UNDEFINED) {
6137
				attribs = {
6138
					stroke: gridLineColor,
6139
					'stroke-width': gridLineWidth
6140
				};
6141
				if (dashStyle) {
6142
					attribs.dashstyle = dashStyle;
6143
				}
6144
				if (!type) {
6145
					attribs.zIndex = 1;
6146
				}
6147
				if (old) {
6148
					attribs.opacity = 0;
6149
				}
6150
				tick.gridLine = gridLine =
6151
					gridLineWidth ?
6152
						renderer.path(gridLinePath)
6153
							.attr(attribs).add(axis.gridGroup) :
6154
						null;
6155
			}
6156
 
6157
			// If the parameter 'old' is set, the current call will be followed
6158
			// by another call, therefore do not do any animations this time
6159
			if (!old && gridLine && gridLinePath) {
6160
				gridLine[tick.isNew ? 'attr' : 'animate']({
6161
					d: gridLinePath,
6162
					opacity: opacity
6163
				});
6164
			}
6165
		}
6166
 
6167
		// create the tick mark
6168
		if (tickWidth && tickLength) {
6169
 
6170
			// negate the length
6171
			if (tickPosition === 'inside') {
6172
				tickLength = -tickLength;
6173
			}
6174
			if (axis.opposite) {
6175
				tickLength = -tickLength;
6176
			}
6177
 
6178
			markPath = tick.getMarkPath(x, y, tickLength, tickWidth * reverseCrisp, horiz, renderer);
6179
			if (mark) { // updating
6180
				mark.animate({
6181
					d: markPath,
6182
					opacity: opacity
6183
				});
6184
			} else { // first time
6185
				tick.mark = renderer.path(
6186
					markPath
6187
				).attr({
6188
					stroke: tickColor,
6189
					'stroke-width': tickWidth,
6190
					opacity: opacity
6191
				}).add(axis.axisGroup);
6192
			}
6193
		}
6194
 
6195
		// the label is created on init - now move it into place
6196
		if (label && !isNaN(x)) {
6197
			label.xy = xy = tick.getLabelPosition(x, y, label, horiz, labelOptions, tickmarkOffset, index, step);
6198
 
6199
			// Apply show first and show last. If the tick is both first and last, it is
6200
			// a single centered tick, in which case we show the label anyway (#2100).
6201
			if ((tick.isFirst && !tick.isLast && !pick(options.showFirstLabel, 1)) ||
6202
					(tick.isLast && !tick.isFirst && !pick(options.showLastLabel, 1))) {
6203
				show = false;
6204
 
6205
			// Handle label overflow and show or hide accordingly
6206
			} else if (horiz && !axis.isRadial && !labelOptions.step && !labelOptions.rotation && !old && opacity !== 0) {
6207
				tick.handleOverflow(xy);
6208
			}
6209
 
6210
			// apply step
6211
			if (step && index % step) {
6212
				// show those indices dividable by step
6213
				show = false;
6214
			}
6215
 
6216
			// Set the new position, and show or hide
6217
			if (show && !isNaN(xy.y)) {
6218
				xy.opacity = opacity;
6219
				label[tick.isNew ? 'attr' : 'animate'](xy);
6220
				tick.isNew = false;
6221
			} else {
6222
				label.attr('y', -9999); // #1338
6223
			}
6224
		}
6225
	},
6226
 
6227
	/**
6228
	 * Destructor for the tick prototype
6229
	 */
6230
	destroy: function () {
6231
		destroyObjectProperties(this, this.axis);
6232
	}
6233
};
6234
 
6235
/**
6236
 * The object wrapper for plot lines and plot bands
6237
 * @param {Object} options
6238
 */
6239
Highcharts.PlotLineOrBand = function (axis, options) {
6240
	this.axis = axis;
6241
 
6242
	if (options) {
6243
		this.options = options;
6244
		this.id = options.id;
6245
	}
6246
};
6247
 
6248
Highcharts.PlotLineOrBand.prototype = {
6249
 
6250
	/**
6251
	 * Render the plot line or plot band. If it is already existing,
6252
	 * move it.
6253
	 */
6254
	render: function () {
6255
		var plotLine = this,
6256
			axis = plotLine.axis,
6257
			horiz = axis.horiz,
6258
			options = plotLine.options,
6259
			optionsLabel = options.label,
6260
			label = plotLine.label,
6261
			width = options.width,
6262
			to = options.to,
6263
			from = options.from,
6264
			isBand = defined(from) && defined(to),
6265
			value = options.value,
6266
			dashStyle = options.dashStyle,
6267
			svgElem = plotLine.svgElem,
6268
			path = [],
6269
			addEvent,
6270
			eventType,
6271
			xs,
6272
			ys,
6273
			x,
6274
			y,
6275
			color = options.color,
6276
			zIndex = options.zIndex,
6277
			events = options.events,
6278
			attribs = {},
6279
			renderer = axis.chart.renderer;
6280
 
6281
		// logarithmic conversion
6282
		if (axis.isLog) {
6283
			from = log2lin(from);
6284
			to = log2lin(to);
6285
			value = log2lin(value);
6286
		}
6287
 
6288
		// plot line
6289
		if (width) {
6290
			path = axis.getPlotLinePath(value, width);
6291
			attribs = {
6292
				stroke: color,
6293
				'stroke-width': width
6294
			};
6295
			if (dashStyle) {
6296
				attribs.dashstyle = dashStyle;
6297
			}
6298
		} else if (isBand) { // plot band
6299
 
6300
			path = axis.getPlotBandPath(from, to, options);
6301
			if (color) {
6302
				attribs.fill = color;
6303
			}
6304
			if (options.borderWidth) {
6305
				attribs.stroke = options.borderColor;
6306
				attribs['stroke-width'] = options.borderWidth;
6307
			}
6308
		} else {
6309
			return;
6310
		}
6311
		// zIndex
6312
		if (defined(zIndex)) {
6313
			attribs.zIndex = zIndex;
6314
		}
6315
 
6316
		// common for lines and bands
6317
		if (svgElem) {
6318
			if (path) {
6319
				svgElem.animate({
6320
					d: path
6321
				}, null, svgElem.onGetPath);
6322
			} else {
6323
				svgElem.hide();
6324
				svgElem.onGetPath = function () {
6325
					svgElem.show();
6326
				};
6327
				if (label) {
6328
					plotLine.label = label = label.destroy();
6329
				}
6330
			}
6331
		} else if (path && path.length) {
6332
			plotLine.svgElem = svgElem = renderer.path(path)
6333
				.attr(attribs).add();
6334
 
6335
			// events
6336
			if (events) {
6337
				addEvent = function (eventType) {
6338
					svgElem.on(eventType, function (e) {
6339
						events[eventType].apply(plotLine, [e]);
6340
					});
6341
				};
6342
				for (eventType in events) {
6343
					addEvent(eventType);
6344
				}
6345
			}
6346
		}
6347
 
6348
		// the plot band/line label
6349
		if (optionsLabel && defined(optionsLabel.text) && path && path.length && axis.width > 0 && axis.height > 0) {
6350
			// apply defaults
6351
			optionsLabel = merge({
6352
				align: horiz && isBand && 'center',
6353
				x: horiz ? !isBand && 4 : 10,
6354
				verticalAlign : !horiz && isBand && 'middle',
6355
				y: horiz ? isBand ? 16 : 10 : isBand ? 6 : -4,
6356
				rotation: horiz && !isBand && 90
6357
			}, optionsLabel);
6358
 
6359
			// add the SVG element
6360
			if (!label) {
6361
				attribs = {
6362
					align: optionsLabel.textAlign || optionsLabel.align,
6363
					rotation: optionsLabel.rotation
6364
				};
6365
				if (defined(zIndex)) {
6366
					attribs.zIndex = zIndex;
6367
				}
6368
				plotLine.label = label = renderer.text(
6369
						optionsLabel.text,
6370
						0,
6371
						0,
6372
						optionsLabel.useHTML
6373
					)
6374
					.attr(attribs)
6375
					.css(optionsLabel.style)
6376
					.add();
6377
			}
6378
 
6379
			// get the bounding box and align the label
6380
			// #3000 changed to better handle choice between plotband or plotline
6381
			xs = [path[1], path[4], (isBand ? path[6] : path[1])];
6382
			ys = [path[2], path[5], (isBand ? path[7] : path[2])];
6383
			x = arrayMin(xs);
6384
			y = arrayMin(ys);
6385
 
6386
			label.align(optionsLabel, false, {
6387
				x: x,
6388
				y: y,
6389
				width: arrayMax(xs) - x,
6390
				height: arrayMax(ys) - y
6391
			});
6392
			label.show();
6393
 
6394
		} else if (label) { // move out of sight
6395
			label.hide();
6396
		}
6397
 
6398
		// chainable
6399
		return plotLine;
6400
	},
6401
 
6402
	/**
6403
	 * Remove the plot line or band
6404
	 */
6405
	destroy: function () {
6406
		// remove it from the lookup
6407
		erase(this.axis.plotLinesAndBands, this);
6408
 
6409
		delete this.axis;
6410
		destroyObjectProperties(this);
6411
	}
6412
};
6413
 
6414
/**
6415
 * Object with members for extending the Axis prototype
6416
 */
6417
 
6418
AxisPlotLineOrBandExtension = {
6419
 
6420
	/**
6421
	 * Create the path for a plot band
6422
	 */
6423
	getPlotBandPath: function (from, to) {
6424
		var toPath = this.getPlotLinePath(to, null, null, true),
6425
			path = this.getPlotLinePath(from, null, null, true);
6426
 
6427
		if (path && toPath && path.toString() !== toPath.toString()) { // #3836
6428
			path.push(
6429
				toPath[4],
6430
				toPath[5],
6431
				toPath[1],
6432
				toPath[2]
6433
			);
6434
		} else { // outside the axis area
6435
			path = null;
6436
		}
6437
 
6438
		return path;
6439
	},
6440
 
6441
	addPlotBand: function (options) {
6442
		return this.addPlotBandOrLine(options, 'plotBands');
6443
	},
6444
 
6445
	addPlotLine: function (options) {
6446
		return this.addPlotBandOrLine(options, 'plotLines');
6447
	},
6448
 
6449
	/**
6450
	 * Add a plot band or plot line after render time
6451
	 *
6452
	 * @param options {Object} The plotBand or plotLine configuration object
6453
	 */
6454
	addPlotBandOrLine: function (options, coll) {
6455
		var obj = new Highcharts.PlotLineOrBand(this, options).render(),
6456
			userOptions = this.userOptions;
6457
 
6458
		if (obj) { // #2189
6459
			// Add it to the user options for exporting and Axis.update
6460
			if (coll) {
6461
				userOptions[coll] = userOptions[coll] || [];
6462
				userOptions[coll].push(options);
6463
			}
6464
			this.plotLinesAndBands.push(obj);
6465
		}
6466
 
6467
		return obj;
6468
	},
6469
 
6470
	/**
6471
	 * Remove a plot band or plot line from the chart by id
6472
	 * @param {Object} id
6473
	 */
6474
	removePlotBandOrLine: function (id) {
6475
		var plotLinesAndBands = this.plotLinesAndBands,
6476
			options = this.options,
6477
			userOptions = this.userOptions,
6478
			i = plotLinesAndBands.length;
6479
		while (i--) {
6480
			if (plotLinesAndBands[i].id === id) {
6481
				plotLinesAndBands[i].destroy();
6482
			}
6483
		}
6484
		each([options.plotLines || [], userOptions.plotLines || [], options.plotBands || [], userOptions.plotBands || []], function (arr) {
6485
			i = arr.length;
6486
			while (i--) {
6487
				if (arr[i].id === id) {
6488
					erase(arr, arr[i]);
6489
				}
6490
			}
6491
		});
6492
	}
6493
};
6494
 
6495
/**
6496
 * Create a new axis object
6497
 * @param {Object} chart
6498
 * @param {Object} options
6499
 */
6500
var Axis = Highcharts.Axis = function () {
6501
	this.init.apply(this, arguments);
6502
};
6503
 
6504
Axis.prototype = {
6505
 
6506
	/**
6507
	 * Default options for the X axis - the Y axis has extended defaults
6508
	 */
6509
	defaultOptions: {
6510
		// allowDecimals: null,
6511
		// alternateGridColor: null,
6512
		// categories: [],
6513
		dateTimeLabelFormats: {
6514
			millisecond: '%H:%M:%S.%L',
6515
			second: '%H:%M:%S',
6516
			minute: '%H:%M',
6517
			hour: '%H:%M',
6518
			day: '%e. %b',
6519
			week: '%e. %b',
6520
			month: '%b \'%y',
6521
			year: '%Y'
6522
		},
6523
		endOnTick: false,
6524
		gridLineColor: '#D8D8D8',
6525
		// gridLineDashStyle: 'solid',
6526
		// gridLineWidth: 0,
6527
		// reversed: false,
6528
 
6529
		labels: {
6530
			enabled: true,
6531
			// rotation: 0,
6532
			// align: 'center',
6533
			// step: null,
6534
			style: {
6535
				color: '#606060',
6536
				cursor: 'default',
6537
				fontSize: '11px'
6538
			},
6539
			x: 0,
6540
			y: 15
6541
			/*formatter: function () {
6542
				return this.value;
6543
			},*/
6544
		},
6545
		lineColor: '#C0D0E0',
6546
		lineWidth: 1,
6547
		//linkedTo: null,
6548
		//max: undefined,
6549
		//min: undefined,
6550
		minPadding: 0.01,
6551
		maxPadding: 0.01,
6552
		//minRange: null,
6553
		minorGridLineColor: '#E0E0E0',
6554
		// minorGridLineDashStyle: null,
6555
		minorGridLineWidth: 1,
6556
		minorTickColor: '#A0A0A0',
6557
		//minorTickInterval: null,
6558
		minorTickLength: 2,
6559
		minorTickPosition: 'outside', // inside or outside
6560
		//minorTickWidth: 0,
6561
		//opposite: false,
6562
		//offset: 0,
6563
		//plotBands: [{
6564
		//	events: {},
6565
		//	zIndex: 1,
6566
		//	labels: { align, x, verticalAlign, y, style, rotation, textAlign }
6567
		//}],
6568
		//plotLines: [{
6569
		//	events: {}
6570
		//  dashStyle: {}
6571
		//	zIndex:
6572
		//	labels: { align, x, verticalAlign, y, style, rotation, textAlign }
6573
		//}],
6574
		//reversed: false,
6575
		// showFirstLabel: true,
6576
		// showLastLabel: true,
6577
		startOfWeek: 1,
6578
		startOnTick: false,
6579
		tickColor: '#C0D0E0',
6580
		//tickInterval: null,
6581
		tickLength: 10,
6582
		tickmarkPlacement: 'between', // on or between
6583
		tickPixelInterval: 100,
6584
		tickPosition: 'outside',
6585
		//tickWidth: 1,
6586
		title: {
6587
			//text: null,
6588
			align: 'middle', // low, middle or high
6589
			//margin: 0 for horizontal, 10 for vertical axes,
6590
			//rotation: 0,
6591
			//side: 'outside',
6592
			style: {
6593
				color: '#707070'
6594
			}
6595
			//x: 0,
6596
			//y: 0
6597
		},
6598
		type: 'linear' // linear, logarithmic or datetime
6599
		//visible: true
6600
	},
6601
 
6602
	/**
6603
	 * This options set extends the defaultOptions for Y axes
6604
	 */
6605
	defaultYAxisOptions: {
6606
		endOnTick: true,
6607
		gridLineWidth: 1,
6608
		tickPixelInterval: 72,
6609
		showLastLabel: true,
6610
		labels: {
6611
			x: -8,
6612
			y: 3
6613
		},
6614
		lineWidth: 0,
6615
		maxPadding: 0.05,
6616
		minPadding: 0.05,
6617
		startOnTick: true,
6618
		//tickWidth: 0,
6619
		title: {
6620
			rotation: 270,
6621
			text: 'Values'
6622
		},
6623
		stackLabels: {
6624
			enabled: false,
6625
			//align: dynamic,
6626
			//y: dynamic,
6627
			//x: dynamic,
6628
			//verticalAlign: dynamic,
6629
			//textAlign: dynamic,
6630
			//rotation: 0,
6631
			formatter: function () {
6632
				return Highcharts.numberFormat(this.total, -1);
6633
			},
6634
			style: merge(defaultPlotOptions.line.dataLabels.style, { color: '#000000' })
6635
		}
6636
	},
6637
 
6638
	/**
6639
	 * These options extend the defaultOptions for left axes
6640
	 */
6641
	defaultLeftAxisOptions: {
6642
		labels: {
6643
			x: -15,
6644
			y: null
6645
		},
6646
		title: {
6647
			rotation: 270
6648
		}
6649
	},
6650
 
6651
	/**
6652
	 * These options extend the defaultOptions for right axes
6653
	 */
6654
	defaultRightAxisOptions: {
6655
		labels: {
6656
			x: 15,
6657
			y: null
6658
		},
6659
		title: {
6660
			rotation: 90
6661
		}
6662
	},
6663
 
6664
	/**
6665
	 * These options extend the defaultOptions for bottom axes
6666
	 */
6667
	defaultBottomAxisOptions: {
6668
		labels: {
6669
			autoRotation: [-45],
6670
			x: 0,
6671
			y: null // based on font size
6672
			// overflow: undefined,
6673
			// staggerLines: null
6674
		},
6675
		title: {
6676
			rotation: 0
6677
		}
6678
	},
6679
	/**
6680
	 * These options extend the defaultOptions for top axes
6681
	 */
6682
	defaultTopAxisOptions: {
6683
		labels: {
6684
			autoRotation: [-45],
6685
			x: 0,
6686
			y: -15
6687
			// overflow: undefined
6688
			// staggerLines: null
6689
		},
6690
		title: {
6691
			rotation: 0
6692
		}
6693
	},
6694
 
6695
	/**
6696
	 * Initialize the axis
6697
	 */
6698
	init: function (chart, userOptions) {
6699
 
6700
 
6701
		var isXAxis = userOptions.isX,
6702
			axis = this;
6703
 
6704
		axis.chart = chart;
6705
 
6706
		// Flag, is the axis horizontal
6707
		axis.horiz = chart.inverted ? !isXAxis : isXAxis;
6708
 
6709
		// Flag, isXAxis
6710
		axis.isXAxis = isXAxis;
6711
		axis.coll = isXAxis ? 'xAxis' : 'yAxis';
6712
 
6713
		axis.opposite = userOptions.opposite; // needed in setOptions
6714
		axis.side = userOptions.side || (axis.horiz ?
6715
				(axis.opposite ? 0 : 2) : // top : bottom
6716
				(axis.opposite ? 1 : 3));  // right : left
6717
 
6718
		axis.setOptions(userOptions);
6719
 
6720
 
6721
		var options = this.options,
6722
			type = options.type,
6723
			isDatetimeAxis = type === 'datetime';
6724
 
6725
		axis.labelFormatter = options.labels.formatter || axis.defaultLabelFormatter; // can be overwritten by dynamic format
6726
 
6727
 
6728
		// Flag, stagger lines or not
6729
		axis.userOptions = userOptions;
6730
 
6731
		//axis.axisTitleMargin = UNDEFINED,// = options.title.margin,
6732
		axis.minPixelPadding = 0;
6733
 
6734
		axis.reversed = options.reversed;
6735
		axis.visible = options.visible !== false;
6736
		axis.zoomEnabled = options.zoomEnabled !== false;
6737
 
6738
		// Initial categories
6739
		axis.categories = options.categories || type === 'category';
6740
		axis.names = axis.names || []; // Preserve on update (#3830)
6741
 
6742
		// Elements
6743
		//axis.axisGroup = UNDEFINED;
6744
		//axis.gridGroup = UNDEFINED;
6745
		//axis.axisTitle = UNDEFINED;
6746
		//axis.axisLine = UNDEFINED;
6747
 
6748
		// Shorthand types
6749
		axis.isLog = type === 'logarithmic';
6750
		axis.isDatetimeAxis = isDatetimeAxis;
6751
 
6752
		// Flag, if axis is linked to another axis
6753
		axis.isLinked = defined(options.linkedTo);
6754
		// Linked axis.
6755
		//axis.linkedParent = UNDEFINED;
6756
 
6757
		// Tick positions
6758
		//axis.tickPositions = UNDEFINED; // array containing predefined positions
6759
		// Tick intervals
6760
		//axis.tickInterval = UNDEFINED;
6761
		//axis.minorTickInterval = UNDEFINED;
6762
 
6763
 
6764
		// Major ticks
6765
		axis.ticks = {};
6766
		axis.labelEdge = [];
6767
		// Minor ticks
6768
		axis.minorTicks = {};
6769
 
6770
		// List of plotLines/Bands
6771
		axis.plotLinesAndBands = [];
6772
 
6773
		// Alternate bands
6774
		axis.alternateBands = {};
6775
 
6776
		// Axis metrics
6777
		//axis.left = UNDEFINED;
6778
		//axis.top = UNDEFINED;
6779
		//axis.width = UNDEFINED;
6780
		//axis.height = UNDEFINED;
6781
		//axis.bottom = UNDEFINED;
6782
		//axis.right = UNDEFINED;
6783
		//axis.transA = UNDEFINED;
6784
		//axis.transB = UNDEFINED;
6785
		//axis.oldTransA = UNDEFINED;
6786
		axis.len = 0;
6787
		//axis.oldMin = UNDEFINED;
6788
		//axis.oldMax = UNDEFINED;
6789
		//axis.oldUserMin = UNDEFINED;
6790
		//axis.oldUserMax = UNDEFINED;
6791
		//axis.oldAxisLength = UNDEFINED;
6792
		axis.minRange = axis.userMinRange = options.minRange || options.maxZoom;
6793
		axis.range = options.range;
6794
		axis.offset = options.offset || 0;
6795
 
6796
 
6797
		// Dictionary for stacks
6798
		axis.stacks = {};
6799
		axis.oldStacks = {};
6800
		axis.stacksTouched = 0;
6801
 
6802
		// Min and max in the data
6803
		//axis.dataMin = UNDEFINED,
6804
		//axis.dataMax = UNDEFINED,
6805
 
6806
		// The axis range
6807
		axis.max = null;
6808
		axis.min = null;
6809
 
6810
		// User set min and max
6811
		//axis.userMin = UNDEFINED,
6812
		//axis.userMax = UNDEFINED,
6813
 
6814
		// Crosshair options
6815
		axis.crosshair = pick(options.crosshair, splat(chart.options.tooltip.crosshairs)[isXAxis ? 0 : 1], false);
6816
		// Run Axis
6817
 
6818
		var eventType,
6819
			events = axis.options.events;
6820
 
6821
		// Register
6822
		if (inArray(axis, chart.axes) === -1) { // don't add it again on Axis.update()
6823
			if (isXAxis && !this.isColorAxis) { // #2713
6824
				chart.axes.splice(chart.xAxis.length, 0, axis);
6825
			} else {
6826
				chart.axes.push(axis);
6827
			}
6828
 
6829
			chart[axis.coll].push(axis);
6830
		}
6831
 
6832
		axis.series = axis.series || []; // populated by Series
6833
 
6834
		// inverted charts have reversed xAxes as default
6835
		if (chart.inverted && isXAxis && axis.reversed === UNDEFINED) {
6836
			axis.reversed = true;
6837
		}
6838
 
6839
		axis.removePlotBand = axis.removePlotBandOrLine;
6840
		axis.removePlotLine = axis.removePlotBandOrLine;
6841
 
6842
 
6843
		// register event listeners
6844
		for (eventType in events) {
6845
			addEvent(axis, eventType, events[eventType]);
6846
		}
6847
 
6848
		// extend logarithmic axis
6849
		if (axis.isLog) {
6850
			axis.val2lin = log2lin;
6851
			axis.lin2val = lin2log;
6852
		}
6853
	},
6854
 
6855
	/**
6856
	 * Merge and set options
6857
	 */
6858
	setOptions: function (userOptions) {
6859
		this.options = merge(
6860
			this.defaultOptions,
6861
			this.isXAxis ? {} : this.defaultYAxisOptions,
6862
			[this.defaultTopAxisOptions, this.defaultRightAxisOptions,
6863
				this.defaultBottomAxisOptions, this.defaultLeftAxisOptions][this.side],
6864
			merge(
6865
				defaultOptions[this.coll], // if set in setOptions (#1053)
6866
				userOptions
6867
			)
6868
		);
6869
	},
6870
 
6871
	/**
6872
	 * The default label formatter. The context is a special config object for the label.
6873
	 */
6874
	defaultLabelFormatter: function () {
6875
		var axis = this.axis,
6876
			value = this.value,
6877
			categories = axis.categories,
6878
			dateTimeLabelFormat = this.dateTimeLabelFormat,
6879
			numericSymbols = defaultOptions.lang.numericSymbols,
6880
			i = numericSymbols && numericSymbols.length,
6881
			multi,
6882
			ret,
6883
			formatOption = axis.options.labels.format,
6884
 
6885
			// make sure the same symbol is added for all labels on a linear axis
6886
			numericSymbolDetector = axis.isLog ? value : axis.tickInterval;
6887
 
6888
		if (formatOption) {
6889
			ret = format(formatOption, this);
6890
 
6891
		} else if (categories) {
6892
			ret = value;
6893
 
6894
		} else if (dateTimeLabelFormat) { // datetime axis
6895
			ret = dateFormat(dateTimeLabelFormat, value);
6896
 
6897
		} else if (i && numericSymbolDetector >= 1000) {
6898
			// Decide whether we should add a numeric symbol like k (thousands) or M (millions).
6899
			// If we are to enable this in tooltip or other places as well, we can move this
6900
			// logic to the numberFormatter and enable it by a parameter.
6901
			while (i-- && ret === UNDEFINED) {
6902
				multi = Math.pow(1000, i + 1);
6903
				if (numericSymbolDetector >= multi && (value * 10) % multi === 0 && numericSymbols[i] !== null) {
6904
					ret = Highcharts.numberFormat(value / multi, -1) + numericSymbols[i];
6905
				}
6906
			}
6907
		}
6908
 
6909
		if (ret === UNDEFINED) {
6910
			if (mathAbs(value) >= 10000) { // add thousands separators
6911
				ret = Highcharts.numberFormat(value, -1);
6912
 
6913
			} else { // small numbers
6914
				ret = Highcharts.numberFormat(value, -1, UNDEFINED, ''); // #2466
6915
			}
6916
		}
6917
 
6918
		return ret;
6919
	},
6920
 
6921
	/**
6922
	 * Get the minimum and maximum for the series of each axis
6923
	 */
6924
	getSeriesExtremes: function () {
6925
		var axis = this,
6926
			chart = axis.chart;
6927
 
6928
		axis.hasVisibleSeries = false;
6929
 
6930
		// Reset properties in case we're redrawing (#3353)
6931
		axis.dataMin = axis.dataMax = axis.threshold = null;
6932
		axis.softThreshold = !axis.isXAxis;
6933
 
6934
		if (axis.buildStacks) {
6935
			axis.buildStacks();
6936
		}
6937
 
6938
		// loop through this axis' series
6939
		each(axis.series, function (series) {
6940
 
6941
			if (series.visible || !chart.options.chart.ignoreHiddenSeries) {
6942
 
6943
				var seriesOptions = series.options,
6944
					xData,
6945
					threshold = seriesOptions.threshold,
6946
					seriesDataMin,
6947
					seriesDataMax;
6948
 
6949
				axis.hasVisibleSeries = true;
6950
 
6951
				// Validate threshold in logarithmic axes
6952
				if (axis.isLog && threshold <= 0) {
6953
					threshold = null;
6954
				}
6955
 
6956
				// Get dataMin and dataMax for X axes
6957
				if (axis.isXAxis) {
6958
					xData = series.xData;
6959
					if (xData.length) {
6960
						axis.dataMin = mathMin(pick(axis.dataMin, xData[0]), arrayMin(xData));
6961
						axis.dataMax = mathMax(pick(axis.dataMax, xData[0]), arrayMax(xData));
6962
					}
6963
 
6964
				// Get dataMin and dataMax for Y axes, as well as handle stacking and processed data
6965
				} else {
6966
 
6967
					// Get this particular series extremes
6968
					series.getExtremes();
6969
					seriesDataMax = series.dataMax;
6970
					seriesDataMin = series.dataMin;
6971
 
6972
					// Get the dataMin and dataMax so far. If percentage is used, the min and max are
6973
					// always 0 and 100. If seriesDataMin and seriesDataMax is null, then series
6974
					// doesn't have active y data, we continue with nulls
6975
					if (defined(seriesDataMin) && defined(seriesDataMax)) {
6976
						axis.dataMin = mathMin(pick(axis.dataMin, seriesDataMin), seriesDataMin);
6977
						axis.dataMax = mathMax(pick(axis.dataMax, seriesDataMax), seriesDataMax);
6978
					}
6979
 
6980
					// Adjust to threshold
6981
					if (defined(threshold)) {
6982
						axis.threshold = threshold;
6983
					}
6984
					// If any series has a hard threshold, it takes precedence
6985
					if (!seriesOptions.softThreshold || axis.isLog) {
6986
						axis.softThreshold = false;
6987
					}
6988
				}
6989
			}
6990
		});
6991
	},
6992
 
6993
	/**
6994
	 * Translate from axis value to pixel position on the chart, or back
6995
	 *
6996
	 */
6997
	translate: function (val, backwards, cvsCoord, old, handleLog, pointPlacement) {
6998
		var axis = this.linkedParent || this, // #1417
6999
			sign = 1,
7000
			cvsOffset = 0,
7001
			localA = old ? axis.oldTransA : axis.transA,
7002
			localMin = old ? axis.oldMin : axis.min,
7003
			returnValue,
7004
			minPixelPadding = axis.minPixelPadding,
7005
			doPostTranslate = (axis.doPostTranslate || (axis.isLog && handleLog)) && axis.lin2val;
7006
 
7007
		if (!localA) {
7008
			localA = axis.transA;
7009
		}
7010
 
7011
		// In vertical axes, the canvas coordinates start from 0 at the top like in
7012
		// SVG.
7013
		if (cvsCoord) {
7014
			sign *= -1; // canvas coordinates inverts the value
7015
			cvsOffset = axis.len;
7016
		}
7017
 
7018
		// Handle reversed axis
7019
		if (axis.reversed) {
7020
			sign *= -1;
7021
			cvsOffset -= sign * (axis.sector || axis.len);
7022
		}
7023
 
7024
		// From pixels to value
7025
		if (backwards) { // reverse translation
7026
 
7027
			val = val * sign + cvsOffset;
7028
			val -= minPixelPadding;
7029
			returnValue = val / localA + localMin; // from chart pixel to value
7030
			if (doPostTranslate) { // log and ordinal axes
7031
				returnValue = axis.lin2val(returnValue);
7032
			}
7033
 
7034
		// From value to pixels
7035
		} else {
7036
			if (doPostTranslate) { // log and ordinal axes
7037
				val = axis.val2lin(val);
7038
			}
7039
			if (pointPlacement === 'between') {
7040
				pointPlacement = 0.5;
7041
			}
7042
			returnValue = sign * (val - localMin) * localA + cvsOffset + (sign * minPixelPadding) +
7043
				(isNumber(pointPlacement) ? localA * pointPlacement * axis.pointRange : 0);
7044
		}
7045
 
7046
		return returnValue;
7047
	},
7048
 
7049
	/**
7050
	 * Utility method to translate an axis value to pixel position.
7051
	 * @param {Number} value A value in terms of axis units
7052
	 * @param {Boolean} paneCoordinates Whether to return the pixel coordinate relative to the chart
7053
	 *        or just the axis/pane itself.
7054
	 */
7055
	toPixels: function (value, paneCoordinates) {
7056
		return this.translate(value, false, !this.horiz, null, true) + (paneCoordinates ? 0 : this.pos);
7057
	},
7058
 
7059
	/*
7060
	 * Utility method to translate a pixel position in to an axis value
7061
	 * @param {Number} pixel The pixel value coordinate
7062
	 * @param {Boolean} paneCoordiantes Whether the input pixel is relative to the chart or just the
7063
	 *        axis/pane itself.
7064
	 */
7065
	toValue: function (pixel, paneCoordinates) {
7066
		return this.translate(pixel - (paneCoordinates ? 0 : this.pos), true, !this.horiz, null, true);
7067
	},
7068
 
7069
	/**
7070
	 * Create the path for a plot line that goes from the given value on
7071
	 * this axis, across the plot to the opposite side
7072
	 * @param {Number} value
7073
	 * @param {Number} lineWidth Used for calculation crisp line
7074
	 * @param {Number] old Use old coordinates (for resizing and rescaling)
7075
	 */
7076
	getPlotLinePath: function (value, lineWidth, old, force, translatedValue) {
7077
		var axis = this,
7078
			chart = axis.chart,
7079
			axisLeft = axis.left,
7080
			axisTop = axis.top,
7081
			x1,
7082
			y1,
7083
			x2,
7084
			y2,
7085
			cHeight = (old && chart.oldChartHeight) || chart.chartHeight,
7086
			cWidth = (old && chart.oldChartWidth) || chart.chartWidth,
7087
			skip,
7088
			transB = axis.transB,
7089
			/**
7090
			 * Check if x is between a and b. If not, either move to a/b or skip,
7091
			 * depending on the force parameter.
7092
			 */
7093
			between = function (x, a, b) {
7094
				if (x < a || x > b) {
7095
					if (force) {
7096
						x = mathMin(mathMax(a, x), b);
7097
					} else {
7098
						skip = true;
7099
					}
7100
				}
7101
				return x;
7102
			};
7103
 
7104
		translatedValue = pick(translatedValue, axis.translate(value, null, null, old));
7105
		x1 = x2 = mathRound(translatedValue + transB);
7106
		y1 = y2 = mathRound(cHeight - translatedValue - transB);
7107
 
7108
		if (isNaN(translatedValue)) { // no min or max
7109
			skip = true;
7110
 
7111
		} else if (axis.horiz) {
7112
			y1 = axisTop;
7113
			y2 = cHeight - axis.bottom;
7114
			x1 = x2 = between(x1, axisLeft, axisLeft + axis.width);
7115
		} else {
7116
			x1 = axisLeft;
7117
			x2 = cWidth - axis.right;
7118
			y1 = y2 = between(y1, axisTop, axisTop + axis.height);
7119
		}
7120
		return skip && !force ?
7121
			null :
7122
			chart.renderer.crispLine([M, x1, y1, L, x2, y2], lineWidth || 1);
7123
	},
7124
 
7125
	/**
7126
	 * Set the tick positions of a linear axis to round values like whole tens or every five.
7127
	 */
7128
	getLinearTickPositions: function (tickInterval, min, max) {
7129
		var pos,
7130
			lastPos,
7131
			roundedMin = correctFloat(mathFloor(min / tickInterval) * tickInterval),
7132
			roundedMax = correctFloat(mathCeil(max / tickInterval) * tickInterval),
7133
			tickPositions = [];
7134
 
7135
		// For single points, add a tick regardless of the relative position (#2662)
7136
		if (min === max && isNumber(min)) {
7137
			return [min];
7138
		}
7139
 
7140
		// Populate the intermediate values
7141
		pos = roundedMin;
7142
		while (pos <= roundedMax) {
7143
 
7144
			// Place the tick on the rounded value
7145
			tickPositions.push(pos);
7146
 
7147
			// Always add the raw tickInterval, not the corrected one.
7148
			pos = correctFloat(pos + tickInterval);
7149
 
7150
			// If the interval is not big enough in the current min - max range to actually increase
7151
			// the loop variable, we need to break out to prevent endless loop. Issue #619
7152
			if (pos === lastPos) {
7153
				break;
7154
			}
7155
 
7156
			// Record the last value
7157
			lastPos = pos;
7158
		}
7159
		return tickPositions;
7160
	},
7161
 
7162
	/**
7163
	 * Return the minor tick positions. For logarithmic axes, reuse the same logic
7164
	 * as for major ticks.
7165
	 */
7166
	getMinorTickPositions: function () {
7167
		var axis = this,
7168
			options = axis.options,
7169
			tickPositions = axis.tickPositions,
7170
			minorTickInterval = axis.minorTickInterval,
7171
			minorTickPositions = [],
7172
			pos,
7173
			i,
7174
			pointRangePadding = axis.pointRangePadding || 0,
7175
			min = axis.min - pointRangePadding, // #1498
7176
			max = axis.max + pointRangePadding, // #1498
7177
			range = max - min,
7178
			len;
7179
 
7180
		// If minor ticks get too dense, they are hard to read, and may cause long running script. So we don't draw them.
7181
		if (range && range / minorTickInterval < axis.len / 3) { // #3875
7182
 
7183
			if (axis.isLog) {
7184
				len = tickPositions.length;
7185
				for (i = 1; i < len; i++) {
7186
					minorTickPositions = minorTickPositions.concat(
7187
						axis.getLogTickPositions(minorTickInterval, tickPositions[i - 1], tickPositions[i], true)
7188
					);
7189
				}
7190
			} else if (axis.isDatetimeAxis && options.minorTickInterval === 'auto') { // #1314
7191
				minorTickPositions = minorTickPositions.concat(
7192
					axis.getTimeTicks(
7193
						axis.normalizeTimeTickInterval(minorTickInterval),
7194
						min,
7195
						max,
7196
						options.startOfWeek
7197
					)
7198
				);
7199
			} else {
7200
				for (pos = min + (tickPositions[0] - min) % minorTickInterval; pos <= max; pos += minorTickInterval) {
7201
					minorTickPositions.push(pos);
7202
				}
7203
			}
7204
		}
7205
 
7206
		if(minorTickPositions.length !== 0) { // don't change the extremes, when there is no minor ticks
7207
			axis.trimTicks(minorTickPositions, options.startOnTick, options.endOnTick); // #3652 #3743 #1498
7208
		}
7209
		return minorTickPositions;
7210
	},
7211
 
7212
	/**
7213
	 * Adjust the min and max for the minimum range. Keep in mind that the series data is
7214
	 * not yet processed, so we don't have information on data cropping and grouping, or
7215
	 * updated axis.pointRange or series.pointRange. The data can't be processed until
7216
	 * we have finally established min and max.
7217
	 */
7218
	adjustForMinRange: function () {
7219
		var axis = this,
7220
			options = axis.options,
7221
			min = axis.min,
7222
			max = axis.max,
7223
			zoomOffset,
7224
			spaceAvailable = axis.dataMax - axis.dataMin >= axis.minRange,
7225
			closestDataRange,
7226
			i,
7227
			distance,
7228
			xData,
7229
			loopLength,
7230
			minArgs,
7231
			maxArgs,
7232
			minRange;
7233
 
7234
		// Set the automatic minimum range based on the closest point distance
7235
		if (axis.isXAxis && axis.minRange === UNDEFINED && !axis.isLog) {
7236
 
7237
			if (defined(options.min) || defined(options.max)) {
7238
				axis.minRange = null; // don't do this again
7239
 
7240
			} else {
7241
 
7242
				// Find the closest distance between raw data points, as opposed to
7243
				// closestPointRange that applies to processed points (cropped and grouped)
7244
				each(axis.series, function (series) {
7245
					xData = series.xData;
7246
					loopLength = series.xIncrement ? 1 : xData.length - 1;
7247
					for (i = loopLength; i > 0; i--) {
7248
						distance = xData[i] - xData[i - 1];
7249
						if (closestDataRange === UNDEFINED || distance < closestDataRange) {
7250
							closestDataRange = distance;
7251
						}
7252
					}
7253
				});
7254
				axis.minRange = mathMin(closestDataRange * 5, axis.dataMax - axis.dataMin);
7255
			}
7256
		}
7257
 
7258
		// if minRange is exceeded, adjust
7259
		if (max - min < axis.minRange) {
7260
			minRange = axis.minRange;
7261
			zoomOffset = (minRange - max + min) / 2;
7262
 
7263
			// if min and max options have been set, don't go beyond it
7264
			minArgs = [min - zoomOffset, pick(options.min, min - zoomOffset)];
7265
			if (spaceAvailable) { // if space is available, stay within the data range
7266
				minArgs[2] = axis.dataMin;
7267
			}
7268
			min = arrayMax(minArgs);
7269
 
7270
			maxArgs = [min + minRange, pick(options.max, min + minRange)];
7271
			if (spaceAvailable) { // if space is availabe, stay within the data range
7272
				maxArgs[2] = axis.dataMax;
7273
			}
7274
 
7275
			max = arrayMin(maxArgs);
7276
 
7277
			// now if the max is adjusted, adjust the min back
7278
			if (max - min < minRange) {
7279
				minArgs[0] = max - minRange;
7280
				minArgs[1] = pick(options.min, max - minRange);
7281
				min = arrayMax(minArgs);
7282
			}
7283
		}
7284
 
7285
		// Record modified extremes
7286
		axis.min = min;
7287
		axis.max = max;
7288
	},
7289
 
7290
	/**
7291
	 * Update translation information
7292
	 */
7293
	setAxisTranslation: function (saveOld) {
7294
		var axis = this,
7295
			range = axis.max - axis.min,
7296
			pointRange = axis.axisPointRange || 0,
7297
			closestPointRange,
7298
			minPointOffset = 0,
7299
			pointRangePadding = 0,
7300
			linkedParent = axis.linkedParent,
7301
			ordinalCorrection,
7302
			hasCategories = !!axis.categories,
7303
			transA = axis.transA,
7304
			isXAxis = axis.isXAxis;
7305
 
7306
		// Adjust translation for padding. Y axis with categories need to go through the same (#1784).
7307
		if (isXAxis || hasCategories || pointRange) {
7308
			if (linkedParent) {
7309
				minPointOffset = linkedParent.minPointOffset;
7310
				pointRangePadding = linkedParent.pointRangePadding;
7311
 
7312
			} else {
7313
				each(axis.series, function (series) {
7314
					var seriesPointRange = hasCategories ? 1 : (isXAxis ? series.pointRange : (axis.axisPointRange || 0)), // #2806
7315
						pointPlacement = series.options.pointPlacement,
7316
						seriesClosestPointRange = series.closestPointRange;
7317
 
7318
					pointRange = mathMax(pointRange, seriesPointRange);
7319
 
7320
					if (!axis.single) {
7321
						// minPointOffset is the value padding to the left of the axis in order to make
7322
						// room for points with a pointRange, typically columns. When the pointPlacement option
7323
						// is 'between' or 'on', this padding does not apply.
7324
						minPointOffset = mathMax(
7325
							minPointOffset,
7326
							isString(pointPlacement) ? 0 : seriesPointRange / 2
7327
						);
7328
 
7329
						// Determine the total padding needed to the length of the axis to make room for the
7330
						// pointRange. If the series' pointPlacement is 'on', no padding is added.
7331
						pointRangePadding = mathMax(
7332
							pointRangePadding,
7333
							pointPlacement === 'on' ? 0 : seriesPointRange
7334
						);
7335
					}
7336
 
7337
					// Set the closestPointRange
7338
					if (!series.noSharedTooltip && defined(seriesClosestPointRange)) {
7339
						closestPointRange = defined(closestPointRange) ?
7340
							mathMin(closestPointRange, seriesClosestPointRange) :
7341
							seriesClosestPointRange;
7342
					}
7343
				});
7344
			}
7345
 
7346
			// Record minPointOffset and pointRangePadding
7347
			ordinalCorrection = axis.ordinalSlope && closestPointRange ? axis.ordinalSlope / closestPointRange : 1; // #988, #1853
7348
			axis.minPointOffset = minPointOffset = minPointOffset * ordinalCorrection;
7349
			axis.pointRangePadding = pointRangePadding = pointRangePadding * ordinalCorrection;
7350
 
7351
			// pointRange means the width reserved for each point, like in a column chart
7352
			axis.pointRange = mathMin(pointRange, range);
7353
 
7354
			// closestPointRange means the closest distance between points. In columns
7355
			// it is mostly equal to pointRange, but in lines pointRange is 0 while closestPointRange
7356
			// is some other value
7357
			if (isXAxis) {
7358
				axis.closestPointRange = closestPointRange;
7359
			}
7360
		}
7361
 
7362
		// Secondary values
7363
		if (saveOld) {
7364
			axis.oldTransA = transA;
7365
		}
7366
		axis.translationSlope = axis.transA = transA = axis.len / ((range + pointRangePadding) || 1);
7367
		axis.transB = axis.horiz ? axis.left : axis.bottom; // translation addend
7368
		axis.minPixelPadding = transA * minPointOffset;
7369
	},
7370
 
7371
	minFromRange: function () {
7372
		return this.max - this.range;
7373
	},
7374
 
7375
	/**
7376
	 * Set the tick positions to round values and optionally extend the extremes
7377
	 * to the nearest tick
7378
	 */
7379
	setTickInterval: function (secondPass) {
7380
		var axis = this,
7381
			chart = axis.chart,
7382
			options = axis.options,
7383
			isLog = axis.isLog,
7384
			isDatetimeAxis = axis.isDatetimeAxis,
7385
			isXAxis = axis.isXAxis,
7386
			isLinked = axis.isLinked,
7387
			maxPadding = options.maxPadding,
7388
			minPadding = options.minPadding,
7389
			length,
7390
			linkedParentExtremes,
7391
			tickIntervalOption = options.tickInterval,
7392
			minTickInterval,
7393
			tickPixelIntervalOption = options.tickPixelInterval,
7394
			categories = axis.categories,
7395
			threshold = axis.threshold,
7396
			softThreshold = axis.softThreshold,
7397
			thresholdMin,
7398
			thresholdMax,
7399
			hardMin,
7400
			hardMax;
7401
 
7402
		if (!isDatetimeAxis && !categories && !isLinked) {
7403
			this.getTickAmount();
7404
		}
7405
 
7406
		// Min or max set either by zooming/setExtremes or initial options
7407
		hardMin = pick(axis.userMin, options.min);
7408
		hardMax = pick(axis.userMax, options.max);
7409
 
7410
		// Linked axis gets the extremes from the parent axis
7411
		if (isLinked) {
7412
			axis.linkedParent = chart[axis.coll][options.linkedTo];
7413
			linkedParentExtremes = axis.linkedParent.getExtremes();
7414
			axis.min = pick(linkedParentExtremes.min, linkedParentExtremes.dataMin);
7415
			axis.max = pick(linkedParentExtremes.max, linkedParentExtremes.dataMax);
7416
			if (options.type !== axis.linkedParent.options.type) {
7417
				error(11, 1); // Can't link axes of different type
7418
			}
7419
 
7420
		// Initial min and max from the extreme data values
7421
		} else {
7422
 
7423
			// Adjust to hard threshold
7424
			if (!softThreshold && defined(threshold)) {
7425
				if (axis.dataMin >= threshold) {
7426
					thresholdMin = threshold;
7427
					minPadding = 0;
7428
				} else if (axis.dataMax <= threshold) {
7429
					thresholdMax = threshold;
7430
					maxPadding = 0;
7431
				}
7432
			}
7433
 
7434
			axis.min = pick(hardMin, thresholdMin, axis.dataMin);
7435
			axis.max = pick(hardMax, thresholdMax, axis.dataMax);
7436
 
7437
		}
7438
 
7439
		if (isLog) {
7440
			if (!secondPass && mathMin(axis.min, pick(axis.dataMin, axis.min)) <= 0) { // #978
7441
				error(10, 1); // Can't plot negative values on log axis
7442
			}
7443
			// The correctFloat cures #934, float errors on full tens. But it
7444
			// was too aggressive for #4360 because of conversion back to lin,
7445
			// therefore use precision 15.
7446
			axis.min = correctFloat(log2lin(axis.min), 15);
7447
			axis.max = correctFloat(log2lin(axis.max), 15);
7448
		}
7449
 
7450
		// handle zoomed range
7451
		if (axis.range && defined(axis.max)) {
7452
			axis.userMin = axis.min = hardMin = mathMax(axis.min, axis.minFromRange()); // #618
7453
			axis.userMax = hardMax = axis.max;
7454
 
7455
			axis.range = null;  // don't use it when running setExtremes
7456
		}
7457
 
7458
		// Hook for adjusting this.min and this.max. Used by bubble series.
7459
		if (axis.beforePadding) {
7460
			axis.beforePadding();
7461
		}
7462
 
7463
		// adjust min and max for the minimum range
7464
		axis.adjustForMinRange();
7465
 
7466
		// Pad the values to get clear of the chart's edges. To avoid tickInterval taking the padding
7467
		// into account, we do this after computing tick interval (#1337).
7468
		if (!categories && !axis.axisPointRange && !axis.usePercentage && !isLinked && defined(axis.min) && defined(axis.max)) {
7469
			length = axis.max - axis.min;
7470
			if (length) {
7471
				if (!defined(hardMin) && minPadding) {
7472
					axis.min -= length * minPadding;
7473
				}
7474
				if (!defined(hardMax)  && maxPadding) {
7475
					axis.max += length * maxPadding;
7476
				}
7477
			}
7478
		}
7479
 
7480
		// Stay within floor and ceiling
7481
		if (isNumber(options.floor)) {
7482
			axis.min = mathMax(axis.min, options.floor);
7483
		}
7484
		if (isNumber(options.ceiling)) {
7485
			axis.max = mathMin(axis.max, options.ceiling);
7486
		}
7487
 
7488
		// When the threshold is soft, adjust the extreme value only if
7489
		// the data extreme and the padded extreme land on either side of the threshold. For example,
7490
		// a series of [0, 1, 2, 3] would make the yAxis add a tick for -1 because of the
7491
		// default minPadding and startOnTick options. This is prevented by the softThreshold
7492
		// option.
7493
		if (softThreshold && defined(axis.dataMin)) {
7494
			threshold = threshold || 0;
7495
			if (!defined(hardMin) && axis.min < threshold && axis.dataMin >= threshold) {
7496
				axis.min = threshold;
7497
			} else if (!defined(hardMax) && axis.max > threshold && axis.dataMax <= threshold) {
7498
				axis.max = threshold;
7499
			}
7500
		}
7501
 
7502
 
7503
		// get tickInterval
7504
		if (axis.min === axis.max || axis.min === undefined || axis.max === undefined) {
7505
			axis.tickInterval = 1;
7506
		} else if (isLinked && !tickIntervalOption &&
7507
				tickPixelIntervalOption === axis.linkedParent.options.tickPixelInterval) {
7508
			axis.tickInterval = tickIntervalOption = axis.linkedParent.tickInterval;
7509
		} else {
7510
			axis.tickInterval = pick(
7511
				tickIntervalOption,
7512
				this.tickAmount ? ((axis.max - axis.min) / mathMax(this.tickAmount - 1, 1)) : undefined,
7513
				categories ? // for categoried axis, 1 is default, for linear axis use tickPix
7514
					1 :
7515
					// don't let it be more than the data range
7516
					(axis.max - axis.min) * tickPixelIntervalOption / mathMax(axis.len, tickPixelIntervalOption)
7517
			);
7518
		}
7519
 
7520
		// Now we're finished detecting min and max, crop and group series data. This
7521
		// is in turn needed in order to find tick positions in ordinal axes.
7522
		if (isXAxis && !secondPass) {
7523
			each(axis.series, function (series) {
7524
				series.processData(axis.min !== axis.oldMin || axis.max !== axis.oldMax);
7525
			});
7526
		}
7527
 
7528
		// set the translation factor used in translate function
7529
		axis.setAxisTranslation(true);
7530
 
7531
		// hook for ordinal axes and radial axes
7532
		if (axis.beforeSetTickPositions) {
7533
			axis.beforeSetTickPositions();
7534
		}
7535
 
7536
		// hook for extensions, used in Highstock ordinal axes
7537
		if (axis.postProcessTickInterval) {
7538
			axis.tickInterval = axis.postProcessTickInterval(axis.tickInterval);
7539
		}
7540
 
7541
		// In column-like charts, don't cramp in more ticks than there are points (#1943)
7542
		if (axis.pointRange) {
7543
			axis.tickInterval = mathMax(axis.pointRange, axis.tickInterval);
7544
		}
7545
 
7546
		// Before normalizing the tick interval, handle minimum tick interval. This applies only if tickInterval is not defined.
7547
		minTickInterval = pick(options.minTickInterval, axis.isDatetimeAxis && axis.closestPointRange);
7548
		if (!tickIntervalOption && axis.tickInterval < minTickInterval) {
7549
			axis.tickInterval = minTickInterval;
7550
		}
7551
 
7552
		// for linear axes, get magnitude and normalize the interval
7553
		if (!isDatetimeAxis && !isLog && !tickIntervalOption) {
7554
			axis.tickInterval = normalizeTickInterval(
7555
				axis.tickInterval,
7556
				null,
7557
				getMagnitude(axis.tickInterval),
7558
				// If the tick interval is between 0.5 and 5 and the axis max is in the order of
7559
				// thousands, chances are we are dealing with years. Don't allow decimals. #3363.
7560
				pick(options.allowDecimals, !(axis.tickInterval > 0.5 && axis.tickInterval < 5 && axis.max > 1000 && axis.max < 9999)),
7561
				!!this.tickAmount
7562
			);
7563
		}
7564
 
7565
		// Prevent ticks from getting so close that we can't draw the labels
7566
		if (!this.tickAmount && this.len) { // Color axis with disabled legend has no length
7567
			axis.tickInterval = axis.unsquish();
7568
		}
7569
 
7570
		this.setTickPositions();
7571
	},
7572
 
7573
	/**
7574
	 * Now we have computed the normalized tickInterval, get the tick positions
7575
	 */
7576
	setTickPositions: function () {
7577
 
7578
		var options = this.options,
7579
			tickPositions,
7580
			tickPositionsOption = options.tickPositions,
7581
			tickPositioner = options.tickPositioner,
7582
			startOnTick = options.startOnTick,
7583
			endOnTick = options.endOnTick,
7584
			single;
7585
 
7586
		// Set the tickmarkOffset
7587
		this.tickmarkOffset = (this.categories && options.tickmarkPlacement === 'between' &&
7588
			this.tickInterval === 1) ? 0.5 : 0; // #3202
7589
 
7590
 
7591
		// get minorTickInterval
7592
		this.minorTickInterval = options.minorTickInterval === 'auto' && this.tickInterval ?
7593
			this.tickInterval / 5 : options.minorTickInterval;
7594
 
7595
		// Find the tick positions
7596
		this.tickPositions = tickPositions = tickPositionsOption && tickPositionsOption.slice(); // Work on a copy (#1565)
7597
		if (!tickPositions) {
7598
 
7599
			if (this.isDatetimeAxis) {
7600
				tickPositions = this.getTimeTicks(
7601
					this.normalizeTimeTickInterval(this.tickInterval, options.units),
7602
					this.min,
7603
					this.max,
7604
					options.startOfWeek,
7605
					this.ordinalPositions,
7606
					this.closestPointRange,
7607
					true
7608
				);
7609
			} else if (this.isLog) {
7610
				tickPositions = this.getLogTickPositions(this.tickInterval, this.min, this.max);
7611
			} else {
7612
				tickPositions = this.getLinearTickPositions(this.tickInterval, this.min, this.max);
7613
			}
7614
 
7615
			// Too dense ticks, keep only the first and last (#4477)
7616
			if (tickPositions.length > this.len) {
7617
				tickPositions = [tickPositions[0], tickPositions.pop()];
7618
			}
7619
 
7620
			this.tickPositions = tickPositions;
7621
 
7622
			// Run the tick positioner callback, that allows modifying auto tick positions.
7623
			if (tickPositioner) {
7624
				tickPositioner = tickPositioner.apply(this, [this.min, this.max]);
7625
				if (tickPositioner) {
7626
					this.tickPositions = tickPositions = tickPositioner;
7627
				}
7628
			}
7629
 
7630
		}
7631
 
7632
		if (!this.isLinked) {
7633
 
7634
			// reset min/max or remove extremes based on start/end on tick
7635
			this.trimTicks(tickPositions, startOnTick, endOnTick);
7636
 
7637
			// When there is only one point, or all points have the same value on this axis, then min
7638
			// and max are equal and tickPositions.length is 0 or 1. In this case, add some padding
7639
			// in order to center the point, but leave it with one tick. #1337.
7640
			if (this.min === this.max && defined(this.min) && !this.tickAmount) {
7641
				// Substract half a unit (#2619, #2846, #2515, #3390)
7642
				single = true;
7643
				this.min -= 0.5;
7644
				this.max += 0.5;
7645
			}
7646
			this.single = single;
7647
 
7648
			if (!tickPositionsOption && !tickPositioner) {
7649
				this.adjustTickAmount();
7650
			}
7651
		}
7652
	},
7653
 
7654
	/**
7655
	 * Handle startOnTick and endOnTick by either adapting to padding min/max or rounded min/max
7656
	 */
7657
	trimTicks: function (tickPositions, startOnTick, endOnTick) {
7658
		var roundedMin = tickPositions[0],
7659
			roundedMax = tickPositions[tickPositions.length - 1],
7660
			minPointOffset = this.minPointOffset || 0;
7661
 
7662
		if (startOnTick) {
7663
			this.min = roundedMin;
7664
		} else if (this.min - minPointOffset > roundedMin) {
7665
			tickPositions.shift();
7666
		}
7667
 
7668
		if (endOnTick) {
7669
			this.max = roundedMax;
7670
		} else if (this.max + minPointOffset < roundedMax) {
7671
			tickPositions.pop();
7672
		}
7673
 
7674
		// If no tick are left, set one tick in the middle (#3195)
7675
		if (tickPositions.length === 0 && defined(roundedMin)) {
7676
			tickPositions.push((roundedMax + roundedMin) / 2);
7677
		}
7678
	},
7679
 
7680
	/**
7681
	 * Set the max ticks of either the x and y axis collection
7682
	 */
7683
	getTickAmount: function () {
7684
		var others = {}, // Whether there is another axis to pair with this one
7685
			hasOther,
7686
			options = this.options,
7687
			tickAmount = options.tickAmount,
7688
			tickPixelInterval = options.tickPixelInterval;
7689
 
7690
		if (!defined(options.tickInterval) && this.len < tickPixelInterval && !this.isRadial &&
7691
				!this.isLog && options.startOnTick && options.endOnTick) {
7692
			tickAmount = 2;
7693
		}
7694
 
7695
		if (!tickAmount && this.chart.options.chart.alignTicks !== false && options.alignTicks !== false) {
7696
			// Check if there are multiple axes in the same pane
7697
			each(this.chart[this.coll], function (axis) {
7698
				var options = axis.options,
7699
					horiz = axis.horiz,
7700
					key = [horiz ? options.left : options.top, horiz ? options.width : options.height, options.pane].join(',');
7701
 
7702
				if (axis.series.length) { // #4442
7703
					if (others[key]) {
7704
						hasOther = true; // #4201
7705
					} else {
7706
						others[key] = 1;
7707
					}
7708
				}
7709
			});
7710
 
7711
			if (hasOther) {
7712
				// Add 1 because 4 tick intervals require 5 ticks (including first and last)
7713
				tickAmount = mathCeil(this.len / tickPixelInterval) + 1;
7714
			}
7715
		}
7716
 
7717
		// For tick amounts of 2 and 3, compute five ticks and remove the intermediate ones. This
7718
		// prevents the axis from adding ticks that are too far away from the data extremes.
7719
		if (tickAmount < 4) {
7720
			this.finalTickAmt = tickAmount;
7721
			tickAmount = 5;
7722
		}
7723
 
7724
		this.tickAmount = tickAmount;
7725
	},
7726
 
7727
	/**
7728
	 * When using multiple axes, adjust the number of ticks to match the highest
7729
	 * number of ticks in that group
7730
	 */
7731
	adjustTickAmount: function () {
7732
		var tickInterval = this.tickInterval,
7733
			tickPositions = this.tickPositions,
7734
			tickAmount = this.tickAmount,
7735
			finalTickAmt = this.finalTickAmt,
7736
			currentTickAmount = tickPositions && tickPositions.length,
7737
			i,
7738
			len;
7739
 
7740
		if (currentTickAmount < tickAmount) { // TODO: Check #3411
7741
			while (tickPositions.length < tickAmount) {
7742
				tickPositions.push(correctFloat(
7743
					tickPositions[tickPositions.length - 1] + tickInterval
7744
				));
7745
			}
7746
			this.transA *= (currentTickAmount - 1) / (tickAmount - 1);
7747
			this.max = tickPositions[tickPositions.length - 1];
7748
 
7749
		// We have too many ticks, run second pass to try to reduce ticks
7750
		} else if (currentTickAmount > tickAmount) {
7751
			this.tickInterval *= 2;
7752
			this.setTickPositions();
7753
		}
7754
 
7755
		// The finalTickAmt property is set in getTickAmount
7756
		if (defined(finalTickAmt)) {
7757
			i = len = tickPositions.length;
7758
			while (i--) {
7759
				if (
7760
					(finalTickAmt === 3 && i % 2 === 1) || // Remove every other tick
7761
					(finalTickAmt <= 2 && i > 0 && i < len - 1) // Remove all but first and last
7762
				) {
7763
					tickPositions.splice(i, 1);
7764
				}
7765
			}
7766
			this.finalTickAmt = UNDEFINED;
7767
		}
7768
	},
7769
 
7770
	/**
7771
	 * Set the scale based on data min and max, user set min and max or options
7772
	 *
7773
	 */
7774
	setScale: function () {
7775
		var axis = this,
7776
			isDirtyData,
7777
			isDirtyAxisLength;
7778
 
7779
		axis.oldMin = axis.min;
7780
		axis.oldMax = axis.max;
7781
		axis.oldAxisLength = axis.len;
7782
 
7783
		// set the new axisLength
7784
		axis.setAxisSize();
7785
		//axisLength = horiz ? axisWidth : axisHeight;
7786
		isDirtyAxisLength = axis.len !== axis.oldAxisLength;
7787
 
7788
		// is there new data?
7789
		each(axis.series, function (series) {
7790
			if (series.isDirtyData || series.isDirty ||
7791
					series.xAxis.isDirty) { // when x axis is dirty, we need new data extremes for y as well
7792
				isDirtyData = true;
7793
			}
7794
		});
7795
 
7796
		// do we really need to go through all this?
7797
		if (isDirtyAxisLength || isDirtyData || axis.isLinked || axis.forceRedraw ||
7798
			axis.userMin !== axis.oldUserMin || axis.userMax !== axis.oldUserMax) {
7799
 
7800
			if (axis.resetStacks) {
7801
				axis.resetStacks();
7802
			}
7803
 
7804
			axis.forceRedraw = false;
7805
 
7806
			// get data extremes if needed
7807
			axis.getSeriesExtremes();
7808
 
7809
			// get fixed positions based on tickInterval
7810
			axis.setTickInterval();
7811
 
7812
			// record old values to decide whether a rescale is necessary later on (#540)
7813
			axis.oldUserMin = axis.userMin;
7814
			axis.oldUserMax = axis.userMax;
7815
 
7816
			// Mark as dirty if it is not already set to dirty and extremes have changed. #595.
7817
			if (!axis.isDirty) {
7818
				axis.isDirty = isDirtyAxisLength || axis.min !== axis.oldMin || axis.max !== axis.oldMax;
7819
			}
7820
		} else if (axis.cleanStacks) {
7821
			axis.cleanStacks();
7822
		}
7823
	},
7824
 
7825
	/**
7826
	 * Set the extremes and optionally redraw
7827
	 * @param {Number} newMin
7828
	 * @param {Number} newMax
7829
	 * @param {Boolean} redraw
7830
	 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
7831
	 *    configuration
7832
	 * @param {Object} eventArguments
7833
	 *
7834
	 */
7835
	setExtremes: function (newMin, newMax, redraw, animation, eventArguments) {
7836
		var axis = this,
7837
			chart = axis.chart;
7838
 
7839
		redraw = pick(redraw, true); // defaults to true
7840
 
7841
		each(axis.series, function (serie) {
7842
			delete serie.kdTree;
7843
		});
7844
 
7845
		// Extend the arguments with min and max
7846
		eventArguments = extend(eventArguments, {
7847
			min: newMin,
7848
			max: newMax
7849
		});
7850
 
7851
		// Fire the event
7852
		fireEvent(axis, 'setExtremes', eventArguments, function () { // the default event handler
7853
 
7854
			axis.userMin = newMin;
7855
			axis.userMax = newMax;
7856
			axis.eventArgs = eventArguments;
7857
 
7858
			if (redraw) {
7859
				chart.redraw(animation);
7860
			}
7861
		});
7862
	},
7863
 
7864
	/**
7865
	 * Overridable method for zooming chart. Pulled out in a separate method to allow overriding
7866
	 * in stock charts.
7867
	 */
7868
	zoom: function (newMin, newMax) {
7869
		var dataMin = this.dataMin,
7870
			dataMax = this.dataMax,
7871
			options = this.options,
7872
			min = mathMin(dataMin, pick(options.min, dataMin)),
7873
			max = mathMax(dataMax, pick(options.max, dataMax));
7874
 
7875
		// Prevent pinch zooming out of range. Check for defined is for #1946. #1734.
7876
		if (!this.allowZoomOutside) {
7877
			if (defined(dataMin) && newMin <= min) {
7878
				newMin = min;
7879
			}
7880
			if (defined(dataMax) && newMax >= max) {
7881
				newMax = max;
7882
			}
7883
		}
7884
 
7885
		// In full view, displaying the reset zoom button is not required
7886
		this.displayBtn = newMin !== UNDEFINED || newMax !== UNDEFINED;
7887
 
7888
		// Do it
7889
		this.setExtremes(
7890
			newMin,
7891
			newMax,
7892
			false,
7893
			UNDEFINED,
7894
			{ trigger: 'zoom' }
7895
		);
7896
		return true;
7897
	},
7898
 
7899
	/**
7900
	 * Update the axis metrics
7901
	 */
7902
	setAxisSize: function () {
7903
		var chart = this.chart,
7904
			options = this.options,
7905
			offsetLeft = options.offsetLeft || 0,
7906
			offsetRight = options.offsetRight || 0,
7907
			horiz = this.horiz,
7908
			width = pick(options.width, chart.plotWidth - offsetLeft + offsetRight),
7909
			height = pick(options.height, chart.plotHeight),
7910
			top = pick(options.top, chart.plotTop),
7911
			left = pick(options.left, chart.plotLeft + offsetLeft),
7912
			percentRegex = /%$/;
7913
 
7914
		// Check for percentage based input values
7915
		if (percentRegex.test(height)) {
7916
			height = parseFloat(height) / 100 * chart.plotHeight;
7917
		}
7918
		if (percentRegex.test(top)) {
7919
			top = parseFloat(top) / 100 * chart.plotHeight + chart.plotTop;
7920
		}
7921
 
7922
		// Expose basic values to use in Series object and navigator
7923
		this.left = left;
7924
		this.top = top;
7925
		this.width = width;
7926
		this.height = height;
7927
		this.bottom = chart.chartHeight - height - top;
7928
		this.right = chart.chartWidth - width - left;
7929
 
7930
		// Direction agnostic properties
7931
		this.len = mathMax(horiz ? width : height, 0); // mathMax fixes #905
7932
		this.pos = horiz ? left : top; // distance from SVG origin
7933
	},
7934
 
7935
	/**
7936
	 * Get the actual axis extremes
7937
	 */
7938
	getExtremes: function () {
7939
		var axis = this,
7940
			isLog = axis.isLog;
7941
 
7942
		return {
7943
			min: isLog ? correctFloat(lin2log(axis.min)) : axis.min,
7944
			max: isLog ? correctFloat(lin2log(axis.max)) : axis.max,
7945
			dataMin: axis.dataMin,
7946
			dataMax: axis.dataMax,
7947
			userMin: axis.userMin,
7948
			userMax: axis.userMax
7949
		};
7950
	},
7951
 
7952
	/**
7953
	 * Get the zero plane either based on zero or on the min or max value.
7954
	 * Used in bar and area plots
7955
	 */
7956
	getThreshold: function (threshold) {
7957
		var axis = this,
7958
			isLog = axis.isLog,
7959
			realMin = isLog ? lin2log(axis.min) : axis.min,
7960
			realMax = isLog ? lin2log(axis.max) : axis.max;
7961
 
7962
		// With a threshold of null, make the columns/areas rise from the top or bottom
7963
		// depending on the value, assuming an actual threshold of 0 (#4233).
7964
		if (threshold === null) {
7965
			threshold = realMax < 0 ? realMax : realMin;
7966
		} else if (realMin > threshold) {
7967
			threshold = realMin;
7968
		} else if (realMax < threshold) {
7969
			threshold = realMax;
7970
		}
7971
 
7972
		return axis.translate(threshold, 0, 1, 0, 1);
7973
	},
7974
 
7975
	/**
7976
	 * Compute auto alignment for the axis label based on which side the axis is on
7977
	 * and the given rotation for the label
7978
	 */
7979
	autoLabelAlign: function (rotation) {
7980
		var ret,
7981
			angle = (pick(rotation, 0) - (this.side * 90) + 720) % 360;
7982
 
7983
		if (angle > 15 && angle < 165) {
7984
			ret = 'right';
7985
		} else if (angle > 195 && angle < 345) {
7986
			ret = 'left';
7987
		} else {
7988
			ret = 'center';
7989
		}
7990
		return ret;
7991
	},
7992
 
7993
	/**
7994
	 * Prevent the ticks from getting so close we can't draw the labels. On a horizontal
7995
	 * axis, this is handled by rotating the labels, removing ticks and adding ellipsis.
7996
	 * On a vertical axis remove ticks and add ellipsis.
7997
	 */
7998
	unsquish: function () {
7999
		var chart = this.chart,
8000
			ticks = this.ticks,
8001
			labelOptions = this.options.labels,
8002
			horiz = this.horiz,
8003
			tickInterval = this.tickInterval,
8004
			newTickInterval = tickInterval,
8005
			slotSize = this.len / (((this.categories ? 1 : 0) + this.max - this.min) / tickInterval),
8006
			rotation,
8007
			rotationOption = labelOptions.rotation,
8008
			labelMetrics = chart.renderer.fontMetrics(labelOptions.style.fontSize, ticks[0] && ticks[0].label),
8009
			step,
8010
			bestScore = Number.MAX_VALUE,
8011
			autoRotation,
8012
			// Return the multiple of tickInterval that is needed to avoid collision
8013
			getStep = function (spaceNeeded) {
8014
				var step = spaceNeeded / (slotSize || 1);
8015
				step = step > 1 ? mathCeil(step) : 1;
8016
				return step * tickInterval;
8017
			};
8018
 
8019
		if (horiz) {
8020
			autoRotation = !labelOptions.staggerLines && !labelOptions.step && ( // #3971
8021
				defined(rotationOption) ?
8022
					[rotationOption] :
8023
					slotSize < pick(labelOptions.autoRotationLimit, 80) && labelOptions.autoRotation
8024
			);
8025
 
8026
			if (autoRotation) {
8027
 
8028
				// Loop over the given autoRotation options, and determine which gives the best score. The
8029
				// best score is that with the lowest number of steps and a rotation closest to horizontal.
8030
				each(autoRotation, function (rot) {
8031
					var score;
8032
 
8033
					if (rot === rotationOption || (rot && rot >= -90 && rot <= 90)) { // #3891
8034
 
8035
						step = getStep(mathAbs(labelMetrics.h / mathSin(deg2rad * rot)));
8036
 
8037
						score = step + mathAbs(rot / 360);
8038
 
8039
						if (score < bestScore) {
8040
							bestScore = score;
8041
							rotation = rot;
8042
							newTickInterval = step;
8043
						}
8044
					}
8045
				});
8046
			}
8047
 
8048
		} else if (!labelOptions.step) { // #4411
8049
			newTickInterval = getStep(labelMetrics.h);
8050
		}
8051
 
8052
		this.autoRotation = autoRotation;
8053
		this.labelRotation = pick(rotation, rotationOption);
8054
 
8055
		return newTickInterval;
8056
	},
8057
 
8058
	renderUnsquish: function () {
8059
		var chart = this.chart,
8060
			renderer = chart.renderer,
8061
			tickPositions = this.tickPositions,
8062
			ticks = this.ticks,
8063
			labelOptions = this.options.labels,
8064
			horiz = this.horiz,
8065
			margin = chart.margin,
8066
			slotCount = this.categories ? tickPositions.length : tickPositions.length - 1,
8067
			slotWidth = this.slotWidth = (horiz && !labelOptions.step && !labelOptions.rotation &&
8068
				((this.staggerLines || 1) * chart.plotWidth) / slotCount) ||
8069
				(!horiz && ((margin[3] && (margin[3] - chart.spacing[3])) || chart.chartWidth * 0.33)), // #1580, #1931,
8070
			innerWidth = mathMax(1, mathRound(slotWidth - 2 * (labelOptions.padding || 5))),
8071
			attr = {},
8072
			labelMetrics = renderer.fontMetrics(labelOptions.style.fontSize, ticks[0] && ticks[0].label),
8073
			textOverflowOption = labelOptions.style.textOverflow,
8074
			css,
8075
			labelLength = 0,
8076
			label,
8077
			i,
8078
			pos;
8079
 
8080
		// Set rotation option unless it is "auto", like in gauges
8081
		if (!isString(labelOptions.rotation)) {
8082
			attr.rotation = labelOptions.rotation || 0; // #4443
8083
		}
8084
 
8085
		// Handle auto rotation on horizontal axis
8086
		if (this.autoRotation) {
8087
 
8088
			// Get the longest label length
8089
			each(tickPositions, function (tick) {
8090
				tick = ticks[tick];
8091
				if (tick && tick.labelLength > labelLength) {
8092
					labelLength = tick.labelLength;
8093
				}
8094
			});
8095
 
8096
			// Apply rotation only if the label is too wide for the slot, and
8097
			// the label is wider than its height.
8098
			if (labelLength > innerWidth && labelLength > labelMetrics.h) {
8099
				attr.rotation = this.labelRotation;
8100
			} else {
8101
				this.labelRotation = 0;
8102
			}
8103
 
8104
		// Handle word-wrap or ellipsis on vertical axis
8105
		} else if (slotWidth) {
8106
			// For word-wrap or ellipsis
8107
			css = { width: innerWidth + PX };
8108
 
8109
			if (!textOverflowOption) {
8110
				css.textOverflow = 'clip';
8111
 
8112
				// On vertical axis, only allow word wrap if there is room for more lines.
8113
				i = tickPositions.length;
8114
				while (!horiz && i--) {
8115
					pos = tickPositions[i];
8116
					label = ticks[pos].label;
8117
					if (label) {
8118
						// Reset ellipsis in order to get the correct bounding box (#4070)
8119
						if (label.styles.textOverflow === 'ellipsis') {
8120
							label.css({ textOverflow: 'clip' });
8121
						}
8122
						if (label.getBBox().height > this.len / tickPositions.length - (labelMetrics.h - labelMetrics.f)) {
8123
							label.specCss = { textOverflow: 'ellipsis' };
8124
						}
8125
					}
8126
				}
8127
			}
8128
		}
8129
 
8130
 
8131
		// Add ellipsis if the label length is significantly longer than ideal
8132
		if (attr.rotation) {
8133
			css = {
8134
				width: (labelLength > chart.chartHeight * 0.5 ? chart.chartHeight * 0.33 : chart.chartHeight) + PX
8135
			};
8136
			if (!textOverflowOption) {
8137
				css.textOverflow = 'ellipsis';
8138
			}
8139
		}
8140
 
8141
		// Set the explicit or automatic label alignment
8142
		this.labelAlign = attr.align = labelOptions.align || this.autoLabelAlign(this.labelRotation);
8143
 
8144
		// Apply general and specific CSS
8145
		each(tickPositions, function (pos) {
8146
			var tick = ticks[pos],
8147
				label = tick && tick.label;
8148
			if (label) {
8149
				label.attr(attr); // This needs to go before the CSS in old IE (#4502)
8150
				if (css) {
8151
					label.css(merge(css, label.specCss));
8152
				}
8153
				delete label.specCss;
8154
				tick.rotation = attr.rotation;
8155
			}
8156
		});
8157
 
8158
		// TODO: Why not part of getLabelPosition?
8159
		this.tickRotCorr = renderer.rotCorr(labelMetrics.b, this.labelRotation || 0, this.side === 2);
8160
	},
8161
 
8162
	/**
8163
	 * Return true if the axis has associated data
8164
	 */
8165
	hasData: function () {
8166
		return this.hasVisibleSeries || (defined(this.min) && defined(this.max) && !!this.tickPositions);
8167
	},
8168
 
8169
	/**
8170
	 * Render the tick labels to a preliminary position to get their sizes
8171
	 */
8172
	getOffset: function () {
8173
		var axis = this,
8174
			chart = axis.chart,
8175
			renderer = chart.renderer,
8176
			options = axis.options,
8177
			tickPositions = axis.tickPositions,
8178
			ticks = axis.ticks,
8179
			horiz = axis.horiz,
8180
			side = axis.side,
8181
			invertedSide = chart.inverted ? [1, 0, 3, 2][side] : side,
8182
			hasData,
8183
			showAxis,
8184
			titleOffset = 0,
8185
			titleOffsetOption,
8186
			titleMargin = 0,
8187
			axisTitleOptions = options.title,
8188
			labelOptions = options.labels,
8189
			labelOffset = 0, // reset
8190
			labelOffsetPadded,
8191
			axisOffset = chart.axisOffset,
8192
			clipOffset = chart.clipOffset,
8193
			clip,
8194
			directionFactor = [-1, 1, 1, -1][side],
8195
			n,
8196
			axisParent = axis.axisParent, // Used in color axis
8197
			lineHeightCorrection;
8198
 
8199
		// For reuse in Axis.render
8200
		hasData = axis.hasData();
8201
		axis.showAxis = showAxis = hasData || pick(options.showEmpty, true);
8202
 
8203
		// Set/reset staggerLines
8204
		axis.staggerLines = axis.horiz && labelOptions.staggerLines;
8205
 
8206
		// Create the axisGroup and gridGroup elements on first iteration
8207
		if (!axis.axisGroup) {
8208
			axis.gridGroup = renderer.g('grid')
8209
				.attr({ zIndex: options.gridZIndex || 1 })
8210
				.add(axisParent);
8211
			axis.axisGroup = renderer.g('axis')
8212
				.attr({ zIndex: options.zIndex || 2 })
8213
				.add(axisParent);
8214
			axis.labelGroup = renderer.g('axis-labels')
8215
				.attr({ zIndex: labelOptions.zIndex || 7 })
8216
				.addClass(PREFIX + axis.coll.toLowerCase() + '-labels')
8217
				.add(axisParent);
8218
		}
8219
 
8220
		if (hasData || axis.isLinked) {
8221
 
8222
			// Generate ticks
8223
			each(tickPositions, function (pos) {
8224
				if (!ticks[pos]) {
8225
					ticks[pos] = new Tick(axis, pos);
8226
				} else {
8227
					ticks[pos].addLabel(); // update labels depending on tick interval
8228
				}
8229
			});
8230
 
8231
			axis.renderUnsquish();
8232
 
8233
			each(tickPositions, function (pos) {
8234
				// left side must be align: right and right side must have align: left for labels
8235
				if (side === 0 || side === 2 || { 1: 'left', 3: 'right' }[side] === axis.labelAlign) {
8236
 
8237
					// get the highest offset
8238
					labelOffset = mathMax(
8239
						ticks[pos].getLabelSize(),
8240
						labelOffset
8241
					);
8242
				}
8243
			});
8244
 
8245
			if (axis.staggerLines) {
8246
				labelOffset *= axis.staggerLines;
8247
				axis.labelOffset = labelOffset;
8248
			}
8249
 
8250
 
8251
		} else { // doesn't have data
8252
			for (n in ticks) {
8253
				ticks[n].destroy();
8254
				delete ticks[n];
8255
			}
8256
		}
8257
 
8258
		if (axisTitleOptions && axisTitleOptions.text && axisTitleOptions.enabled !== false) {
8259
			if (!axis.axisTitle) {
8260
				axis.axisTitle = renderer.text(
8261
					axisTitleOptions.text,
8262
					0,
8263
					0,
8264
					axisTitleOptions.useHTML
8265
				)
8266
				.attr({
8267
					zIndex: 7,
8268
					rotation: axisTitleOptions.rotation || 0,
8269
					align:
8270
						axisTitleOptions.textAlign ||
8271
						{ low: 'left', middle: 'center', high: 'right' }[axisTitleOptions.align]
8272
				})
8273
				.addClass(PREFIX + this.coll.toLowerCase() + '-title')
8274
				.css(axisTitleOptions.style)
8275
				.add(axis.axisGroup);
8276
				axis.axisTitle.isNew = true;
8277
			}
8278
 
8279
			if (showAxis) {
8280
				titleOffset = axis.axisTitle.getBBox()[horiz ? 'height' : 'width'];
8281
				titleOffsetOption = axisTitleOptions.offset;
8282
				titleMargin = defined(titleOffsetOption) ? 0 : pick(axisTitleOptions.margin, horiz ? 5 : 10);
8283
			}
8284
 
8285
			// hide or show the title depending on whether showEmpty is set
8286
			axis.axisTitle[showAxis ? 'show' : 'hide']();
8287
		}
8288
 
8289
		// handle automatic or user set offset
8290
		axis.offset = directionFactor * pick(options.offset, axisOffset[side]);
8291
 
8292
		axis.tickRotCorr = axis.tickRotCorr || { x: 0, y: 0 }; // polar
8293
		lineHeightCorrection = side === 2 ? axis.tickRotCorr.y : 0;
8294
		labelOffsetPadded = labelOffset + titleMargin +
8295
			(labelOffset && (directionFactor * (horiz ? pick(labelOptions.y, axis.tickRotCorr.y + 8) : labelOptions.x) - lineHeightCorrection));
8296
		axis.axisTitleMargin = pick(titleOffsetOption, labelOffsetPadded);
8297
 
8298
		axisOffset[side] = mathMax(
8299
			axisOffset[side],
8300
			axis.axisTitleMargin + titleOffset + directionFactor * axis.offset,
8301
			labelOffsetPadded // #3027
8302
		);
8303
 
8304
		// Decide the clipping needed to keep the graph inside the plot area and axis lines
8305
		clip = options.offset ? 0 : mathFloor(options.lineWidth / 2) * 2; // #4308, #4371
8306
		clipOffset[invertedSide] = mathMax(clipOffset[invertedSide], clip);
8307
	},
8308
 
8309
	/**
8310
	 * Get the path for the axis line
8311
	 */
8312
	getLinePath: function (lineWidth) {
8313
		var chart = this.chart,
8314
			opposite = this.opposite,
8315
			offset = this.offset,
8316
			horiz = this.horiz,
8317
			lineLeft = this.left + (opposite ? this.width : 0) + offset,
8318
			lineTop = chart.chartHeight - this.bottom - (opposite ? this.height : 0) + offset;
8319
 
8320
		if (opposite) {
8321
			lineWidth *= -1; // crispify the other way - #1480, #1687
8322
		}
8323
 
8324
		return chart.renderer.crispLine([
8325
				M,
8326
				horiz ?
8327
					this.left :
8328
					lineLeft,
8329
				horiz ?
8330
					lineTop :
8331
					this.top,
8332
				L,
8333
				horiz ?
8334
					chart.chartWidth - this.right :
8335
					lineLeft,
8336
				horiz ?
8337
					lineTop :
8338
					chart.chartHeight - this.bottom
8339
			], lineWidth);
8340
	},
8341
 
8342
	/**
8343
	 * Position the title
8344
	 */
8345
	getTitlePosition: function () {
8346
		// compute anchor points for each of the title align options
8347
		var horiz = this.horiz,
8348
			axisLeft = this.left,
8349
			axisTop = this.top,
8350
			axisLength = this.len,
8351
			axisTitleOptions = this.options.title,
8352
			margin = horiz ? axisLeft : axisTop,
8353
			opposite = this.opposite,
8354
			offset = this.offset,
8355
			xOption = axisTitleOptions.x || 0,
8356
			yOption = axisTitleOptions.y || 0,
8357
			fontSize = pInt(axisTitleOptions.style.fontSize || 12),
8358
 
8359
			// the position in the length direction of the axis
8360
			alongAxis = {
8361
				low: margin + (horiz ? 0 : axisLength),
8362
				middle: margin + axisLength / 2,
8363
				high: margin + (horiz ? axisLength : 0)
8364
			}[axisTitleOptions.align],
8365
 
8366
			// the position in the perpendicular direction of the axis
8367
			offAxis = (horiz ? axisTop + this.height : axisLeft) +
8368
				(horiz ? 1 : -1) * // horizontal axis reverses the margin
8369
				(opposite ? -1 : 1) * // so does opposite axes
8370
				this.axisTitleMargin +
8371
				(this.side === 2 ? fontSize : 0);
8372
 
8373
		return {
8374
			x: horiz ?
8375
				alongAxis + xOption :
8376
				offAxis + (opposite ? this.width : 0) + offset + xOption,
8377
			y: horiz ?
8378
				offAxis + yOption - (opposite ? this.height : 0) + offset :
8379
				alongAxis + yOption
8380
		};
8381
	},
8382
 
8383
	/**
8384
	 * Render the axis
8385
	 */
8386
	render: function () {
8387
		var axis = this,
8388
			chart = axis.chart,
8389
			renderer = chart.renderer,
8390
			options = axis.options,
8391
			isLog = axis.isLog,
8392
			isLinked = axis.isLinked,
8393
			tickPositions = axis.tickPositions,
8394
			axisTitle = axis.axisTitle,
8395
			ticks = axis.ticks,
8396
			minorTicks = axis.minorTicks,
8397
			alternateBands = axis.alternateBands,
8398
			stackLabelOptions = options.stackLabels,
8399
			alternateGridColor = options.alternateGridColor,
8400
			tickmarkOffset = axis.tickmarkOffset,
8401
			lineWidth = options.lineWidth,
8402
			linePath,
8403
			hasRendered = chart.hasRendered,
8404
			slideInTicks = hasRendered && defined(axis.oldMin) && !isNaN(axis.oldMin),
8405
			showAxis = axis.showAxis,
8406
			globalAnimation = renderer.globalAnimation,
8407
			from,
8408
			to;
8409
 
8410
		// Reset
8411
		axis.labelEdge.length = 0;
8412
		//axis.justifyToPlot = overflow === 'justify';
8413
		axis.overlap = false;
8414
 
8415
		// Mark all elements inActive before we go over and mark the active ones
8416
		each([ticks, minorTicks, alternateBands], function (coll) {
8417
			var pos;
8418
			for (pos in coll) {
8419
				coll[pos].isActive = false;
8420
			}
8421
		});
8422
 
8423
		// If the series has data draw the ticks. Else only the line and title
8424
		if (axis.hasData() || isLinked) {
8425
 
8426
			// minor ticks
8427
			if (axis.minorTickInterval && !axis.categories) {
8428
				each(axis.getMinorTickPositions(), function (pos) {
8429
					if (!minorTicks[pos]) {
8430
						minorTicks[pos] = new Tick(axis, pos, 'minor');
8431
					}
8432
 
8433
					// render new ticks in old position
8434
					if (slideInTicks && minorTicks[pos].isNew) {
8435
						minorTicks[pos].render(null, true);
8436
					}
8437
 
8438
					minorTicks[pos].render(null, false, 1);
8439
				});
8440
			}
8441
 
8442
			// Major ticks. Pull out the first item and render it last so that
8443
			// we can get the position of the neighbour label. #808.
8444
			if (tickPositions.length) { // #1300
8445
				each(tickPositions, function (pos, i) {
8446
 
8447
					// linked axes need an extra check to find out if
8448
					if (!isLinked || (pos >= axis.min && pos <= axis.max)) {
8449
 
8450
						if (!ticks[pos]) {
8451
							ticks[pos] = new Tick(axis, pos);
8452
						}
8453
 
8454
						// render new ticks in old position
8455
						if (slideInTicks && ticks[pos].isNew) {
8456
							ticks[pos].render(i, true, 0.1);
8457
						}
8458
 
8459
						ticks[pos].render(i);
8460
					}
8461
 
8462
				});
8463
				// In a categorized axis, the tick marks are displayed between labels. So
8464
				// we need to add a tick mark and grid line at the left edge of the X axis.
8465
				if (tickmarkOffset && (axis.min === 0 || axis.single)) {
8466
					if (!ticks[-1]) {
8467
						ticks[-1] = new Tick(axis, -1, null, true);
8468
					}
8469
					ticks[-1].render(-1);
8470
				}
8471
 
8472
			}
8473
 
8474
			// alternate grid color
8475
			if (alternateGridColor) {
8476
				each(tickPositions, function (pos, i) {
8477
					to = tickPositions[i + 1] !== UNDEFINED ? tickPositions[i + 1] + tickmarkOffset : axis.max - tickmarkOffset;
8478
					if (i % 2 === 0 && pos < axis.max && to <= axis.max - tickmarkOffset) { // #2248
8479
						if (!alternateBands[pos]) {
8480
							alternateBands[pos] = new Highcharts.PlotLineOrBand(axis);
8481
						}
8482
						from = pos + tickmarkOffset; // #949
8483
						alternateBands[pos].options = {
8484
							from: isLog ? lin2log(from) : from,
8485
							to: isLog ? lin2log(to) : to,
8486
							color: alternateGridColor
8487
						};
8488
						alternateBands[pos].render();
8489
						alternateBands[pos].isActive = true;
8490
					}
8491
				});
8492
			}
8493
 
8494
			// custom plot lines and bands
8495
			if (!axis._addedPlotLB) { // only first time
8496
				each((options.plotLines || []).concat(options.plotBands || []), function (plotLineOptions) {
8497
					axis.addPlotBandOrLine(plotLineOptions);
8498
				});
8499
				axis._addedPlotLB = true;
8500
			}
8501
 
8502
		} // end if hasData
8503
 
8504
		// Remove inactive ticks
8505
		each([ticks, minorTicks, alternateBands], function (coll) {
8506
			var pos,
8507
				i,
8508
				forDestruction = [],
8509
				delay = globalAnimation ? globalAnimation.duration || 500 : 0,
8510
				destroyInactiveItems = function () {
8511
					i = forDestruction.length;
8512
					while (i--) {
8513
						// When resizing rapidly, the same items may be destroyed in different timeouts,
8514
						// or the may be reactivated
8515
						if (coll[forDestruction[i]] && !coll[forDestruction[i]].isActive) {
8516
							coll[forDestruction[i]].destroy();
8517
							delete coll[forDestruction[i]];
8518
						}
8519
					}
8520
 
8521
				};
8522
 
8523
			for (pos in coll) {
8524
 
8525
				if (!coll[pos].isActive) {
8526
					// Render to zero opacity
8527
					coll[pos].render(pos, false, 0);
8528
					coll[pos].isActive = false;
8529
					forDestruction.push(pos);
8530
				}
8531
			}
8532
 
8533
			// When the objects are finished fading out, destroy them
8534
			if (coll === alternateBands || !chart.hasRendered || !delay) {
8535
				destroyInactiveItems();
8536
			} else if (delay) {
8537
				setTimeout(destroyInactiveItems, delay);
8538
			}
8539
		});
8540
 
8541
		// Static items. As the axis group is cleared on subsequent calls
8542
		// to render, these items are added outside the group.
8543
		// axis line
8544
		if (lineWidth) {
8545
			linePath = axis.getLinePath(lineWidth);
8546
			if (!axis.axisLine) {
8547
				axis.axisLine = renderer.path(linePath)
8548
					.attr({
8549
						stroke: options.lineColor,
8550
						'stroke-width': lineWidth,
8551
						zIndex: 7
8552
					})
8553
					.add(axis.axisGroup);
8554
			} else {
8555
				axis.axisLine.animate({ d: linePath });
8556
			}
8557
 
8558
			// show or hide the line depending on options.showEmpty
8559
			axis.axisLine[showAxis ? 'show' : 'hide']();
8560
		}
8561
 
8562
		if (axisTitle && showAxis) {
8563
 
8564
			axisTitle[axisTitle.isNew ? 'attr' : 'animate'](
8565
				axis.getTitlePosition()
8566
			);
8567
			axisTitle.isNew = false;
8568
		}
8569
 
8570
		// Stacked totals:
8571
		if (stackLabelOptions && stackLabelOptions.enabled) {
8572
			axis.renderStackTotals();
8573
		}
8574
		// End stacked totals
8575
 
8576
		axis.isDirty = false;
8577
	},
8578
 
8579
	/**
8580
	 * Redraw the axis to reflect changes in the data or axis extremes
8581
	 */
8582
	redraw: function () {
8583
 
8584
		if (this.visible) {
8585
			// render the axis
8586
			this.render();
8587
 
8588
			// move plot lines and bands
8589
			each(this.plotLinesAndBands, function (plotLine) {
8590
				plotLine.render();
8591
			});
8592
		}
8593
 
8594
		// mark associated series as dirty and ready for redraw
8595
		each(this.series, function (series) {
8596
			series.isDirty = true;
8597
		});
8598
 
8599
	},
8600
 
8601
	/**
8602
	 * Destroys an Axis instance.
8603
	 */
8604
	destroy: function (keepEvents) {
8605
		var axis = this,
8606
			stacks = axis.stacks,
8607
			stackKey,
8608
			plotLinesAndBands = axis.plotLinesAndBands,
8609
			i;
8610
 
8611
		// Remove the events
8612
		if (!keepEvents) {
8613
			removeEvent(axis);
8614
		}
8615
 
8616
		// Destroy each stack total
8617
		for (stackKey in stacks) {
8618
			destroyObjectProperties(stacks[stackKey]);
8619
 
8620
			stacks[stackKey] = null;
8621
		}
8622
 
8623
		// Destroy collections
8624
		each([axis.ticks, axis.minorTicks, axis.alternateBands], function (coll) {
8625
			destroyObjectProperties(coll);
8626
		});
8627
		i = plotLinesAndBands.length;
8628
		while (i--) { // #1975
8629
			plotLinesAndBands[i].destroy();
8630
		}
8631
 
8632
		// Destroy local variables
8633
		each(['stackTotalGroup', 'axisLine', 'axisTitle', 'axisGroup', 'cross', 'gridGroup', 'labelGroup'], function (prop) {
8634
			if (axis[prop]) {
8635
				axis[prop] = axis[prop].destroy();
8636
			}
8637
		});
8638
 
8639
		// Destroy crosshair
8640
		if (this.cross) {
8641
			this.cross.destroy();
8642
		}
8643
	},
8644
 
8645
	/**
8646
	 * Draw the crosshair
8647
	 */
8648
	drawCrosshair: function (e, point) { // docs: Missing docs for Axis.crosshair. Also for properties.
8649
 
8650
		var path,
8651
			options = this.crosshair,
8652
			animation = options.animation,
8653
			pos,
8654
			attribs,
8655
			categorized;
8656
 
8657
		if (
8658
			// Disabled in options
8659
			!this.crosshair ||
8660
			// Snap
8661
			((defined(point) || !pick(this.crosshair.snap, true)) === false) ||
8662
			// Not on this axis (#4095, #2888)
8663
			(point && point.series && point.series[this.coll] !== this)
8664
		) {
8665
			this.hideCrosshair();
8666
 
8667
		} else {
8668
 
8669
			// Get the path
8670
			if (!pick(options.snap, true)) {
8671
				pos = (this.horiz ? e.chartX - this.pos : this.len - e.chartY + this.pos);
8672
			} else if (defined(point)) {
8673
				/*jslint eqeq: true*/
8674
				pos = this.isXAxis ? point.plotX : this.len - point.plotY; // #3834
8675
				/*jslint eqeq: false*/
8676
			}
8677
 
8678
			if (this.isRadial) {
8679
				path = this.getPlotLinePath(this.isXAxis ? point.x : pick(point.stackY, point.y)) || null; // #3189
8680
			} else {
8681
				path = this.getPlotLinePath(null, null, null, null, pos) || null; // #3189
8682
			}
8683
 
8684
			if (path === null) {
8685
				this.hideCrosshair();
8686
				return;
8687
			}
8688
 
8689
			// Draw the cross
8690
			if (this.cross) {
8691
				this.cross
8692
					.attr({ visibility: VISIBLE })[animation ? 'animate' : 'attr']({ d: path }, animation);
8693
			} else {
8694
				categorized = this.categories && !this.isRadial;
8695
				attribs = {
8696
					'stroke-width': options.width || (categorized ? this.transA : 1),
8697
					stroke: options.color || (categorized ? 'rgba(155,200,255,0.2)' : '#C0C0C0'),
8698
					zIndex: options.zIndex || 2
8699
				};
8700
				if (options.dashStyle) {
8701
					attribs.dashstyle = options.dashStyle;
8702
				}
8703
				this.cross = this.chart.renderer.path(path).attr(attribs).add();
8704
			}
8705
 
8706
		}
8707
 
8708
	},
8709
 
8710
	/**
8711
	 *	Hide the crosshair.
8712
	 */
8713
	hideCrosshair: function () {
8714
		if (this.cross) {
8715
			this.cross.hide();
8716
		}
8717
	}
8718
}; // end Axis
8719
 
8720
extend(Axis.prototype, AxisPlotLineOrBandExtension);
8721
 
8722
/**
8723
 * Set the tick positions to a time unit that makes sense, for example
8724
 * on the first of each month or on every Monday. Return an array
8725
 * with the time positions. Used in datetime axes as well as for grouping
8726
 * data on a datetime axis.
8727
 *
8728
 * @param {Object} normalizedInterval The interval in axis values (ms) and the count
8729
 * @param {Number} min The minimum in axis values
8730
 * @param {Number} max The maximum in axis values
8731
 * @param {Number} startOfWeek
8732
 */
8733
Axis.prototype.getTimeTicks = function (normalizedInterval, min, max, startOfWeek) {
8734
	var tickPositions = [],
8735
		i,
8736
		higherRanks = {},
8737
		useUTC = defaultOptions.global.useUTC,
8738
		minYear, // used in months and years as a basis for Date.UTC()
8739
		minDate = new Date(min - getTZOffset(min)),
8740
		interval = normalizedInterval.unitRange,
8741
		count = normalizedInterval.count;
8742
 
8743
	if (defined(min)) { // #1300
8744
		minDate[setMilliseconds](interval >= timeUnits.second ? 0 : // #3935
8745
			count * mathFloor(minDate.getMilliseconds() / count)); // #3652, #3654
8746
 
8747
		if (interval >= timeUnits.second) { // second
8748
			minDate[setSeconds](interval >= timeUnits.minute ? 0 : // #3935
8749
				count * mathFloor(minDate.getSeconds() / count));
8750
		}
8751
 
8752
		if (interval >= timeUnits.minute) { // minute
8753
			minDate[setMinutes](interval >= timeUnits.hour ? 0 :
8754
				count * mathFloor(minDate[getMinutes]() / count));
8755
		}
8756
 
8757
		if (interval >= timeUnits.hour) { // hour
8758
			minDate[setHours](interval >= timeUnits.day ? 0 :
8759
				count * mathFloor(minDate[getHours]() / count));
8760
		}
8761
 
8762
		if (interval >= timeUnits.day) { // day
8763
			minDate[setDate](interval >= timeUnits.month ? 1 :
8764
				count * mathFloor(minDate[getDate]() / count));
8765
		}
8766
 
8767
		if (interval >= timeUnits.month) { // month
8768
			minDate[setMonth](interval >= timeUnits.year ? 0 :
8769
				count * mathFloor(minDate[getMonth]() / count));
8770
			minYear = minDate[getFullYear]();
8771
		}
8772
 
8773
		if (interval >= timeUnits.year) { // year
8774
			minYear -= minYear % count;
8775
			minDate[setFullYear](minYear);
8776
		}
8777
 
8778
		// week is a special case that runs outside the hierarchy
8779
		if (interval === timeUnits.week) {
8780
			// get start of current week, independent of count
8781
			minDate[setDate](minDate[getDate]() - minDate[getDay]() +
8782
				pick(startOfWeek, 1));
8783
		}
8784
 
8785
 
8786
		// get tick positions
8787
		i = 1;
8788
		if (timezoneOffset || getTimezoneOffset) {
8789
			minDate = minDate.getTime();
8790
			minDate = new Date(minDate + getTZOffset(minDate));
8791
		}
8792
		minYear = minDate[getFullYear]();
8793
		var time = minDate.getTime(),
8794
			minMonth = minDate[getMonth](),
8795
			minDateDate = minDate[getDate](),
8796
			localTimezoneOffset = (timeUnits.day +
8797
					(useUTC ? getTZOffset(minDate) : minDate.getTimezoneOffset() * 60 * 1000)
8798
				) % timeUnits.day; // #950, #3359
8799
 
8800
		// iterate and add tick positions at appropriate values
8801
		while (time < max) {
8802
			tickPositions.push(time);
8803
 
8804
			// if the interval is years, use Date.UTC to increase years
8805
			if (interval === timeUnits.year) {
8806
				time = makeTime(minYear + i * count, 0);
8807
 
8808
			// if the interval is months, use Date.UTC to increase months
8809
			} else if (interval === timeUnits.month) {
8810
				time = makeTime(minYear, minMonth + i * count);
8811
 
8812
			// if we're using global time, the interval is not fixed as it jumps
8813
			// one hour at the DST crossover
8814
			} else if (!useUTC && (interval === timeUnits.day || interval === timeUnits.week)) {
8815
				time = makeTime(minYear, minMonth, minDateDate +
8816
					i * count * (interval === timeUnits.day ? 1 : 7));
8817
 
8818
			// else, the interval is fixed and we use simple addition
8819
			} else {
8820
				time += interval * count;
8821
			}
8822
 
8823
			i++;
8824
		}
8825
 
8826
		// push the last time
8827
		tickPositions.push(time);
8828
 
8829
 
8830
		// mark new days if the time is dividible by day (#1649, #1760)
8831
		each(grep(tickPositions, function (time) {
8832
			return interval <= timeUnits.hour && time % timeUnits.day === localTimezoneOffset;
8833
		}), function (time) {
8834
			higherRanks[time] = 'day';
8835
		});
8836
	}
8837
 
8838
 
8839
	// record information on the chosen unit - for dynamic label formatter
8840
	tickPositions.info = extend(normalizedInterval, {
8841
		higherRanks: higherRanks,
8842
		totalRange: interval * count
8843
	});
8844
 
8845
	return tickPositions;
8846
};
8847
 
8848
/**
8849
 * Get a normalized tick interval for dates. Returns a configuration object with
8850
 * unit range (interval), count and name. Used to prepare data for getTimeTicks.
8851
 * Previously this logic was part of getTimeTicks, but as getTimeTicks now runs
8852
 * of segments in stock charts, the normalizing logic was extracted in order to
8853
 * prevent it for running over again for each segment having the same interval.
8854
 * #662, #697.
8855
 */
8856
Axis.prototype.normalizeTimeTickInterval = function (tickInterval, unitsOption) {
8857
	var units = unitsOption || [[
8858
				'millisecond', // unit name
8859
				[1, 2, 5, 10, 20, 25, 50, 100, 200, 500] // allowed multiples
8860
			], [
8861
				'second',
8862
				[1, 2, 5, 10, 15, 30]
8863
			], [
8864
				'minute',
8865
				[1, 2, 5, 10, 15, 30]
8866
			], [
8867
				'hour',
8868
				[1, 2, 3, 4, 6, 8, 12]
8869
			], [
8870
				'day',
8871
				[1, 2]
8872
			], [
8873
				'week',
8874
				[1, 2]
8875
			], [
8876
				'month',
8877
				[1, 2, 3, 4, 6]
8878
			], [
8879
				'year',
8880
				null
8881
			]],
8882
		unit = units[units.length - 1], // default unit is years
8883
		interval = timeUnits[unit[0]],
8884
		multiples = unit[1],
8885
		count,
8886
		i;
8887
 
8888
	// loop through the units to find the one that best fits the tickInterval
8889
	for (i = 0; i < units.length; i++) {
8890
		unit = units[i];
8891
		interval = timeUnits[unit[0]];
8892
		multiples = unit[1];
8893
 
8894
 
8895
		if (units[i + 1]) {
8896
			// lessThan is in the middle between the highest multiple and the next unit.
8897
			var lessThan = (interval * multiples[multiples.length - 1] +
8898
						timeUnits[units[i + 1][0]]) / 2;
8899
 
8900
			// break and keep the current unit
8901
			if (tickInterval <= lessThan) {
8902
				break;
8903
			}
8904
		}
8905
	}
8906
 
8907
	// prevent 2.5 years intervals, though 25, 250 etc. are allowed
8908
	if (interval === timeUnits.year && tickInterval < 5 * interval) {
8909
		multiples = [1, 2, 5];
8910
	}
8911
 
8912
	// get the count
8913
	count = normalizeTickInterval(
8914
		tickInterval / interval,
8915
		multiples,
8916
		unit[0] === 'year' ? mathMax(getMagnitude(tickInterval / interval), 1) : 1 // #1913, #2360
8917
	);
8918
 
8919
	return {
8920
		unitRange: interval,
8921
		count: count,
8922
		unitName: unit[0]
8923
	};
8924
};/**
8925
 * Methods defined on the Axis prototype
8926
 */
8927
 
8928
/**
8929
 * Set the tick positions of a logarithmic axis
8930
 */
8931
Axis.prototype.getLogTickPositions = function (interval, min, max, minor) {
8932
	var axis = this,
8933
		options = axis.options,
8934
		axisLength = axis.len,
8935
		// Since we use this method for both major and minor ticks,
8936
		// use a local variable and return the result
8937
		positions = [];
8938
 
8939
	// Reset
8940
	if (!minor) {
8941
		axis._minorAutoInterval = null;
8942
	}
8943
 
8944
	// First case: All ticks fall on whole logarithms: 1, 10, 100 etc.
8945
	if (interval >= 0.5) {
8946
		interval = mathRound(interval);
8947
		positions = axis.getLinearTickPositions(interval, min, max);
8948
 
8949
	// Second case: We need intermediary ticks. For example
8950
	// 1, 2, 4, 6, 8, 10, 20, 40 etc.
8951
	} else if (interval >= 0.08) {
8952
		var roundedMin = mathFloor(min),
8953
			intermediate,
8954
			i,
8955
			j,
8956
			len,
8957
			pos,
8958
			lastPos,
8959
			break2;
8960
 
8961
		if (interval > 0.3) {
8962
			intermediate = [1, 2, 4];
8963
		} else if (interval > 0.15) { // 0.2 equals five minor ticks per 1, 10, 100 etc
8964
			intermediate = [1, 2, 4, 6, 8];
8965
		} else { // 0.1 equals ten minor ticks per 1, 10, 100 etc
8966
			intermediate = [1, 2, 3, 4, 5, 6, 7, 8, 9];
8967
		}
8968
 
8969
		for (i = roundedMin; i < max + 1 && !break2; i++) {
8970
			len = intermediate.length;
8971
			for (j = 0; j < len && !break2; j++) {
8972
				pos = log2lin(lin2log(i) * intermediate[j]);
8973
				if (pos > min && (!minor || lastPos <= max) && lastPos !== UNDEFINED) { // #1670, lastPos is #3113
8974
					positions.push(lastPos);
8975
				}
8976
 
8977
				if (lastPos > max) {
8978
					break2 = true;
8979
				}
8980
				lastPos = pos;
8981
			}
8982
		}
8983
 
8984
	// Third case: We are so deep in between whole logarithmic values that
8985
	// we might as well handle the tick positions like a linear axis. For
8986
	// example 1.01, 1.02, 1.03, 1.04.
8987
	} else {
8988
		var realMin = lin2log(min),
8989
			realMax = lin2log(max),
8990
			tickIntervalOption = options[minor ? 'minorTickInterval' : 'tickInterval'],
8991
			filteredTickIntervalOption = tickIntervalOption === 'auto' ? null : tickIntervalOption,
8992
			tickPixelIntervalOption = options.tickPixelInterval / (minor ? 5 : 1),
8993
			totalPixelLength = minor ? axisLength / axis.tickPositions.length : axisLength;
8994
 
8995
		interval = pick(
8996
			filteredTickIntervalOption,
8997
			axis._minorAutoInterval,
8998
			(realMax - realMin) * tickPixelIntervalOption / (totalPixelLength || 1)
8999
		);
9000
 
9001
		interval = normalizeTickInterval(
9002
			interval,
9003
			null,
9004
			getMagnitude(interval)
9005
		);
9006
 
9007
		positions = map(axis.getLinearTickPositions(
9008
			interval,
9009
			realMin,
9010
			realMax
9011
		), log2lin);
9012
 
9013
		if (!minor) {
9014
			axis._minorAutoInterval = interval / 5;
9015
		}
9016
	}
9017
 
9018
	// Set the axis-level tickInterval variable
9019
	if (!minor) {
9020
		axis.tickInterval = interval;
9021
	}
9022
	return positions;
9023
};/**
9024
 * The tooltip object
9025
 * @param {Object} chart The chart instance
9026
 * @param {Object} options Tooltip options
9027
 */
9028
var Tooltip = Highcharts.Tooltip = function () {
9029
	this.init.apply(this, arguments);
9030
};
9031
 
9032
Tooltip.prototype = {
9033
 
9034
	init: function (chart, options) {
9035
 
9036
		var borderWidth = options.borderWidth,
9037
			style = options.style,
9038
			padding = pInt(style.padding);
9039
 
9040
		// Save the chart and options
9041
		this.chart = chart;
9042
		this.options = options;
9043
 
9044
		// Keep track of the current series
9045
		//this.currentSeries = UNDEFINED;
9046
 
9047
		// List of crosshairs
9048
		this.crosshairs = [];
9049
 
9050
		// Current values of x and y when animating
9051
		this.now = { x: 0, y: 0 };
9052
 
9053
		// The tooltip is initially hidden
9054
		this.isHidden = true;
9055
 
9056
 
9057
		// create the label
9058
		this.label = chart.renderer.label('', 0, 0, options.shape || 'callout', null, null, options.useHTML, null, 'tooltip')
9059
			.attr({
9060
				padding: padding,
9061
				fill: options.backgroundColor,
9062
				'stroke-width': borderWidth,
9063
				r: options.borderRadius,
9064
				zIndex: 8
9065
			})
9066
			.css(style)
9067
			.css({ padding: 0 }) // Remove it from VML, the padding is applied as an attribute instead (#1117)
9068
			.add()
9069
			.attr({ y: -9999 }); // #2301, #2657
9070
 
9071
		// When using canVG the shadow shows up as a gray circle
9072
		// even if the tooltip is hidden.
9073
		if (!useCanVG) {
9074
			this.label.shadow(options.shadow);
9075
		}
9076
 
9077
		// Public property for getting the shared state.
9078
		this.shared = options.shared;
9079
	},
9080
 
9081
	/**
9082
	 * Destroy the tooltip and its elements.
9083
	 */
9084
	destroy: function () {
9085
		// Destroy and clear local variables
9086
		if (this.label) {
9087
			this.label = this.label.destroy();
9088
		}
9089
		clearTimeout(this.hideTimer);
9090
		clearTimeout(this.tooltipTimeout);
9091
	},
9092
 
9093
	/**
9094
	 * Provide a soft movement for the tooltip
9095
	 *
9096
	 * @param {Number} x
9097
	 * @param {Number} y
9098
	 * @private
9099
	 */
9100
	move: function (x, y, anchorX, anchorY) {
9101
		var tooltip = this,
9102
			now = tooltip.now,
9103
			animate = tooltip.options.animation !== false && !tooltip.isHidden &&
9104
				// When we get close to the target position, abort animation and land on the right place (#3056)
9105
				(mathAbs(x - now.x) > 1 || mathAbs(y - now.y) > 1),
9106
			skipAnchor = tooltip.followPointer || tooltip.len > 1;
9107
 
9108
		// Get intermediate values for animation
9109
		extend(now, {
9110
			x: animate ? (2 * now.x + x) / 3 : x,
9111
			y: animate ? (now.y + y) / 2 : y,
9112
			anchorX: skipAnchor ? UNDEFINED : animate ? (2 * now.anchorX + anchorX) / 3 : anchorX,
9113
			anchorY: skipAnchor ? UNDEFINED : animate ? (now.anchorY + anchorY) / 2 : anchorY
9114
		});
9115
 
9116
		// Move to the intermediate value
9117
		tooltip.label.attr(now);
9118
 
9119
 
9120
		// Run on next tick of the mouse tracker
9121
		if (animate) {
9122
 
9123
			// Never allow two timeouts
9124
			clearTimeout(this.tooltipTimeout);
9125
 
9126
			// Set the fixed interval ticking for the smooth tooltip
9127
			this.tooltipTimeout = setTimeout(function () {
9128
				// The interval function may still be running during destroy, so check that the chart is really there before calling.
9129
				if (tooltip) {
9130
					tooltip.move(x, y, anchorX, anchorY);
9131
				}
9132
			}, 32);
9133
 
9134
		}
9135
	},
9136
 
9137
	/**
9138
	 * Hide the tooltip
9139
	 */
9140
	hide: function (delay) {
9141
		var tooltip = this,
9142
			hoverPoints;
9143
 
9144
		clearTimeout(this.hideTimer); // disallow duplicate timers (#1728, #1766)
9145
		if (!this.isHidden) {
9146
			hoverPoints = this.chart.hoverPoints;
9147
 
9148
			this.hideTimer = setTimeout(function () {
9149
				tooltip.label.fadeOut();
9150
				tooltip.isHidden = true;
9151
			}, pick(delay, this.options.hideDelay, 500));
9152
		}
9153
	},
9154
 
9155
	/**
9156
	 * Extendable method to get the anchor position of the tooltip
9157
	 * from a point or set of points
9158
	 */
9159
	getAnchor: function (points, mouseEvent) {
9160
		var ret,
9161
			chart = this.chart,
9162
			inverted = chart.inverted,
9163
			plotTop = chart.plotTop,
9164
			plotLeft = chart.plotLeft,
9165
			plotX = 0,
9166
			plotY = 0,
9167
			yAxis,
9168
			xAxis;
9169
 
9170
		points = splat(points);
9171
 
9172
		// Pie uses a special tooltipPos
9173
		ret = points[0].tooltipPos;
9174
 
9175
		// When tooltip follows mouse, relate the position to the mouse
9176
		if (this.followPointer && mouseEvent) {
9177
			if (mouseEvent.chartX === UNDEFINED) {
9178
				mouseEvent = chart.pointer.normalize(mouseEvent);
9179
			}
9180
			ret = [
9181
				mouseEvent.chartX - chart.plotLeft,
9182
				mouseEvent.chartY - plotTop
9183
			];
9184
		}
9185
		// When shared, use the average position
9186
		if (!ret) {
9187
			each(points, function (point) {
9188
				yAxis = point.series.yAxis;
9189
				xAxis = point.series.xAxis;
9190
				plotX += point.plotX  + (!inverted && xAxis ? xAxis.left - plotLeft : 0);
9191
				plotY += (point.plotLow ? (point.plotLow + point.plotHigh) / 2 : point.plotY) +
9192
					(!inverted && yAxis ? yAxis.top - plotTop : 0); // #1151
9193
			});
9194
 
9195
			plotX /= points.length;
9196
			plotY /= points.length;
9197
 
9198
			ret = [
9199
				inverted ? chart.plotWidth - plotY : plotX,
9200
				this.shared && !inverted && points.length > 1 && mouseEvent ?
9201
					mouseEvent.chartY - plotTop : // place shared tooltip next to the mouse (#424)
9202
					inverted ? chart.plotHeight - plotX : plotY
9203
			];
9204
		}
9205
 
9206
		return map(ret, mathRound);
9207
	},
9208
 
9209
	/**
9210
	 * Place the tooltip in a chart without spilling over
9211
	 * and not covering the point it self.
9212
	 */
9213
	getPosition: function (boxWidth, boxHeight, point) {
9214
 
9215
		var chart = this.chart,
9216
			distance = this.distance,
9217
			ret = {},
9218
			h = point.h || 0, // #4117
9219
			swapped,
9220
			first = ['y', chart.chartHeight, boxHeight, point.plotY + chart.plotTop, chart.plotTop, chart.plotTop + chart.plotHeight],
9221
			second = ['x', chart.chartWidth, boxWidth, point.plotX + chart.plotLeft, chart.plotLeft, chart.plotLeft + chart.plotWidth],
9222
			// The far side is right or bottom
9223
			preferFarSide = pick(point.ttBelow, (chart.inverted && !point.negative) || (!chart.inverted && point.negative)),
9224
			/**
9225
			 * Handle the preferred dimension. When the preferred dimension is tooltip
9226
			 * on top or bottom of the point, it will look for space there.
9227
			 */
9228
			firstDimension = function (dim, outerSize, innerSize, point, min, max) {
9229
				var roomLeft = innerSize < point - distance,
9230
					roomRight = point + distance + innerSize < outerSize,
9231
					alignedLeft = point - distance - innerSize,
9232
					alignedRight = point + distance;
9233
 
9234
				if (preferFarSide && roomRight) {
9235
					ret[dim] = alignedRight;
9236
				} else if (!preferFarSide && roomLeft) {
9237
					ret[dim] = alignedLeft;
9238
				} else if (roomLeft) {
9239
					ret[dim] = mathMin(max - innerSize, alignedLeft - h < 0 ? alignedLeft : alignedLeft - h);
9240
				} else if (roomRight) {
9241
					ret[dim] = mathMax(min, alignedRight + h + innerSize > outerSize ? alignedRight : alignedRight + h);
9242
				} else {
9243
					return false;
9244
				}
9245
			},
9246
			/**
9247
			 * Handle the secondary dimension. If the preferred dimension is tooltip
9248
			 * on top or bottom of the point, the second dimension is to align the tooltip
9249
			 * above the point, trying to align center but allowing left or right
9250
			 * align within the chart box.
9251
			 */
9252
			secondDimension = function (dim, outerSize, innerSize, point) {
9253
				// Too close to the edge, return false and swap dimensions
9254
				if (point < distance || point > outerSize - distance) {
9255
					return false;
9256
 
9257
				// Align left/top
9258
				} else if (point < innerSize / 2) {
9259
					ret[dim] = 1;
9260
				// Align right/bottom
9261
				} else if (point > outerSize - innerSize / 2) {
9262
					ret[dim] = outerSize - innerSize - 2;
9263
				// Align center
9264
				} else {
9265
					ret[dim] = point - innerSize / 2;
9266
				}
9267
			},
9268
			/**
9269
			 * Swap the dimensions
9270
			 */
9271
			swap = function (count) {
9272
				var temp = first;
9273
				first = second;
9274
				second = temp;
9275
				swapped = count;
9276
			},
9277
			run = function () {
9278
				if (firstDimension.apply(0, first) !== false) {
9279
					if (secondDimension.apply(0, second) === false && !swapped) {
9280
						swap(true);
9281
						run();
9282
					}
9283
				} else if (!swapped) {
9284
					swap(true);
9285
					run();
9286
				} else {
9287
					ret.x = ret.y = 0;
9288
				}
9289
			};
9290
 
9291
		// Under these conditions, prefer the tooltip on the side of the point
9292
		if (chart.inverted || this.len > 1) {
9293
			swap();
9294
		}
9295
		run();
9296
 
9297
		return ret;
9298
 
9299
	},
9300
 
9301
	/**
9302
	 * In case no user defined formatter is given, this will be used. Note that the context
9303
	 * here is an object holding point, series, x, y etc.
9304
	 */
9305
	defaultFormatter: function (tooltip) {
9306
		var items = this.points || splat(this),
9307
			s;
9308
 
9309
		// build the header
9310
		s = [tooltip.tooltipFooterHeaderFormatter(items[0])]; //#3397: abstraction to enable formatting of footer and header
9311
 
9312
		// build the values
9313
		s = s.concat(tooltip.bodyFormatter(items));
9314
 
9315
		// footer
9316
		s.push(tooltip.tooltipFooterHeaderFormatter(items[0], true)); //#3397: abstraction to enable formatting of footer and header
9317
 
9318
		return s.join('');
9319
	},
9320
 
9321
	/**
9322
	 * Refresh the tooltip's text and position.
9323
	 * @param {Object} point
9324
	 */
9325
	refresh: function (point, mouseEvent) {
9326
		var tooltip = this,
9327
			chart = tooltip.chart,
9328
			label = tooltip.label,
9329
			options = tooltip.options,
9330
			x,
9331
			y,
9332
			anchor,
9333
			textConfig = {},
9334
			text,
9335
			pointConfig = [],
9336
			formatter = options.formatter || tooltip.defaultFormatter,
9337
			hoverPoints = chart.hoverPoints,
9338
			borderColor,
9339
			shared = tooltip.shared,
9340
			currentSeries;
9341
 
9342
		clearTimeout(this.hideTimer);
9343
 
9344
		// get the reference point coordinates (pie charts use tooltipPos)
9345
		tooltip.followPointer = splat(point)[0].series.tooltipOptions.followPointer;
9346
		anchor = tooltip.getAnchor(point, mouseEvent);
9347
		x = anchor[0];
9348
		y = anchor[1];
9349
 
9350
		// shared tooltip, array is sent over
9351
		if (shared && !(point.series && point.series.noSharedTooltip)) {
9352
 
9353
			// hide previous hoverPoints and set new
9354
 
9355
			chart.hoverPoints = point;
9356
			if (hoverPoints) {
9357
				each(hoverPoints, function (point) {
9358
					point.setState();
9359
				});
9360
			}
9361
 
9362
			each(point, function (item) {
9363
				item.setState(HOVER_STATE);
9364
 
9365
				pointConfig.push(item.getLabelConfig());
9366
			});
9367
 
9368
			textConfig = {
9369
				x: point[0].category,
9370
				y: point[0].y
9371
			};
9372
			textConfig.points = pointConfig;
9373
			this.len = pointConfig.length;
9374
			point = point[0];
9375
 
9376
		// single point tooltip
9377
		} else {
9378
			textConfig = point.getLabelConfig();
9379
		}
9380
		text = formatter.call(textConfig, tooltip);
9381
 
9382
		// register the current series
9383
		currentSeries = point.series;
9384
		this.distance = pick(currentSeries.tooltipOptions.distance, 16);
9385
 
9386
		// update the inner HTML
9387
		if (text === false) {
9388
			this.hide();
9389
		} else {
9390
 
9391
			// show it
9392
			if (tooltip.isHidden) {
9393
				stop(label);
9394
				label.attr('opacity', 1).show();
9395
			}
9396
 
9397
			// update text
9398
			label.attr({
9399
				text: text
9400
			});
9401
 
9402
			// set the stroke color of the box
9403
			borderColor = options.borderColor || point.color || currentSeries.color || '#606060';
9404
			label.attr({
9405
				stroke: borderColor
9406
			});
9407
			tooltip.updatePosition({
9408
				plotX: x,
9409
				plotY: y,
9410
				negative: point.negative,
9411
				ttBelow: point.ttBelow,
9412
				h: anchor[2] || 0
9413
			});
9414
 
9415
			this.isHidden = false;
9416
		}
9417
		fireEvent(chart, 'tooltipRefresh', {
9418
				text: text,
9419
				x: x + chart.plotLeft,
9420
				y: y + chart.plotTop,
9421
				borderColor: borderColor
9422
			});
9423
	},
9424
 
9425
	/**
9426
	 * Find the new position and perform the move
9427
	 */
9428
	updatePosition: function (point) {
9429
		var chart = this.chart,
9430
			label = this.label,
9431
			pos = (this.options.positioner || this.getPosition).call(
9432
				this,
9433
				label.width,
9434
				label.height,
9435
				point
9436
			);
9437
 
9438
		// do the move
9439
		this.move(
9440
			mathRound(pos.x),
9441
			mathRound(pos.y || 0), // can be undefined (#3977)
9442
			point.plotX + chart.plotLeft,
9443
			point.plotY + chart.plotTop
9444
		);
9445
	},
9446
 
9447
	/**
9448
	 * Get the best X date format based on the closest point range on the axis.
9449
	 */
9450
	getXDateFormat: function (point, options, xAxis) {
9451
		var xDateFormat,
9452
			dateTimeLabelFormats = options.dateTimeLabelFormats,
9453
			closestPointRange = xAxis && xAxis.closestPointRange,
9454
			n,
9455
			blank = '01-01 00:00:00.000',
9456
			strpos = {
9457
				millisecond: 15,
9458
				second: 12,
9459
				minute: 9,
9460
				hour: 6,
9461
				day: 3
9462
			},
9463
			date,
9464
			lastN = 'millisecond'; // for sub-millisecond data, #4223
9465
 
9466
		if (closestPointRange) {
9467
			date = dateFormat('%m-%d %H:%M:%S.%L', point.x);
9468
			for (n in timeUnits) {
9469
 
9470
				// If the range is exactly one week and we're looking at a Sunday/Monday, go for the week format
9471
				if (closestPointRange === timeUnits.week && +dateFormat('%w', point.x) === xAxis.options.startOfWeek &&
9472
						date.substr(6) === blank.substr(6)) {
9473
					n = 'week';
9474
					break;
9475
 
9476
				// The first format that is too great for the range
9477
				} else if (timeUnits[n] > closestPointRange) {
9478
					n = lastN;
9479
					break;
9480
 
9481
				// If the point is placed every day at 23:59, we need to show
9482
				// the minutes as well. #2637.
9483
				} else if (strpos[n] && date.substr(strpos[n]) !== blank.substr(strpos[n])) {
9484
					break;
9485
				}
9486
 
9487
				// Weeks are outside the hierarchy, only apply them on Mondays/Sundays like in the first condition
9488
				if (n !== 'week') {
9489
					lastN = n;
9490
				}
9491
			}
9492
 
9493
			if (n) {
9494
				xDateFormat = dateTimeLabelFormats[n];
9495
			}
9496
		} else {
9497
			xDateFormat = dateTimeLabelFormats.day;
9498
		}
9499
 
9500
		return xDateFormat || dateTimeLabelFormats.year; // #2546, 2581
9501
	},
9502
 
9503
	/**
9504
	 * Format the footer/header of the tooltip
9505
	 * #3397: abstraction to enable formatting of footer and header
9506
	 */
9507
	tooltipFooterHeaderFormatter: function (point, isFooter) {
9508
		var footOrHead = isFooter ? 'footer' : 'header',
9509
			series = point.series,
9510
			tooltipOptions = series.tooltipOptions,
9511
			xDateFormat = tooltipOptions.xDateFormat,
9512
			xAxis = series.xAxis,
9513
			isDateTime = xAxis && xAxis.options.type === 'datetime' && isNumber(point.key),
9514
			formatString = tooltipOptions[footOrHead+'Format'];
9515
 
9516
		// Guess the best date format based on the closest point distance (#568, #3418)
9517
		if (isDateTime && !xDateFormat) {
9518
			xDateFormat = this.getXDateFormat(point, tooltipOptions, xAxis);
9519
		}
9520
 
9521
		// Insert the footer date format if any
9522
		if (isDateTime && xDateFormat) {
9523
			formatString = formatString.replace('{point.key}', '{point.key:' + xDateFormat + '}');
9524
		}
9525
 
9526
		return format(formatString, {
9527
			point: point,
9528
			series: series
9529
		});
9530
	},
9531
 
9532
	/**
9533
     * Build the body (lines) of the tooltip by iterating over the items and returning one entry for each item,
9534
     * abstracting this functionality allows to easily overwrite and extend it.
9535
	 */
9536
	bodyFormatter: function (items) {
9537
        return map(items, function (item) {
9538
            var tooltipOptions = item.series.tooltipOptions;
9539
            return (tooltipOptions.pointFormatter || item.point.tooltipFormatter).call(item.point, tooltipOptions.pointFormat);
9540
        });
9541
    }
9542
 
9543
};
9544
 
9545
var hoverChartIndex;
9546
 
9547
// Global flag for touch support
9548
hasTouch = doc.documentElement.ontouchstart !== UNDEFINED;
9549
 
9550
/**
9551
 * The mouse tracker object. All methods starting with "on" are primary DOM event handlers.
9552
 * Subsequent methods should be named differently from what they are doing.
9553
 * @param {Object} chart The Chart instance
9554
 * @param {Object} options The root options object
9555
 */
9556
var Pointer = Highcharts.Pointer = function (chart, options) {
9557
	this.init(chart, options);
9558
};
9559
 
9560
Pointer.prototype = {
9561
	/**
9562
	 * Initialize Pointer
9563
	 */
9564
	init: function (chart, options) {
9565
 
9566
		var chartOptions = options.chart,
9567
			chartEvents = chartOptions.events,
9568
			zoomType = useCanVG ? '' : chartOptions.zoomType,
9569
			inverted = chart.inverted,
9570
			zoomX,
9571
			zoomY;
9572
 
9573
		// Store references
9574
		this.options = options;
9575
		this.chart = chart;
9576
 
9577
		// Zoom status
9578
		this.zoomX = zoomX = /x/.test(zoomType);
9579
		this.zoomY = zoomY = /y/.test(zoomType);
9580
		this.zoomHor = (zoomX && !inverted) || (zoomY && inverted);
9581
		this.zoomVert = (zoomY && !inverted) || (zoomX && inverted);
9582
		this.hasZoom = zoomX || zoomY;
9583
 
9584
		// Do we need to handle click on a touch device?
9585
		this.runChartClick = chartEvents && !!chartEvents.click;
9586
 
9587
		this.pinchDown = [];
9588
		this.lastValidTouch = {};
9589
 
9590
		if (Highcharts.Tooltip && options.tooltip.enabled) {
9591
			chart.tooltip = new Tooltip(chart, options.tooltip);
9592
			this.followTouchMove = pick(options.tooltip.followTouchMove, true);
9593
		}
9594
 
9595
		this.setDOMEvents();
9596
	},
9597
 
9598
	/**
9599
	 * Add crossbrowser support for chartX and chartY
9600
	 * @param {Object} e The event object in standard browsers
9601
	 */
9602
	normalize: function (e, chartPosition) {
9603
		var chartX,
9604
			chartY,
9605
			ePos;
9606
 
9607
		// common IE normalizing
9608
		e = e || window.event;
9609
 
9610
		// Framework specific normalizing (#1165)
9611
		e = washMouseEvent(e);
9612
 
9613
		// More IE normalizing, needs to go after washMouseEvent
9614
		if (!e.target) {
9615
			e.target = e.srcElement;
9616
		}
9617
 
9618
		// iOS (#2757)
9619
		ePos = e.touches ?  (e.touches.length ? e.touches.item(0) : e.changedTouches[0]) : e;
9620
 
9621
		// Get mouse position
9622
		if (!chartPosition) {
9623
			this.chartPosition = chartPosition = offset(this.chart.container);
9624
		}
9625
 
9626
		// chartX and chartY
9627
		if (ePos.pageX === UNDEFINED) { // IE < 9. #886.
9628
			chartX = mathMax(e.x, e.clientX - chartPosition.left); // #2005, #2129: the second case is
9629
				// for IE10 quirks mode within framesets
9630
			chartY = e.y;
9631
		} else {
9632
			chartX = ePos.pageX - chartPosition.left;
9633
			chartY = ePos.pageY - chartPosition.top;
9634
		}
9635
 
9636
		return extend(e, {
9637
			chartX: mathRound(chartX),
9638
			chartY: mathRound(chartY)
9639
		});
9640
	},
9641
 
9642
	/**
9643
	 * Get the click position in terms of axis values.
9644
	 *
9645
	 * @param {Object} e A pointer event
9646
	 */
9647
	getCoordinates: function (e) {
9648
		var coordinates = {
9649
				xAxis: [],
9650
				yAxis: []
9651
			};
9652
 
9653
		each(this.chart.axes, function (axis) {
9654
			coordinates[axis.isXAxis ? 'xAxis' : 'yAxis'].push({
9655
				axis: axis,
9656
				value: axis.toValue(e[axis.horiz ? 'chartX' : 'chartY'])
9657
			});
9658
		});
9659
		return coordinates;
9660
	},
9661
 
9662
	/**
9663
	 * With line type charts with a single tracker, get the point closest to the mouse.
9664
	 * Run Point.onMouseOver and display tooltip for the point or points.
9665
	 */
9666
	runPointActions: function (e) {
9667
 
9668
		var pointer = this,
9669
			chart = pointer.chart,
9670
			series = chart.series,
9671
			tooltip = chart.tooltip,
9672
			shared = tooltip ? tooltip.shared : false,
9673
			followPointer,
9674
			hoverPoint = chart.hoverPoint,
9675
			hoverSeries = chart.hoverSeries,
9676
			i,
9677
			distance = Number.MAX_VALUE, // #4511
9678
			anchor,
9679
			noSharedTooltip,
9680
			stickToHoverSeries,
9681
			directTouch,
9682
			kdpoints = [],
9683
			kdpoint,
9684
			kdpointT;
9685
 
9686
		// For hovering over the empty parts of the plot area (hoverSeries is undefined).
9687
		// If there is one series with point tracking (combo chart), don't go to nearest neighbour.
9688
		if (!shared && !hoverSeries) {
9689
			for (i = 0; i < series.length; i++) {
9690
				if (series[i].directTouch || !series[i].options.stickyTracking) {
9691
					series = [];
9692
				}
9693
			}
9694
		}
9695
 
9696
		// If it has a hoverPoint and that series requires direct touch (like columns, #3899), or we're on
9697
		// a noSharedTooltip series among shared tooltip series (#4546), use the hoverPoint . Otherwise,
9698
		// search the k-d tree.
9699
		stickToHoverSeries = hoverSeries && (shared ? hoverSeries.noSharedTooltip : hoverSeries.directTouch);
9700
		if (stickToHoverSeries && hoverPoint) {
9701
			kdpoint = hoverPoint;
9702
 
9703
		// Handle shared tooltip or cases where a series is not yet hovered
9704
		} else {
9705
			// Find nearest points on all series
9706
			each(series, function (s) {
9707
				// Skip hidden series
9708
				noSharedTooltip = s.noSharedTooltip && shared;
9709
				directTouch = !shared && s.directTouch;
9710
				if (s.visible && !noSharedTooltip && !directTouch && pick(s.options.enableMouseTracking, true)) { // #3821
9711
					kdpointT = s.searchPoint(e, !noSharedTooltip && s.kdDimensions === 1); // #3828
9712
					if (kdpointT) {
9713
						kdpoints.push(kdpointT);
9714
					}
9715
				}
9716
			});
9717
			// Find absolute nearest point
9718
			each(kdpoints, function (p) {
9719
				if (p && typeof p.dist === 'number' && p.dist < distance) {
9720
					distance = p.dist;
9721
					kdpoint = p;
9722
				}
9723
			});
9724
		}
9725
 
9726
		// Refresh tooltip for kdpoint if new hover point or tooltip was hidden // #3926, #4200
9727
		if (kdpoint && (kdpoint !== this.prevKDPoint || (tooltip && tooltip.isHidden))) {
9728
			// Draw tooltip if necessary
9729
			if (shared && !kdpoint.series.noSharedTooltip) {
9730
				i = kdpoints.length;
9731
				while (i--) {
9732
					if (kdpoints[i].clientX !== kdpoint.clientX || kdpoints[i].series.noSharedTooltip) {
9733
						kdpoints.splice(i, 1);
9734
					}
9735
				}
9736
				if (kdpoints.length && tooltip) {
9737
					tooltip.refresh(kdpoints, e);
9738
				}
9739
 
9740
				// Do mouseover on all points (#3919, #3985, #4410)
9741
				each(kdpoints, function (point) {
9742
					point.onMouseOver(e, point !== ((hoverSeries && hoverSeries.directTouch && hoverPoint) || kdpoint));
9743
				});
9744
			} else {
9745
				if (tooltip) {
9746
					tooltip.refresh(kdpoint, e);
9747
				}
9748
				if(!hoverSeries || !hoverSeries.directTouch) { // #4448
9749
					kdpoint.onMouseOver(e);
9750
				}
9751
			}
9752
			this.prevKDPoint = kdpoint;
9753
 
9754
		// Update positions (regardless of kdpoint or hoverPoint)
9755
		} else {
9756
			followPointer = hoverSeries && hoverSeries.tooltipOptions.followPointer;
9757
			if (tooltip && followPointer && !tooltip.isHidden) {
9758
				anchor = tooltip.getAnchor([{}], e);
9759
				tooltip.updatePosition({ plotX: anchor[0], plotY: anchor[1] });
9760
			}
9761
		}
9762
 
9763
		// Start the event listener to pick up the tooltip
9764
		if (tooltip && !pointer._onDocumentMouseMove) {
9765
			pointer._onDocumentMouseMove = function (e) {
9766
				if (charts[hoverChartIndex]) {
9767
					charts[hoverChartIndex].pointer.onDocumentMouseMove(e);
9768
				}
9769
			};
9770
			addEvent(doc, 'mousemove', pointer._onDocumentMouseMove);
9771
		}
9772
 
9773
		// Crosshair
9774
		each(chart.axes, function (axis) {
9775
			axis.drawCrosshair(e, pick(kdpoint, hoverPoint));
9776
		});
9777
 
9778
 
9779
	},
9780
 
9781
 
9782
 
9783
	/**
9784
	 * Reset the tracking by hiding the tooltip, the hover series state and the hover point
9785
	 *
9786
	 * @param allowMove {Boolean} Instead of destroying the tooltip altogether, allow moving it if possible
9787
	 */
9788
	reset: function (allowMove, delay) {
9789
		var pointer = this,
9790
			chart = pointer.chart,
9791
			hoverSeries = chart.hoverSeries,
9792
			hoverPoint = chart.hoverPoint,
9793
			hoverPoints = chart.hoverPoints,
9794
			tooltip = chart.tooltip,
9795
			tooltipPoints = tooltip && tooltip.shared ? hoverPoints : hoverPoint;
9796
 
9797
		// Narrow in allowMove
9798
		allowMove = allowMove && tooltip && tooltipPoints;
9799
 
9800
		// Check if the points have moved outside the plot area, #1003
9801
		if (allowMove  && splat(tooltipPoints)[0].plotX === UNDEFINED) {
9802
			allowMove = false;
9803
		}
9804
		// Just move the tooltip, #349
9805
		if (allowMove) {
9806
			tooltip.refresh(tooltipPoints);
9807
			if (hoverPoint) { // #2500
9808
				hoverPoint.setState(hoverPoint.state, true);
9809
				each(chart.axes, function (axis) {
9810
					if (pick(axis.options.crosshair && axis.options.crosshair.snap, true)) {
9811
						axis.drawCrosshair(null, hoverPoint);
9812
					}  else {
9813
						axis.hideCrosshair();
9814
					}
9815
				});
9816
 
9817
			}
9818
 
9819
		// Full reset
9820
		} else {
9821
 
9822
			if (hoverPoint) {
9823
				hoverPoint.onMouseOut();
9824
			}
9825
 
9826
			if (hoverPoints) {
9827
				each(hoverPoints, function (point) {
9828
					point.setState();
9829
				});
9830
			}
9831
 
9832
			if (hoverSeries) {
9833
				hoverSeries.onMouseOut();
9834
			}
9835
 
9836
			if (tooltip) {
9837
				tooltip.hide(delay);
9838
			}
9839
 
9840
			if (pointer._onDocumentMouseMove) {
9841
				removeEvent(doc, 'mousemove', pointer._onDocumentMouseMove);
9842
				pointer._onDocumentMouseMove = null;
9843
			}
9844
 
9845
			// Remove crosshairs
9846
			each(chart.axes, function (axis) {
9847
				axis.hideCrosshair();
9848
			});
9849
 
9850
			pointer.hoverX = chart.hoverPoints = chart.hoverPoint = null;
9851
 
9852
		}
9853
	},
9854
 
9855
	/**
9856
	 * Scale series groups to a certain scale and translation
9857
	 */
9858
	scaleGroups: function (attribs, clip) {
9859
 
9860
		var chart = this.chart,
9861
			seriesAttribs;
9862
 
9863
		// Scale each series
9864
		each(chart.series, function (series) {
9865
			seriesAttribs = attribs || series.getPlotBox(); // #1701
9866
			if (series.xAxis && series.xAxis.zoomEnabled) {
9867
				series.group.attr(seriesAttribs);
9868
				if (series.markerGroup) {
9869
					series.markerGroup.attr(seriesAttribs);
9870
					series.markerGroup.clip(clip ? chart.clipRect : null);
9871
				}
9872
				if (series.dataLabelsGroup) {
9873
					series.dataLabelsGroup.attr(seriesAttribs);
9874
				}
9875
			}
9876
		});
9877
 
9878
		// Clip
9879
		chart.clipRect.attr(clip || chart.clipBox);
9880
	},
9881
 
9882
	/**
9883
	 * Start a drag operation
9884
	 */
9885
	dragStart: function (e) {
9886
		var chart = this.chart;
9887
 
9888
		// Record the start position
9889
		chart.mouseIsDown = e.type;
9890
		chart.cancelClick = false;
9891
		chart.mouseDownX = this.mouseDownX = e.chartX;
9892
		chart.mouseDownY = this.mouseDownY = e.chartY;
9893
	},
9894
 
9895
	/**
9896
	 * Perform a drag operation in response to a mousemove event while the mouse is down
9897
	 */
9898
	drag: function (e) {
9899
 
9900
		var chart = this.chart,
9901
			chartOptions = chart.options.chart,
9902
			chartX = e.chartX,
9903
			chartY = e.chartY,
9904
			zoomHor = this.zoomHor,
9905
			zoomVert = this.zoomVert,
9906
			plotLeft = chart.plotLeft,
9907
			plotTop = chart.plotTop,
9908
			plotWidth = chart.plotWidth,
9909
			plotHeight = chart.plotHeight,
9910
			clickedInside,
9911
			size,
9912
			selectionMarker = this.selectionMarker,
9913
			mouseDownX = this.mouseDownX,
9914
			mouseDownY = this.mouseDownY,
9915
			panKey = chartOptions.panKey && e[chartOptions.panKey + 'Key'];
9916
 
9917
		// If the device supports both touch and mouse (like IE11), and we are touch-dragging
9918
		// inside the plot area, don't handle the mouse event. #4339.
9919
		if (selectionMarker && selectionMarker.touch) {
9920
			return;
9921
		}
9922
 
9923
		// If the mouse is outside the plot area, adjust to cooordinates
9924
		// inside to prevent the selection marker from going outside
9925
		if (chartX < plotLeft) {
9926
			chartX = plotLeft;
9927
		} else if (chartX > plotLeft + plotWidth) {
9928
			chartX = plotLeft + plotWidth;
9929
		}
9930
 
9931
		if (chartY < plotTop) {
9932
			chartY = plotTop;
9933
		} else if (chartY > plotTop + plotHeight) {
9934
			chartY = plotTop + plotHeight;
9935
		}
9936
 
9937
		// determine if the mouse has moved more than 10px
9938
		this.hasDragged = Math.sqrt(
9939
			Math.pow(mouseDownX - chartX, 2) +
9940
			Math.pow(mouseDownY - chartY, 2)
9941
		);
9942
 
9943
		if (this.hasDragged > 10) {
9944
			clickedInside = chart.isInsidePlot(mouseDownX - plotLeft, mouseDownY - plotTop);
9945
 
9946
			// make a selection
9947
			if (chart.hasCartesianSeries && (this.zoomX || this.zoomY) && clickedInside && !panKey) {
9948
				if (!selectionMarker) {
9949
					this.selectionMarker = selectionMarker = chart.renderer.rect(
9950
						plotLeft,
9951
						plotTop,
9952
						zoomHor ? 1 : plotWidth,
9953
						zoomVert ? 1 : plotHeight,
9954
 
9955
					)
9956
					.attr({
9957
						fill: chartOptions.selectionMarkerFill || 'rgba(69,114,167,0.25)',
9958
						zIndex: 7
9959
					})
9960
					.add();
9961
				}
9962
			}
9963
 
9964
			// adjust the width of the selection marker
9965
			if (selectionMarker && zoomHor) {
9966
				size = chartX - mouseDownX;
9967
				selectionMarker.attr({
9968
					width: mathAbs(size),
9969
					x: (size > 0 ? 0 : size) + mouseDownX
9970
				});
9971
			}
9972
			// adjust the height of the selection marker
9973
			if (selectionMarker && zoomVert) {
9974
				size = chartY - mouseDownY;
9975
				selectionMarker.attr({
9976
					height: mathAbs(size),
9977
					y: (size > 0 ? 0 : size) + mouseDownY
9978
				});
9979
			}
9980
 
9981
			// panning
9982
			if (clickedInside && !selectionMarker && chartOptions.panning) {
9983
				chart.pan(e, chartOptions.panning);
9984
			}
9985
		}
9986
	},
9987
 
9988
	/**
9989
	 * On mouse up or touch end across the entire document, drop the selection.
9990
	 */
9991
	drop: function (e) {
9992
		var pointer = this,
9993
			chart = this.chart,
9994
			hasPinched = this.hasPinched;
9995
 
9996
		if (this.selectionMarker) {
9997
			var selectionData = {
9998
					xAxis: [],
9999
					yAxis: [],
10000
					originalEvent: e.originalEvent || e
10001
				},
10002
				selectionBox = this.selectionMarker,
10003
				selectionLeft = selectionBox.attr ? selectionBox.attr('x') : selectionBox.x,
10004
				selectionTop = selectionBox.attr ? selectionBox.attr('y') : selectionBox.y,
10005
				selectionWidth = selectionBox.attr ? selectionBox.attr('width') : selectionBox.width,
10006
				selectionHeight = selectionBox.attr ? selectionBox.attr('height') : selectionBox.height,
10007
				runZoom;
10008
 
10009
			// a selection has been made
10010
			if (this.hasDragged || hasPinched) {
10011
 
10012
				// record each axis' min and max
10013
				each(chart.axes, function (axis) {
10014
					if (axis.zoomEnabled && defined(axis.min) && (hasPinched || pointer[{ xAxis: 'zoomX', yAxis: 'zoomY' }[axis.coll]])) { // #859, #3569
10015
						var horiz = axis.horiz,
10016
							minPixelPadding = e.type === 'touchend' ? axis.minPixelPadding: 0, // #1207, #3075
10017
							selectionMin = axis.toValue((horiz ? selectionLeft : selectionTop) + minPixelPadding),
10018
							selectionMax = axis.toValue((horiz ? selectionLeft + selectionWidth : selectionTop + selectionHeight) - minPixelPadding);
10019
 
10020
						selectionData[axis.coll].push({
10021
							axis: axis,
10022
							min: mathMin(selectionMin, selectionMax), // for reversed axes
10023
							max: mathMax(selectionMin, selectionMax)
10024
						});
10025
						runZoom = true;
10026
					}
10027
				});
10028
				if (runZoom) {
10029
					fireEvent(chart, 'selection', selectionData, function (args) {
10030
						chart.zoom(extend(args, hasPinched ? { animation: false } : null));
10031
					});
10032
				}
10033
 
10034
			}
10035
			this.selectionMarker = this.selectionMarker.destroy();
10036
 
10037
			// Reset scaling preview
10038
			if (hasPinched) {
10039
				this.scaleGroups();
10040
			}
10041
		}
10042
 
10043
		// Reset all
10044
		if (chart) { // it may be destroyed on mouse up - #877
10045
			css(chart.container, { cursor: chart._cursor });
10046
			chart.cancelClick = this.hasDragged > 10; // #370
10047
			chart.mouseIsDown = this.hasDragged = this.hasPinched = false;
10048
			this.pinchDown = [];
10049
		}
10050
	},
10051
 
10052
	onContainerMouseDown: function (e) {
10053
 
10054
		e = this.normalize(e);
10055
 
10056
		// issue #295, dragging not always working in Firefox
10057
		if (e.preventDefault) {
10058
			e.preventDefault();
10059
		}
10060
 
10061
		this.dragStart(e);
10062
	},
10063
 
10064
 
10065
 
10066
	onDocumentMouseUp: function (e) {
10067
		if (charts[hoverChartIndex]) {
10068
			charts[hoverChartIndex].pointer.drop(e);
10069
		}
10070
	},
10071
 
10072
	/**
10073
	 * Special handler for mouse move that will hide the tooltip when the mouse leaves the plotarea.
10074
	 * Issue #149 workaround. The mouseleave event does not always fire.
10075
	 */
10076
	onDocumentMouseMove: function (e) {
10077
		var chart = this.chart,
10078
			chartPosition = this.chartPosition;
10079
 
10080
		e = this.normalize(e, chartPosition);
10081
 
10082
		// If we're outside, hide the tooltip
10083
		if (chartPosition && !this.inClass(e.target, 'highcharts-tracker') &&
10084
				!chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) {
10085
			this.reset();
10086
		}
10087
	},
10088
 
10089
	/**
10090
	 * When mouse leaves the container, hide the tooltip.
10091
	 */
10092
	onContainerMouseLeave: function () {
10093
		var chart = charts[hoverChartIndex];
10094
		if (chart) {
10095
			chart.pointer.reset();
10096
			chart.pointer.chartPosition = null; // also reset the chart position, used in #149 fix
10097
		}
10098
	},
10099
 
10100
	// The mousemove, touchmove and touchstart event handler
10101
	onContainerMouseMove: function (e) {
10102
 
10103
		var chart = this.chart;
10104
 
10105
		hoverChartIndex = chart.index;
10106
 
10107
		e = this.normalize(e);
10108
		e.returnValue = false; // #2251, #3224
10109
 
10110
		if (chart.mouseIsDown === 'mousedown') {
10111
			this.drag(e);
10112
		}
10113
 
10114
		// Show the tooltip and run mouse over events (#977)
10115
		if ((this.inClass(e.target, 'highcharts-tracker') ||
10116
				chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) && !chart.openMenu) {
10117
			this.runPointActions(e);
10118
		}
10119
	},
10120
 
10121
	/**
10122
	 * Utility to detect whether an element has, or has a parent with, a specific
10123
	 * class name. Used on detection of tracker objects and on deciding whether
10124
	 * hovering the tooltip should cause the active series to mouse out.
10125
	 */
10126
	inClass: function (element, className) {
10127
		var elemClassName;
10128
		while (element) {
10129
			elemClassName = attr(element, 'class');
10130
			if (elemClassName) {
10131
				if (elemClassName.indexOf(className) !== -1) {
10132
					return true;
10133
				} else if (elemClassName.indexOf(PREFIX + 'container') !== -1) {
10134
					return false;
10135
				}
10136
			}
10137
			element = element.parentNode;
10138
		}
10139
	},
10140
 
10141
	onTrackerMouseOut: function (e) {
10142
		var series = this.chart.hoverSeries,
10143
			relatedTarget = e.relatedTarget || e.toElement;
10144
 
10145
		if (series && !series.options.stickyTracking &&
10146
				!this.inClass(relatedTarget, PREFIX + 'tooltip') &&
10147
				!this.inClass(relatedTarget, PREFIX + 'series-' + series.index)) { // #2499, #4465
10148
			series.onMouseOut();
10149
		}
10150
	},
10151
 
10152
	onContainerClick: function (e) {
10153
		var chart = this.chart,
10154
			hoverPoint = chart.hoverPoint,
10155
			plotLeft = chart.plotLeft,
10156
			plotTop = chart.plotTop;
10157
 
10158
		e = this.normalize(e);
10159
		e.originalEvent = e; // #3913
10160
 
10161
		if (!chart.cancelClick) {
10162
 
10163
			// On tracker click, fire the series and point events. #783, #1583
10164
			if (hoverPoint && this.inClass(e.target, PREFIX + 'tracker')) {
10165
 
10166
				// the series click event
10167
				fireEvent(hoverPoint.series, 'click', extend(e, {
10168
					point: hoverPoint
10169
				}));
10170
 
10171
				// the point click event
10172
				if (chart.hoverPoint) { // it may be destroyed (#1844)
10173
					hoverPoint.firePointEvent('click', e);
10174
				}
10175
 
10176
			// When clicking outside a tracker, fire a chart event
10177
			} else {
10178
				extend(e, this.getCoordinates(e));
10179
 
10180
				// fire a click event in the chart
10181
				if (chart.isInsidePlot(e.chartX - plotLeft, e.chartY - plotTop)) {
10182
					fireEvent(chart, 'click', e);
10183
				}
10184
			}
10185
 
10186
 
10187
		}
10188
	},
10189
 
10190
	/**
10191
	 * Set the JS DOM events on the container and document. This method should contain
10192
	 * a one-to-one assignment between methods and their handlers. Any advanced logic should
10193
	 * be moved to the handler reflecting the event's name.
10194
	 */
10195
	setDOMEvents: function () {
10196
 
10197
		var pointer = this,
10198
			container = pointer.chart.container;
10199
 
10200
		container.onmousedown = function (e) {
10201
			pointer.onContainerMouseDown(e);
10202
		};
10203
		container.onmousemove = function (e) {
10204
			pointer.onContainerMouseMove(e);
10205
		};
10206
		container.onclick = function (e) {
10207
			pointer.onContainerClick(e);
10208
		};
10209
		addEvent(container, 'mouseleave', pointer.onContainerMouseLeave);
10210
		if (chartCount === 1) {
10211
			addEvent(doc, 'mouseup', pointer.onDocumentMouseUp);
10212
		}
10213
		if (hasTouch) {
10214
			container.ontouchstart = function (e) {
10215
				pointer.onContainerTouchStart(e);
10216
			};
10217
			container.ontouchmove = function (e) {
10218
				pointer.onContainerTouchMove(e);
10219
			};
10220
			if (chartCount === 1) {
10221
				addEvent(doc, 'touchend', pointer.onDocumentTouchEnd);
10222
			}
10223
		}
10224
 
10225
	},
10226
 
10227
	/**
10228
	 * Destroys the Pointer object and disconnects DOM events.
10229
	 */
10230
	destroy: function () {
10231
		var prop;
10232
 
10233
		removeEvent(this.chart.container, 'mouseleave', this.onContainerMouseLeave);
10234
		if (!chartCount) {
10235
			removeEvent(doc, 'mouseup', this.onDocumentMouseUp);
10236
			removeEvent(doc, 'touchend', this.onDocumentTouchEnd);
10237
		}
10238
 
10239
		// memory and CPU leak
10240
		clearInterval(this.tooltipTimeout);
10241
 
10242
		for (prop in this) {
10243
			this[prop] = null;
10244
		}
10245
	}
10246
};
10247
 
10248
 
10249
/* Support for touch devices */
10250
extend(Highcharts.Pointer.prototype, {
10251
 
10252
	/**
10253
	 * Run translation operations
10254
	 */
10255
	pinchTranslate: function (pinchDown, touches, transform, selectionMarker, clip, lastValidTouch) {
10256
		if (this.zoomHor || this.pinchHor) {
10257
			this.pinchTranslateDirection(true, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch);
10258
		}
10259
		if (this.zoomVert || this.pinchVert) {
10260
			this.pinchTranslateDirection(false, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch);
10261
		}
10262
	},
10263
 
10264
	/**
10265
	 * Run translation operations for each direction (horizontal and vertical) independently
10266
	 */
10267
	pinchTranslateDirection: function (horiz, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch, forcedScale) {
10268
		var chart = this.chart,
10269
			xy = horiz ? 'x' : 'y',
10270
			XY = horiz ? 'X' : 'Y',
10271
			sChartXY = 'chart' + XY,
10272
			wh = horiz ? 'width' : 'height',
10273
			plotLeftTop = chart['plot' + (horiz ? 'Left' : 'Top')],
10274
			selectionWH,
10275
			selectionXY,
10276
			clipXY,
10277
			scale = forcedScale || 1,
10278
			inverted = chart.inverted,
10279
			bounds = chart.bounds[horiz ? 'h' : 'v'],
10280
			singleTouch = pinchDown.length === 1,
10281
			touch0Start = pinchDown[0][sChartXY],
10282
			touch0Now = touches[0][sChartXY],
10283
			touch1Start = !singleTouch && pinchDown[1][sChartXY],
10284
			touch1Now = !singleTouch && touches[1][sChartXY],
10285
			outOfBounds,
10286
			transformScale,
10287
			scaleKey,
10288
			setScale = function () {
10289
				if (!singleTouch && mathAbs(touch0Start - touch1Start) > 20) { // Don't zoom if fingers are too close on this axis
10290
					scale = forcedScale || mathAbs(touch0Now - touch1Now) / mathAbs(touch0Start - touch1Start);
10291
				}
10292
 
10293
				clipXY = ((plotLeftTop - touch0Now) / scale) + touch0Start;
10294
				selectionWH = chart['plot' + (horiz ? 'Width' : 'Height')] / scale;
10295
			};
10296
 
10297
		// Set the scale, first pass
10298
		setScale();
10299
 
10300
		selectionXY = clipXY; // the clip position (x or y) is altered if out of bounds, the selection position is not
10301
 
10302
		// Out of bounds
10303
		if (selectionXY < bounds.min) {
10304
			selectionXY = bounds.min;
10305
			outOfBounds = true;
10306
		} else if (selectionXY + selectionWH > bounds.max) {
10307
			selectionXY = bounds.max - selectionWH;
10308
			outOfBounds = true;
10309
		}
10310
 
10311
		// Is the chart dragged off its bounds, determined by dataMin and dataMax?
10312
		if (outOfBounds) {
10313
 
10314
			// Modify the touchNow position in order to create an elastic drag movement. This indicates
10315
			// to the user that the chart is responsive but can't be dragged further.
10316
			touch0Now -= 0.8 * (touch0Now - lastValidTouch[xy][0]);
10317
			if (!singleTouch) {
10318
				touch1Now -= 0.8 * (touch1Now - lastValidTouch[xy][1]);
10319
			}
10320
 
10321
			// Set the scale, second pass to adapt to the modified touchNow positions
10322
			setScale();
10323
 
10324
		} else {
10325
			lastValidTouch[xy] = [touch0Now, touch1Now];
10326
		}
10327
 
10328
		// Set geometry for clipping, selection and transformation
10329
		if (!inverted) { // TODO: implement clipping for inverted charts
10330
			clip[xy] = clipXY - plotLeftTop;
10331
			clip[wh] = selectionWH;
10332
		}
10333
		scaleKey = inverted ? (horiz ? 'scaleY' : 'scaleX') : 'scale' + XY;
10334
		transformScale = inverted ? 1 / scale : scale;
10335
 
10336
		selectionMarker[wh] = selectionWH;
10337
		selectionMarker[xy] = selectionXY;
10338
		transform[scaleKey] = scale;
10339
		transform['translate' + XY] = (transformScale * plotLeftTop) + (touch0Now - (transformScale * touch0Start));
10340
	},
10341
 
10342
	/**
10343
	 * Handle touch events with two touches
10344
	 */
10345
	pinch: function (e) {
10346
 
10347
		var self = this,
10348
			chart = self.chart,
10349
			pinchDown = self.pinchDown,
10350
			touches = e.touches,
10351
			touchesLength = touches.length,
10352
			lastValidTouch = self.lastValidTouch,
10353
			hasZoom = self.hasZoom,
10354
			selectionMarker = self.selectionMarker,
10355
			transform = {},
10356
			fireClickEvent = touchesLength === 1 && ((self.inClass(e.target, PREFIX + 'tracker') &&
10357
				chart.runTrackerClick) || self.runChartClick),
10358
			clip = {};
10359
 
10360
		// Don't initiate panning until the user has pinched. This prevents us from
10361
		// blocking page scrolling as users scroll down a long page (#4210).
10362
		if (touchesLength > 1) {
10363
			self.initiated = true;
10364
		}
10365
 
10366
		// On touch devices, only proceed to trigger click if a handler is defined
10367
		if (hasZoom && self.initiated && !fireClickEvent) {
10368
			e.preventDefault();
10369
		}
10370
 
10371
		// Normalize each touch
10372
		map(touches, function (e) {
10373
			return self.normalize(e);
10374
		});
10375
 
10376
		// Register the touch start position
10377
		if (e.type === 'touchstart') {
10378
			each(touches, function (e, i) {
10379
				pinchDown[i] = { chartX: e.chartX, chartY: e.chartY };
10380
			});
10381
			lastValidTouch.x = [pinchDown[0].chartX, pinchDown[1] && pinchDown[1].chartX];
10382
			lastValidTouch.y = [pinchDown[0].chartY, pinchDown[1] && pinchDown[1].chartY];
10383
 
10384
			// Identify the data bounds in pixels
10385
			each(chart.axes, function (axis) {
10386
				if (axis.zoomEnabled) {
10387
					var bounds = chart.bounds[axis.horiz ? 'h' : 'v'],
10388
						minPixelPadding = axis.minPixelPadding,
10389
						min = axis.toPixels(pick(axis.options.min, axis.dataMin)),
10390
						max = axis.toPixels(pick(axis.options.max, axis.dataMax)),
10391
						absMin = mathMin(min, max),
10392
						absMax = mathMax(min, max);
10393
 
10394
					// Store the bounds for use in the touchmove handler
10395
					bounds.min = mathMin(axis.pos, absMin - minPixelPadding);
10396
					bounds.max = mathMax(axis.pos + axis.len, absMax + minPixelPadding);
10397
				}
10398
			});
10399
			self.res = true; // reset on next move
10400
 
10401
		// Event type is touchmove, handle panning and pinching
10402
		} else if (pinchDown.length) { // can be 0 when releasing, if touchend fires first
10403
 
10404
 
10405
			// Set the marker
10406
			if (!selectionMarker) {
10407
				self.selectionMarker = selectionMarker = extend({
10408
					destroy: noop,
10409
					touch: true
10410
				}, chart.plotBox);
10411
			}
10412
 
10413
			self.pinchTranslate(pinchDown, touches, transform, selectionMarker, clip, lastValidTouch);
10414
 
10415
			self.hasPinched = hasZoom;
10416
 
10417
			// Scale and translate the groups to provide visual feedback during pinching
10418
			self.scaleGroups(transform, clip);
10419
 
10420
			// Optionally move the tooltip on touchmove
10421
			if (!hasZoom && self.followTouchMove && touchesLength === 1) {
10422
				this.runPointActions(self.normalize(e));
10423
			} else if (self.res) {
10424
				self.res = false;
10425
				this.reset(false, 0);
10426
			}
10427
		}
10428
	},
10429
 
10430
	/**
10431
	 * General touch handler shared by touchstart and touchmove.
10432
	 */
10433
	touch: function (e, start) {
10434
		var chart = this.chart;
10435
 
10436
		hoverChartIndex = chart.index;
10437
 
10438
		if (e.touches.length === 1) {
10439
 
10440
			e = this.normalize(e);
10441
 
10442
			if (chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop) && !chart.openMenu) {
10443
 
10444
				// Run mouse events and display tooltip etc
10445
				if (start) {
10446
					this.runPointActions(e);
10447
				}
10448
 
10449
				this.pinch(e);
10450
 
10451
			} else if (start) {
10452
				// Hide the tooltip on touching outside the plot area (#1203)
10453
				this.reset();
10454
			}
10455
 
10456
		} else if (e.touches.length === 2) {
10457
			this.pinch(e);
10458
		}
10459
	},
10460
 
10461
	onContainerTouchStart: function (e) {
10462
		this.touch(e, true);
10463
	},
10464
 
10465
	onContainerTouchMove: function (e) {
10466
		this.touch(e);
10467
	},
10468
 
10469
	onDocumentTouchEnd: function (e) {
10470
		if (charts[hoverChartIndex]) {
10471
			charts[hoverChartIndex].pointer.drop(e);
10472
		}
10473
	}
10474
 
10475
});
10476
if (win.PointerEvent || win.MSPointerEvent) {
10477
 
10478
	// The touches object keeps track of the points being touched at all times
10479
	var touches = {},
10480
		hasPointerEvent = !!win.PointerEvent,
10481
		getWebkitTouches = function () {
10482
			var key, fake = [];
10483
			fake.item = function (i) { return this[i]; };
10484
			for (key in touches) {
10485
				if (touches.hasOwnProperty(key)) {
10486
					fake.push({
10487
						pageX: touches[key].pageX,
10488
						pageY: touches[key].pageY,
10489
						target: touches[key].target
10490
					});
10491
				}
10492
			}
10493
			return fake;
10494
		},
10495
		translateMSPointer = function (e, method, wktype, callback) {
10496
			var p;
10497
			e = e.originalEvent || e;
10498
			if ((e.pointerType === 'touch' || e.pointerType === e.MSPOINTER_TYPE_TOUCH) && charts[hoverChartIndex]) {
10499
				callback(e);
10500
				p = charts[hoverChartIndex].pointer;
10501
				p[method]({
10502
					type: wktype,
10503
					target: e.currentTarget,
10504
					preventDefault: noop,
10505
					touches: getWebkitTouches()
10506
				});
10507
			}
10508
		};
10509
 
10510
	/**
10511
	 * Extend the Pointer prototype with methods for each event handler and more
10512
	 */
10513
	extend(Pointer.prototype, {
10514
		onContainerPointerDown: function (e) {
10515
			translateMSPointer(e, 'onContainerTouchStart', 'touchstart', function (e) {
10516
				touches[e.pointerId] = { pageX: e.pageX, pageY: e.pageY, target: e.currentTarget };
10517
			});
10518
		},
10519
		onContainerPointerMove: function (e) {
10520
			translateMSPointer(e, 'onContainerTouchMove', 'touchmove', function (e) {
10521
				touches[e.pointerId] = { pageX: e.pageX, pageY: e.pageY };
10522
				if (!touches[e.pointerId].target) {
10523
					touches[e.pointerId].target = e.currentTarget;
10524
				}
10525
			});
10526
		},
10527
		onDocumentPointerUp: function (e) {
10528
			translateMSPointer(e, 'onDocumentTouchEnd', 'touchend', function (e) {
10529
				delete touches[e.pointerId];
10530
			});
10531
		},
10532
 
10533
		/**
10534
		 * Add or remove the MS Pointer specific events
10535
		 */
10536
		batchMSEvents: function (fn) {
10537
			fn(this.chart.container, hasPointerEvent ? 'pointerdown' : 'MSPointerDown', this.onContainerPointerDown);
10538
			fn(this.chart.container, hasPointerEvent ? 'pointermove' : 'MSPointerMove', this.onContainerPointerMove);
10539
			fn(doc, hasPointerEvent ? 'pointerup' : 'MSPointerUp', this.onDocumentPointerUp);
10540
		}
10541
	});
10542
 
10543
	// Disable default IE actions for pinch and such on chart element
10544
	wrap(Pointer.prototype, 'init', function (proceed, chart, options) {
10545
		proceed.call(this, chart, options);
10546
		if (this.hasZoom) { // #4014
10547
			css(chart.container, {
10548
				'-ms-touch-action': NONE,
10549
				'touch-action': NONE
10550
			});
10551
		}
10552
	});
10553
 
10554
	// Add IE specific touch events to chart
10555
	wrap(Pointer.prototype, 'setDOMEvents', function (proceed) {
10556
		proceed.apply(this);
10557
		if (this.hasZoom || this.followTouchMove) {
10558
			this.batchMSEvents(addEvent);
10559
		}
10560
	});
10561
	// Destroy MS events also
10562
	wrap(Pointer.prototype, 'destroy', function (proceed) {
10563
		this.batchMSEvents(removeEvent);
10564
		proceed.call(this);
10565
	});
10566
}
10567
/**
10568
 * The overview of the chart's series
10569
 */
10570
var Legend = Highcharts.Legend = function (chart, options) {
10571
	this.init(chart, options);
10572
};
10573
 
10574
Legend.prototype = {
10575
 
10576
	/**
10577
	 * Initialize the legend
10578
	 */
10579
	init: function (chart, options) {
10580
 
10581
		var legend = this,
10582
			itemStyle = options.itemStyle,
10583
			padding,
10584
			itemMarginTop = options.itemMarginTop || 0;
10585
 
10586
		this.options = options;
10587
 
10588
		if (!options.enabled) {
10589
			return;
10590
		}
10591
 
10592
		legend.itemStyle = itemStyle;
10593
		legend.itemHiddenStyle = merge(itemStyle, options.itemHiddenStyle);
10594
		legend.itemMarginTop = itemMarginTop;
10595
		legend.padding = padding = pick(options.padding, 8);
10596
		legend.initialItemX = padding;
10597
		legend.initialItemY = padding - 5; // 5 is the number of pixels above the text
10598
		legend.maxItemWidth = 0;
10599
		legend.chart = chart;
10600
		legend.itemHeight = 0;
10601
		legend.symbolWidth = pick(options.symbolWidth, 16);
10602
		legend.pages = [];
10603
 
10604
 
10605
		// Render it
10606
		legend.render();
10607
 
10608
		// move checkboxes
10609
		addEvent(legend.chart, 'endResize', function () {
10610
			legend.positionCheckboxes();
10611
		});
10612
 
10613
	},
10614
 
10615
	/**
10616
	 * Set the colors for the legend item
10617
	 * @param {Object} item A Series or Point instance
10618
	 * @param {Object} visible Dimmed or colored
10619
	 */
10620
	colorizeItem: function (item, visible) {
10621
		var legend = this,
10622
			options = legend.options,
10623
			legendItem = item.legendItem,
10624
			legendLine = item.legendLine,
10625
			legendSymbol = item.legendSymbol,
10626
			hiddenColor = legend.itemHiddenStyle.color,
10627
			textColor = visible ? options.itemStyle.color : hiddenColor,
10628
			symbolColor = visible ? (item.legendColor || item.color || '#CCC') : hiddenColor,
10629
			markerOptions = item.options && item.options.marker,
10630
			symbolAttr = { fill: symbolColor },
10631
			key,
10632
			val;
10633
 
10634
		if (legendItem) {
10635
			legendItem.css({ fill: textColor, color: textColor }); // color for #1553, oldIE
10636
		}
10637
		if (legendLine) {
10638
			legendLine.attr({ stroke: symbolColor });
10639
		}
10640
 
10641
		if (legendSymbol) {
10642
 
10643
			// Apply marker options
10644
			if (markerOptions && legendSymbol.isMarker) { // #585
10645
				symbolAttr.stroke = symbolColor;
10646
				markerOptions = item.convertAttribs(markerOptions);
10647
				for (key in markerOptions) {
10648
					val = markerOptions[key];
10649
					if (val !== UNDEFINED) {
10650
						symbolAttr[key] = val;
10651
					}
10652
				}
10653
			}
10654
 
10655
			legendSymbol.attr(symbolAttr);
10656
		}
10657
	},
10658
 
10659
	/**
10660
	 * Position the legend item
10661
	 * @param {Object} item A Series or Point instance
10662
	 */
10663
	positionItem: function (item) {
10664
		var legend = this,
10665
			options = legend.options,
10666
			symbolPadding = options.symbolPadding,
10667
			ltr = !options.rtl,
10668
			legendItemPos = item._legendItemPos,
10669
			itemX = legendItemPos[0],
10670
			itemY = legendItemPos[1],
10671
			checkbox = item.checkbox,
10672
			legendGroup = item.legendGroup;
10673
 
10674
		if (legendGroup && legendGroup.element) {
10675
			legendGroup.translate(
10676
				ltr ? itemX : legend.legendWidth - itemX - 2 * symbolPadding - 4,
10677
				itemY
10678
			);
10679
		}
10680
 
10681
		if (checkbox) {
10682
			checkbox.x = itemX;
10683
			checkbox.y = itemY;
10684
		}
10685
	},
10686
 
10687
	/**
10688
	 * Destroy a single legend item
10689
	 * @param {Object} item The series or point
10690
	 */
10691
	destroyItem: function (item) {
10692
		var checkbox = item.checkbox;
10693
 
10694
		// destroy SVG elements
10695
		each(['legendItem', 'legendLine', 'legendSymbol', 'legendGroup'], function (key) {
10696
			if (item[key]) {
10697
				item[key] = item[key].destroy();
10698
			}
10699
		});
10700
 
10701
		if (checkbox) {
10702
			discardElement(item.checkbox);
10703
		}
10704
	},
10705
 
10706
	/**
10707
	 * Destroys the legend.
10708
	 */
10709
	destroy: function () {
10710
		var legend = this,
10711
			legendGroup = legend.group,
10712
			box = legend.box;
10713
 
10714
		if (box) {
10715
			legend.box = box.destroy();
10716
		}
10717
 
10718
		if (legendGroup) {
10719
			legend.group = legendGroup.destroy();
10720
		}
10721
	},
10722
 
10723
	/**
10724
	 * Position the checkboxes after the width is determined
10725
	 */
10726
	positionCheckboxes: function (scrollOffset) {
10727
		var alignAttr = this.group.alignAttr,
10728
			translateY,
10729
			clipHeight = this.clipHeight || this.legendHeight;
10730
 
10731
		if (alignAttr) {
10732
			translateY = alignAttr.translateY;
10733
			each(this.allItems, function (item) {
10734
				var checkbox = item.checkbox,
10735
					top;
10736
 
10737
				if (checkbox) {
10738
					top = (translateY + checkbox.y + (scrollOffset || 0) + 3);
10739
					css(checkbox, {
10740
						left: (alignAttr.translateX + item.checkboxOffset + checkbox.x - 20) + PX,
10741
						top: top + PX,
10742
						display: top > translateY - 6 && top < translateY + clipHeight - 6 ? '' : NONE
10743
					});
10744
				}
10745
			});
10746
		}
10747
	},
10748
 
10749
	/**
10750
	 * Render the legend title on top of the legend
10751
	 */
10752
	renderTitle: function () {
10753
		var options = this.options,
10754
			padding = this.padding,
10755
			titleOptions = options.title,
10756
			titleHeight = 0,
10757
			bBox;
10758
 
10759
		if (titleOptions.text) {
10760
			if (!this.title) {
10761
				this.title = this.chart.renderer.label(titleOptions.text, padding - 3, padding - 4, null, null, null, null, null, 'legend-title')
10762
					.attr({ zIndex: 1 })
10763
					.css(titleOptions.style)
10764
					.add(this.group);
10765
			}
10766
			bBox = this.title.getBBox();
10767
			titleHeight = bBox.height;
10768
			this.offsetWidth = bBox.width; // #1717
10769
			this.contentGroup.attr({ translateY: titleHeight });
10770
		}
10771
		this.titleHeight = titleHeight;
10772
	},
10773
 
10774
	/**
10775
	 * Set the legend item text
10776
	 */
10777
	setText: function (item) {
10778
		var options = this.options;
10779
		item.legendItem.attr({
10780
			text: options.labelFormat ? format(options.labelFormat, item) : options.labelFormatter.call(item)
10781
		});
10782
	},
10783
 
10784
	/**
10785
	 * Render a single specific legend item
10786
	 * @param {Object} item A series or point
10787
	 */
10788
	renderItem: function (item) {
10789
		var legend = this,
10790
			chart = legend.chart,
10791
			renderer = chart.renderer,
10792
			options = legend.options,
10793
			horizontal = options.layout === 'horizontal',
10794
			symbolWidth = legend.symbolWidth,
10795
			symbolPadding = options.symbolPadding,
10796
			itemStyle = legend.itemStyle,
10797
			itemHiddenStyle = legend.itemHiddenStyle,
10798
			padding = legend.padding,
10799
			itemDistance = horizontal ? pick(options.itemDistance, 20) : 0,
10800
			ltr = !options.rtl,
10801
			itemHeight,
10802
			widthOption = options.width,
10803
			itemMarginBottom = options.itemMarginBottom || 0,
10804
			itemMarginTop = legend.itemMarginTop,
10805
			initialItemX = legend.initialItemX,
10806
			bBox,
10807
			itemWidth,
10808
			li = item.legendItem,
10809
			series = item.series && item.series.drawLegendSymbol ? item.series : item,
10810
			seriesOptions = series.options,
10811
			showCheckbox = legend.createCheckboxForItem && seriesOptions && seriesOptions.showCheckbox,
10812
			useHTML = options.useHTML;
10813
 
10814
		if (!li) { // generate it once, later move it
10815
 
10816
			// Generate the group box
10817
			// A group to hold the symbol and text. Text is to be appended in Legend class.
10818
			item.legendGroup = renderer.g('legend-item')
10819
				.attr({ zIndex: 1 })
10820
				.add(legend.scrollGroup);
10821
 
10822
			// Generate the list item text and add it to the group
10823
			item.legendItem = li = renderer.text(
10824
					'',
10825
					ltr ? symbolWidth + symbolPadding : -symbolPadding,
10826
					legend.baseline || 0,
10827
					useHTML
10828
				)
10829
				.css(merge(item.visible ? itemStyle : itemHiddenStyle)) // merge to prevent modifying original (#1021)
10830
				.attr({
10831
					align: ltr ? 'left' : 'right',
10832
					zIndex: 2
10833
				})
10834
				.add(item.legendGroup);
10835
 
10836
			// Get the baseline for the first item - the font size is equal for all
10837
			if (!legend.baseline) {
10838
				legend.fontMetrics = renderer.fontMetrics(itemStyle.fontSize, li);
10839
				legend.baseline = legend.fontMetrics.f + 3 + itemMarginTop;
10840
				li.attr('y', legend.baseline);
10841
			}
10842
 
10843
			// Draw the legend symbol inside the group box
10844
			series.drawLegendSymbol(legend, item);
10845
 
10846
			if (legend.setItemEvents) {
10847
				legend.setItemEvents(item, li, useHTML, itemStyle, itemHiddenStyle);
10848
			}
10849
 
10850
			// Colorize the items
10851
			legend.colorizeItem(item, item.visible);
10852
 
10853
			// add the HTML checkbox on top
10854
			if (showCheckbox) {
10855
				legend.createCheckboxForItem(item);
10856
			}
10857
		}
10858
 
10859
		// Always update the text
10860
		legend.setText(item);
10861
 
10862
		// calculate the positions for the next line
10863
		bBox = li.getBBox();
10864
 
10865
		itemWidth = item.checkboxOffset =
10866
			options.itemWidth ||
10867
			item.legendItemWidth ||
10868
			symbolWidth + symbolPadding + bBox.width + itemDistance + (showCheckbox ? 20 : 0);
10869
		legend.itemHeight = itemHeight = mathRound(item.legendItemHeight || bBox.height);
10870
 
10871
		// if the item exceeds the width, start a new line
10872
		if (horizontal && legend.itemX - initialItemX + itemWidth >
10873
				(widthOption || (chart.chartWidth - 2 * padding - initialItemX - options.x))) {
10874
			legend.itemX = initialItemX;
10875
			legend.itemY += itemMarginTop + legend.lastLineHeight + itemMarginBottom;
10876
			legend.lastLineHeight = 0; // reset for next line (#915, #3976)
10877
		}
10878
 
10879
		// If the item exceeds the height, start a new column
10880
		/*if (!horizontal && legend.itemY + options.y + itemHeight > chart.chartHeight - spacingTop - spacingBottom) {
10881
			legend.itemY = legend.initialItemY;
10882
			legend.itemX += legend.maxItemWidth;
10883
			legend.maxItemWidth = 0;
10884
		}*/
10885
 
10886
		// Set the edge positions
10887
		legend.maxItemWidth = mathMax(legend.maxItemWidth, itemWidth);
10888
		legend.lastItemY = itemMarginTop + legend.itemY + itemMarginBottom;
10889
		legend.lastLineHeight = mathMax(itemHeight, legend.lastLineHeight); // #915
10890
 
10891
		// cache the position of the newly generated or reordered items
10892
		item._legendItemPos = [legend.itemX, legend.itemY];
10893
 
10894
		// advance
10895
		if (horizontal) {
10896
			legend.itemX += itemWidth;
10897
 
10898
		} else {
10899
			legend.itemY += itemMarginTop + itemHeight + itemMarginBottom;
10900
			legend.lastLineHeight = itemHeight;
10901
		}
10902
 
10903
		// the width of the widest item
10904
		legend.offsetWidth = widthOption || mathMax(
10905
			(horizontal ? legend.itemX - initialItemX - itemDistance : itemWidth) + padding,
10906
			legend.offsetWidth
10907
		);
10908
	},
10909
 
10910
	/**
10911
	 * Get all items, which is one item per series for normal series and one item per point
10912
	 * for pie series.
10913
	 */
10914
	getAllItems: function () {
10915
		var allItems = [];
10916
		each(this.chart.series, function (series) {
10917
			var seriesOptions = series.options;
10918
 
10919
			// Handle showInLegend. If the series is linked to another series, defaults to false.
10920
			if (!pick(seriesOptions.showInLegend, !defined(seriesOptions.linkedTo) ? UNDEFINED : false, true)) {
10921
				return;
10922
			}
10923
 
10924
			// use points or series for the legend item depending on legendType
10925
			allItems = allItems.concat(
10926
					series.legendItems ||
10927
					(seriesOptions.legendType === 'point' ?
10928
							series.data :
10929
							series)
10930
			);
10931
		});
10932
		return allItems;
10933
	},
10934
 
10935
	/**
10936
	 * Adjust the chart margins by reserving space for the legend on only one side
10937
	 * of the chart. If the position is set to a corner, top or bottom is reserved
10938
	 * for horizontal legends and left or right for vertical ones.
10939
	 */
10940
	adjustMargins: function (margin, spacing) {
10941
		var chart = this.chart,
10942
			options = this.options,
10943
			// Use the first letter of each alignment option in order to detect the side
10944
			alignment = options.align.charAt(0) + options.verticalAlign.charAt(0) + options.layout.charAt(0); // #4189 - use charAt(x) notation instead of [x] for IE7
10945
 
10946
		if (this.display && !options.floating) {
10947
 
10948
			each([
10949
				/(lth|ct|rth)/,
10950
				/(rtv|rm|rbv)/,
10951
				/(rbh|cb|lbh)/,
10952
				/(lbv|lm|ltv)/
10953
			], function (alignments, side) {
10954
				if (alignments.test(alignment) && !defined(margin[side])) {
10955
					// Now we have detected on which side of the chart we should reserve space for the legend
10956
					chart[marginNames[side]] = mathMax(
10957
						chart[marginNames[side]],
10958
						chart.legend[(side + 1) % 2 ? 'legendHeight' : 'legendWidth'] +
10959
							[1, -1, -1, 1][side] * options[(side % 2) ? 'x' : 'y'] +
10960
							pick(options.margin, 12) +
10961
							spacing[side]
10962
					);
10963
				}
10964
			});
10965
		}
10966
	},
10967
 
10968
	/**
10969
	 * Render the legend. This method can be called both before and after
10970
	 * chart.render. If called after, it will only rearrange items instead
10971
	 * of creating new ones.
10972
	 */
10973
	render: function () {
10974
		var legend = this,
10975
			chart = legend.chart,
10976
			renderer = chart.renderer,
10977
			legendGroup = legend.group,
10978
			allItems,
10979
			display,
10980
			legendWidth,
10981
			legendHeight,
10982
			box = legend.box,
10983
			options = legend.options,
10984
			padding = legend.padding,
10985
			legendBorderWidth = options.borderWidth,
10986
			legendBackgroundColor = options.backgroundColor;
10987
 
10988
		legend.itemX = legend.initialItemX;
10989
		legend.itemY = legend.initialItemY;
10990
		legend.offsetWidth = 0;
10991
		legend.lastItemY = 0;
10992
 
10993
		if (!legendGroup) {
10994
			legend.group = legendGroup = renderer.g('legend')
10995
				.attr({ zIndex: 7 })
10996
				.add();
10997
			legend.contentGroup = renderer.g()
10998
				.attr({ zIndex: 1 }) // above background
10999
				.add(legendGroup);
11000
			legend.scrollGroup = renderer.g()
11001
				.add(legend.contentGroup);
11002
		}
11003
 
11004
		legend.renderTitle();
11005
 
11006
		// add each series or point
11007
		allItems = legend.getAllItems();
11008
 
11009
		// sort by legendIndex
11010
		stableSort(allItems, function (a, b) {
11011
			return ((a.options && a.options.legendIndex) || 0) - ((b.options && b.options.legendIndex) || 0);
11012
		});
11013
 
11014
		// reversed legend
11015
		if (options.reversed) {
11016
			allItems.reverse();
11017
		}
11018
 
11019
		legend.allItems = allItems;
11020
		legend.display = display = !!allItems.length;
11021
 
11022
		// render the items
11023
		legend.lastLineHeight = 0;
11024
		each(allItems, function (item) {
11025
			legend.renderItem(item);
11026
		});
11027
 
11028
		// Get the box
11029
		legendWidth = (options.width || legend.offsetWidth) + padding;
11030
		legendHeight = legend.lastItemY + legend.lastLineHeight + legend.titleHeight;
11031
		legendHeight = legend.handleOverflow(legendHeight);
11032
		legendHeight += padding;
11033
 
11034
		// Draw the border and/or background
11035
		if (legendBorderWidth || legendBackgroundColor) {
11036
 
11037
			if (!box) {
11038
				legend.box = box = renderer.rect(
11039
					0,
11040
					0,
11041
					legendWidth,
11042
					legendHeight,
11043
					options.borderRadius,
11044
					legendBorderWidth || 0
11045
				).attr({
11046
					stroke: options.borderColor,
11047
					'stroke-width': legendBorderWidth || 0,
11048
					fill: legendBackgroundColor || NONE
11049
				})
11050
				.add(legendGroup)
11051
				.shadow(options.shadow);
11052
				box.isNew = true;
11053
 
11054
			} else if (legendWidth > 0 && legendHeight > 0) {
11055
				box[box.isNew ? 'attr' : 'animate'](
11056
					box.crisp({ width: legendWidth, height: legendHeight })
11057
				);
11058
				box.isNew = false;
11059
			}
11060
 
11061
			// hide the border if no items
11062
			box[display ? 'show' : 'hide']();
11063
		}
11064
 
11065
		legend.legendWidth = legendWidth;
11066
		legend.legendHeight = legendHeight;
11067
 
11068
		// Now that the legend width and height are established, put the items in the
11069
		// final position
11070
		each(allItems, function (item) {
11071
			legend.positionItem(item);
11072
		});
11073
 
11074
		// 1.x compatibility: positioning based on style
11075
		/*var props = ['left', 'right', 'top', 'bottom'],
11076
			prop,
11077
			i = 4;
11078
		while (i--) {
11079
			prop = props[i];
11080
			if (options.style[prop] && options.style[prop] !== 'auto') {
11081
				options[i < 2 ? 'align' : 'verticalAlign'] = prop;
11082
				options[i < 2 ? 'x' : 'y'] = pInt(options.style[prop]) * (i % 2 ? -1 : 1);
11083
			}
11084
		}*/
11085
 
11086
		if (display) {
11087
			legendGroup.align(extend({
11088
				width: legendWidth,
11089
				height: legendHeight
11090
			}, options), true, 'spacingBox');
11091
		}
11092
 
11093
		if (!chart.isResizing) {
11094
			this.positionCheckboxes();
11095
		}
11096
	},
11097
 
11098
	/**
11099
	 * Set up the overflow handling by adding navigation with up and down arrows below the
11100
	 * legend.
11101
	 */
11102
	handleOverflow: function (legendHeight) {
11103
		var legend = this,
11104
			chart = this.chart,
11105
			renderer = chart.renderer,
11106
			options = this.options,
11107
			optionsY = options.y,
11108
			alignTop = options.verticalAlign === 'top',
11109
			spaceHeight = chart.spacingBox.height + (alignTop ? -optionsY : optionsY) - this.padding,
11110
			maxHeight = options.maxHeight,
11111
			clipHeight,
11112
			clipRect = this.clipRect,
11113
			navOptions = options.navigation,
11114
			animation = pick(navOptions.animation, true),
11115
			arrowSize = navOptions.arrowSize || 12,
11116
			nav = this.nav,
11117
			pages = this.pages,
11118
			padding = this.padding,
11119
			lastY,
11120
			allItems = this.allItems,
11121
			clipToHeight = function (height) {
11122
				clipRect.attr({
11123
					height: height
11124
				});
11125
 
11126
				// useHTML
11127
				if (legend.contentGroup.div) {
11128
					legend.contentGroup.div.style.clip = 'rect(' + padding + 'px,9999px,' + (padding + height) + 'px,0)';
11129
				}
11130
			};
11131
 
11132
 
11133
		// Adjust the height
11134
		if (options.layout === 'horizontal') {
11135
			spaceHeight /= 2;
11136
		}
11137
		if (maxHeight) {
11138
			spaceHeight = mathMin(spaceHeight, maxHeight);
11139
		}
11140
 
11141
		// Reset the legend height and adjust the clipping rectangle
11142
		pages.length = 0;
11143
		if (legendHeight > spaceHeight) {
11144
 
11145
			this.clipHeight = clipHeight = mathMax(spaceHeight - 20 - this.titleHeight - padding, 0);
11146
			this.currentPage = pick(this.currentPage, 1);
11147
			this.fullHeight = legendHeight;
11148
 
11149
			// Fill pages with Y positions so that the top of each a legend item defines
11150
			// the scroll top for each page (#2098)
11151
			each(allItems, function (item, i) {
11152
				var y = item._legendItemPos[1],
11153
					h = mathRound(item.legendItem.getBBox().height),
11154
					len = pages.length;
11155
 
11156
				if (!len || (y - pages[len - 1] > clipHeight && (lastY || y) !== pages[len - 1])) {
11157
					pages.push(lastY || y);
11158
					len++;
11159
				}
11160
 
11161
				if (i === allItems.length - 1 && y + h - pages[len - 1] > clipHeight) {
11162
					pages.push(y);
11163
				}
11164
				if (y !== lastY) {
11165
					lastY = y;
11166
				}
11167
			});
11168
 
11169
			// Only apply clipping if needed. Clipping causes blurred legend in PDF export (#1787)
11170
			if (!clipRect) {
11171
				clipRect = legend.clipRect = renderer.clipRect(0, padding, 9999, 0);
11172
				legend.contentGroup.clip(clipRect);
11173
			}
11174
 
11175
			clipToHeight(clipHeight);
11176
 
11177
			// Add navigation elements
11178
			if (!nav) {
11179
				this.nav = nav = renderer.g().attr({ zIndex: 1 }).add(this.group);
11180
				this.up = renderer.symbol('triangle', 0, 0, arrowSize, arrowSize)
11181
					.on('click', function () {
11182
						legend.scroll(-1, animation);
11183
					})
11184
					.add(nav);
11185
				this.pager = renderer.text('', 15, 10)
11186
					.css(navOptions.style)
11187
					.add(nav);
11188
				this.down = renderer.symbol('triangle-down', 0, 0, arrowSize, arrowSize)
11189
					.on('click', function () {
11190
						legend.scroll(1, animation);
11191
					})
11192
					.add(nav);
11193
			}
11194
 
11195
			// Set initial position
11196
			legend.scroll(0);
11197
 
11198
			legendHeight = spaceHeight;
11199
 
11200
		} else if (nav) {
11201
			clipToHeight(chart.chartHeight);
11202
			nav.hide();
11203
			this.scrollGroup.attr({
11204
				translateY: 1
11205
			});
11206
			this.clipHeight = 0; // #1379
11207
		}
11208
 
11209
		return legendHeight;
11210
	},
11211
 
11212
	/**
11213
	 * Scroll the legend by a number of pages
11214
	 * @param {Object} scrollBy
11215
	 * @param {Object} animation
11216
	 */
11217
	scroll: function (scrollBy, animation) {
11218
		var pages = this.pages,
11219
			pageCount = pages.length,
11220
			currentPage = this.currentPage + scrollBy,
11221
			clipHeight = this.clipHeight,
11222
			navOptions = this.options.navigation,
11223
			activeColor = navOptions.activeColor,
11224
			inactiveColor = navOptions.inactiveColor,
11225
			pager = this.pager,
11226
			padding = this.padding,
11227
			scrollOffset;
11228
 
11229
		// When resizing while looking at the last page
11230
		if (currentPage > pageCount) {
11231
			currentPage = pageCount;
11232
		}
11233
 
11234
		if (currentPage > 0) {
11235
 
11236
			if (animation !== UNDEFINED) {
11237
				setAnimation(animation, this.chart);
11238
			}
11239
 
11240
			this.nav.attr({
11241
				translateX: padding,
11242
				translateY: clipHeight + this.padding + 7 + this.titleHeight,
11243
				visibility: VISIBLE
11244
			});
11245
			this.up.attr({
11246
					fill: currentPage === 1 ? inactiveColor : activeColor
11247
				})
11248
				.css({
11249
					cursor: currentPage === 1 ? 'default' : 'pointer'
11250
				});
11251
			pager.attr({
11252
				text: currentPage + '/' + pageCount
11253
			});
11254
			this.down.attr({
11255
					x: 18 + this.pager.getBBox().width, // adjust to text width
11256
					fill: currentPage === pageCount ? inactiveColor : activeColor
11257
				})
11258
				.css({
11259
					cursor: currentPage === pageCount ? 'default' : 'pointer'
11260
				});
11261
 
11262
			scrollOffset = -pages[currentPage - 1] + this.initialItemY;
11263
 
11264
			this.scrollGroup.animate({
11265
				translateY: scrollOffset
11266
			});
11267
 
11268
			this.currentPage = currentPage;
11269
			this.positionCheckboxes(scrollOffset);
11270
		}
11271
 
11272
	}
11273
 
11274
};
11275
 
11276
/*
11277
 * LegendSymbolMixin
11278
 */
11279
 
11280
var LegendSymbolMixin = Highcharts.LegendSymbolMixin = {
11281
 
11282
	/**
11283
	 * Get the series' symbol in the legend
11284
	 *
11285
	 * @param {Object} legend The legend object
11286
	 * @param {Object} item The series (this) or point
11287
	 */
11288
	drawRectangle: function (legend, item) {
11289
		var symbolHeight = legend.options.symbolHeight || legend.fontMetrics.f;
11290
 
11291
		item.legendSymbol = this.chart.renderer.rect(
11292
			0,
11293
			legend.baseline - symbolHeight + 1, // #3988
11294
			legend.symbolWidth,
11295
			symbolHeight,
11296
			legend.options.symbolRadius || 0
11297
		).attr({
11298
			zIndex: 3
11299
		}).add(item.legendGroup);
11300
 
11301
	},
11302
 
11303
	/**
11304
	 * Get the series' symbol in the legend. This method should be overridable to create custom
11305
	 * symbols through Highcharts.seriesTypes[type].prototype.drawLegendSymbols.
11306
	 *
11307
	 * @param {Object} legend The legend object
11308
	 */
11309
	drawLineMarker: function (legend) {
11310
 
11311
		var options = this.options,
11312
			markerOptions = options.marker,
11313
			radius,
11314
			legendSymbol,
11315
			symbolWidth = legend.symbolWidth,
11316
			renderer = this.chart.renderer,
11317
			legendItemGroup = this.legendGroup,
11318
			verticalCenter = legend.baseline - mathRound(legend.fontMetrics.b * 0.3),
11319
			attr;
11320
 
11321
		// Draw the line
11322
		if (options.lineWidth) {
11323
			attr = {
11324
				'stroke-width': options.lineWidth
11325
			};
11326
			if (options.dashStyle) {
11327
				attr.dashstyle = options.dashStyle;
11328
			}
11329
			this.legendLine = renderer.path([
11330
				M,
11331
				0,
11332
				verticalCenter,
11333
				L,
11334
				symbolWidth,
11335
				verticalCenter
11336
			])
11337
			.attr(attr)
11338
			.add(legendItemGroup);
11339
		}
11340
 
11341
		// Draw the marker
11342
		if (markerOptions && markerOptions.enabled !== false) {
11343
			radius = markerOptions.radius;
11344
			this.legendSymbol = legendSymbol = renderer.symbol(
11345
				this.symbol,
11346
				(symbolWidth / 2) - radius,
11347
				verticalCenter - radius,
11348
				2 * radius,
11349
				2 * radius
11350
			)
11351
			.add(legendItemGroup);
11352
			legendSymbol.isMarker = true;
11353
		}
11354
	}
11355
};
11356
 
11357
// Workaround for #2030, horizontal legend items not displaying in IE11 Preview,
11358
// and for #2580, a similar drawing flaw in Firefox 26.
11359
// TODO: Explore if there's a general cause for this. The problem may be related
11360
// to nested group elements, as the legend item texts are within 4 group elements.
11361
if (/Trident\/7\.0/.test(userAgent) || isFirefox) {
11362
	wrap(Legend.prototype, 'positionItem', function (proceed, item) {
11363
		var legend = this,
11364
			runPositionItem = function () { // If chart destroyed in sync, this is undefined (#2030)
11365
				if (item._legendItemPos) {
11366
					proceed.call(legend, item);
11367
				}
11368
			};
11369
 
11370
		// Do it now, for export and to get checkbox placement
11371
		runPositionItem();
11372
 
11373
		// Do it after to work around the core issue
11374
		setTimeout(runPositionItem);
11375
	});
11376
}
11377
/**
11378
 * The chart class
11379
 * @param {Object} options
11380
 * @param {Function} callback Function to run when the chart has loaded
11381
 */
11382
var Chart = Highcharts.Chart = function () {
11383
	this.init.apply(this, arguments);
11384
};
11385
 
11386
Chart.prototype = {
11387
 
11388
	/**
11389
	 * Hook for modules
11390
	 */
11391
	callbacks: [],
11392
 
11393
	/**
11394
	 * Initialize the chart
11395
	 */
11396
	init: function (userOptions, callback) {
11397
 
11398
		// Handle regular options
11399
		var options,
11400
			seriesOptions = userOptions.series; // skip merging data points to increase performance
11401
 
11402
		userOptions.series = null;
11403
		options = merge(defaultOptions, userOptions); // do the merge
11404
		options.series = userOptions.series = seriesOptions; // set back the series data
11405
		this.userOptions = userOptions;
11406
 
11407
		var optionsChart = options.chart;
11408
 
11409
		// Create margin & spacing array
11410
		this.margin = this.splashArray('margin', optionsChart);
11411
		this.spacing = this.splashArray('spacing', optionsChart);
11412
 
11413
		var chartEvents = optionsChart.events;
11414
 
11415
		//this.runChartClick = chartEvents && !!chartEvents.click;
11416
		this.bounds = { h: {}, v: {} }; // Pixel data bounds for touch zoom
11417
 
11418
		this.callback = callback;
11419
		this.isResizing = 0;
11420
		this.options = options;
11421
		//chartTitleOptions = UNDEFINED;
11422
		//chartSubtitleOptions = UNDEFINED;
11423
 
11424
		this.axes = [];
11425
		this.series = [];
11426
		this.hasCartesianSeries = optionsChart.showAxes;
11427
		//this.axisOffset = UNDEFINED;
11428
		//this.maxTicks = UNDEFINED; // handle the greatest amount of ticks on grouped axes
11429
		//this.inverted = UNDEFINED;
11430
		//this.loadingShown = UNDEFINED;
11431
		//this.container = UNDEFINED;
11432
		//this.chartWidth = UNDEFINED;
11433
		//this.chartHeight = UNDEFINED;
11434
		//this.marginRight = UNDEFINED;
11435
		//this.marginBottom = UNDEFINED;
11436
		//this.containerWidth = UNDEFINED;
11437
		//this.containerHeight = UNDEFINED;
11438
		//this.oldChartWidth = UNDEFINED;
11439
		//this.oldChartHeight = UNDEFINED;
11440
 
11441
		//this.renderTo = UNDEFINED;
11442
		//this.renderToClone = UNDEFINED;
11443
 
11444
		//this.spacingBox = UNDEFINED
11445
 
11446
		//this.legend = UNDEFINED;
11447
 
11448
		// Elements
11449
		//this.chartBackground = UNDEFINED;
11450
		//this.plotBackground = UNDEFINED;
11451
		//this.plotBGImage = UNDEFINED;
11452
		//this.plotBorder = UNDEFINED;
11453
		//this.loadingDiv = UNDEFINED;
11454
		//this.loadingSpan = UNDEFINED;
11455
 
11456
		var chart = this,
11457
			eventType;
11458
 
11459
		// Add the chart to the global lookup
11460
		chart.index = charts.length;
11461
		charts.push(chart);
11462
		chartCount++;
11463
 
11464
		// Set up auto resize
11465
		if (optionsChart.reflow !== false) {
11466
			addEvent(chart, 'load', function () {
11467
				chart.initReflow();
11468
			});
11469
		}
11470
 
11471
		// Chart event handlers
11472
		if (chartEvents) {
11473
			for (eventType in chartEvents) {
11474
				addEvent(chart, eventType, chartEvents[eventType]);
11475
			}
11476
		}
11477
 
11478
		chart.xAxis = [];
11479
		chart.yAxis = [];
11480
 
11481
		// Expose methods and variables
11482
		chart.animation = useCanVG ? false : pick(optionsChart.animation, true);
11483
		chart.pointCount = chart.colorCounter = chart.symbolCounter = 0;
11484
 
11485
		chart.firstRender();
11486
	},
11487
 
11488
	/**
11489
	 * Initialize an individual series, called internally before render time
11490
	 */
11491
	initSeries: function (options) {
11492
		var chart = this,
11493
			optionsChart = chart.options.chart,
11494
			type = options.type || optionsChart.type || optionsChart.defaultSeriesType,
11495
			series,
11496
			constr = seriesTypes[type];
11497
 
11498
		// No such series type
11499
		if (!constr) {
11500
			error(17, true);
11501
		}
11502
 
11503
		series = new constr();
11504
		series.init(this, options);
11505
		return series;
11506
	},
11507
 
11508
	/**
11509
	 * Check whether a given point is within the plot area
11510
	 *
11511
	 * @param {Number} plotX Pixel x relative to the plot area
11512
	 * @param {Number} plotY Pixel y relative to the plot area
11513
	 * @param {Boolean} inverted Whether the chart is inverted
11514
	 */
11515
	isInsidePlot: function (plotX, plotY, inverted) {
11516
		var x = inverted ? plotY : plotX,
11517
			y = inverted ? plotX : plotY;
11518
 
11519
		return x >= 0 &&
11520
			x <= this.plotWidth &&
11521
			y >= 0 &&
11522
			y <= this.plotHeight;
11523
	},
11524
 
11525
	/**
11526
	 * Redraw legend, axes or series based on updated data
11527
	 *
11528
	 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
11529
	 *    configuration
11530
	 */
11531
	redraw: function (animation) {
11532
		var chart = this,
11533
			axes = chart.axes,
11534
			series = chart.series,
11535
			pointer = chart.pointer,
11536
			legend = chart.legend,
11537
			redrawLegend = chart.isDirtyLegend,
11538
			hasStackedSeries,
11539
			hasDirtyStacks,
11540
			hasCartesianSeries = chart.hasCartesianSeries,
11541
			isDirtyBox = chart.isDirtyBox, // todo: check if it has actually changed?
11542
			seriesLength = series.length,
11543
			i = seriesLength,
11544
			serie,
11545
			renderer = chart.renderer,
11546
			isHiddenChart = renderer.isHidden(),
11547
			afterRedraw = [];
11548
 
11549
		setAnimation(animation, chart);
11550
 
11551
		if (isHiddenChart) {
11552
			chart.cloneRenderTo();
11553
		}
11554
 
11555
		// Adjust title layout (reflow multiline text)
11556
		chart.layOutTitles();
11557
 
11558
		// link stacked series
11559
		while (i--) {
11560
			serie = series[i];
11561
 
11562
			if (serie.options.stacking) {
11563
				hasStackedSeries = true;
11564
 
11565
				if (serie.isDirty) {
11566
					hasDirtyStacks = true;
11567
					break;
11568
				}
11569
			}
11570
		}
11571
		if (hasDirtyStacks) { // mark others as dirty
11572
			i = seriesLength;
11573
			while (i--) {
11574
				serie = series[i];
11575
				if (serie.options.stacking) {
11576
					serie.isDirty = true;
11577
				}
11578
			}
11579
		}
11580
 
11581
		// Handle updated data in the series
11582
		each(series, function (serie) {
11583
			if (serie.isDirty) {
11584
				if (serie.options.legendType === 'point') {
11585
					if (serie.updateTotals) {
11586
						serie.updateTotals();
11587
					}
11588
					redrawLegend = true;
11589
				}
11590
			}
11591
		});
11592
 
11593
		// handle added or removed series
11594
		if (redrawLegend && legend.options.enabled) { // series or pie points are added or removed
11595
			// draw legend graphics
11596
			legend.render();
11597
 
11598
			chart.isDirtyLegend = false;
11599
		}
11600
 
11601
		// reset stacks
11602
		if (hasStackedSeries) {
11603
			chart.getStacks();
11604
		}
11605
 
11606
 
11607
		if (hasCartesianSeries) {
11608
			if (!chart.isResizing) {
11609
 
11610
				// reset maxTicks
11611
				chart.maxTicks = null;
11612
 
11613
				// set axes scales
11614
				each(axes, function (axis) {
11615
					axis.setScale();
11616
				});
11617
			}
11618
		}
11619
 
11620
		chart.getMargins(); // #3098
11621
 
11622
		if (hasCartesianSeries) {
11623
			// If one axis is dirty, all axes must be redrawn (#792, #2169)
11624
			each(axes, function (axis) {
11625
				if (axis.isDirty) {
11626
					isDirtyBox = true;
11627
				}
11628
			});
11629
 
11630
			// redraw axes
11631
			each(axes, function (axis) {
11632
 
11633
				// Fire 'afterSetExtremes' only if extremes are set
11634
				var key = axis.min + ',' + axis.max;
11635
				if (axis.extKey !== key) { // #821, #4452
11636
					axis.extKey = key;
11637
					afterRedraw.push(function () { // prevent a recursive call to chart.redraw() (#1119)
11638
						fireEvent(axis, 'afterSetExtremes', extend(axis.eventArgs, axis.getExtremes())); // #747, #751
11639
						delete axis.eventArgs;
11640
					});
11641
				}
11642
				if (isDirtyBox || hasStackedSeries) {
11643
					axis.redraw();
11644
				}
11645
			});
11646
		}
11647
 
11648
		// the plot areas size has changed
11649
		if (isDirtyBox) {
11650
			chart.drawChartBox();
11651
		}
11652
 
11653
 
11654
		// redraw affected series
11655
		each(series, function (serie) {
11656
			if (serie.isDirty && serie.visible &&
11657
					(!serie.isCartesian || serie.xAxis)) { // issue #153
11658
				serie.redraw();
11659
			}
11660
		});
11661
 
11662
		// move tooltip or reset
11663
		if (pointer) {
11664
			pointer.reset(true);
11665
		}
11666
 
11667
		// redraw if canvas
11668
		renderer.draw();
11669
 
11670
		// fire the event
11671
		fireEvent(chart, 'redraw'); // jQuery breaks this when calling it from addEvent. Overwrites chart.redraw
11672
 
11673
		if (isHiddenChart) {
11674
			chart.cloneRenderTo(true);
11675
		}
11676
 
11677
		// Fire callbacks that are put on hold until after the redraw
11678
		each(afterRedraw, function (callback) {
11679
			callback.call();
11680
		});
11681
	},
11682
 
11683
	/**
11684
	 * Get an axis, series or point object by id.
11685
	 * @param id {String} The id as given in the configuration options
11686
	 */
11687
	get: function (id) {
11688
		var chart = this,
11689
			axes = chart.axes,
11690
			series = chart.series;
11691
 
11692
		var i,
11693
			j,
11694
			points;
11695
 
11696
		// search axes
11697
		for (i = 0; i < axes.length; i++) {
11698
			if (axes[i].options.id === id) {
11699
				return axes[i];
11700
			}
11701
		}
11702
 
11703
		// search series
11704
		for (i = 0; i < series.length; i++) {
11705
			if (series[i].options.id === id) {
11706
				return series[i];
11707
			}
11708
		}
11709
 
11710
		// search points
11711
		for (i = 0; i < series.length; i++) {
11712
			points = series[i].points || [];
11713
			for (j = 0; j < points.length; j++) {
11714
				if (points[j].id === id) {
11715
					return points[j];
11716
				}
11717
			}
11718
		}
11719
		return null;
11720
	},
11721
 
11722
	/**
11723
	 * Create the Axis instances based on the config options
11724
	 */
11725
	getAxes: function () {
11726
		var chart = this,
11727
			options = this.options,
11728
			xAxisOptions = options.xAxis = splat(options.xAxis || {}),
11729
			yAxisOptions = options.yAxis = splat(options.yAxis || {}),
11730
			optionsArray,
11731
			axis;
11732
 
11733
		// make sure the options are arrays and add some members
11734
		each(xAxisOptions, function (axis, i) {
11735
			axis.index = i;
11736
			axis.isX = true;
11737
		});
11738
 
11739
		each(yAxisOptions, function (axis, i) {
11740
			axis.index = i;
11741
		});
11742
 
11743
		// concatenate all axis options into one array
11744
		optionsArray = xAxisOptions.concat(yAxisOptions);
11745
 
11746
		each(optionsArray, function (axisOptions) {
11747
			axis = new Axis(chart, axisOptions);
11748
		});
11749
	},
11750
 
11751
 
11752
	/**
11753
	 * Get the currently selected points from all series
11754
	 */
11755
	getSelectedPoints: function () {
11756
		var points = [];
11757
		each(this.series, function (serie) {
11758
			points = points.concat(grep(serie.points || [], function (point) {
11759
				return point.selected;
11760
			}));
11761
		});
11762
		return points;
11763
	},
11764
 
11765
	/**
11766
	 * Get the currently selected series
11767
	 */
11768
	getSelectedSeries: function () {
11769
		return grep(this.series, function (serie) {
11770
			return serie.selected;
11771
		});
11772
	},
11773
 
11774
	/**
11775
	 * Show the title and subtitle of the chart
11776
	 *
11777
	 * @param titleOptions {Object} New title options
11778
	 * @param subtitleOptions {Object} New subtitle options
11779
	 *
11780
	 */
11781
	setTitle: function (titleOptions, subtitleOptions, redraw) {
11782
		var chart = this,
11783
			options = chart.options,
11784
			chartTitleOptions,
11785
			chartSubtitleOptions;
11786
 
11787
		chartTitleOptions = options.title = merge(options.title, titleOptions);
11788
		chartSubtitleOptions = options.subtitle = merge(options.subtitle, subtitleOptions);
11789
 
11790
		// add title and subtitle
11791
		each([
11792
			['title', titleOptions, chartTitleOptions],
11793
			['subtitle', subtitleOptions, chartSubtitleOptions]
11794
		], function (arr) {
11795
			var name = arr[0],
11796
				title = chart[name],
11797
				titleOptions = arr[1],
11798
				chartTitleOptions = arr[2];
11799
 
11800
			if (title && titleOptions) {
11801
				chart[name] = title = title.destroy(); // remove old
11802
			}
11803
 
11804
			if (chartTitleOptions && chartTitleOptions.text && !title) {
11805
				chart[name] = chart.renderer.text(
11806
					chartTitleOptions.text,
11807
					0,
11808
					0,
11809
					chartTitleOptions.useHTML
11810
				)
11811
				.attr({
11812
					align: chartTitleOptions.align,
11813
					'class': PREFIX + name,
11814
					zIndex: chartTitleOptions.zIndex || 4
11815
				})
11816
				.css(chartTitleOptions.style)
11817
				.add();
11818
			}
11819
		});
11820
		chart.layOutTitles(redraw);
11821
	},
11822
 
11823
	/**
11824
	 * Lay out the chart titles and cache the full offset height for use in getMargins
11825
	 */
11826
	layOutTitles: function (redraw) {
11827
		var titleOffset = 0,
11828
			title = this.title,
11829
			subtitle = this.subtitle,
11830
			options = this.options,
11831
			titleOptions = options.title,
11832
			subtitleOptions = options.subtitle,
11833
			requiresDirtyBox,
11834
			renderer = this.renderer,
11835
			autoWidth = this.spacingBox.width - 44; // 44 makes room for default context button
11836
 
11837
		if (title) {
11838
			title
11839
				.css({ width: (titleOptions.width || autoWidth) + PX })
11840
				.align(extend({
11841
					y: renderer.fontMetrics(titleOptions.style.fontSize, title).b - 3
11842
				}, titleOptions), false, 'spacingBox');
11843
 
11844
			if (!titleOptions.floating && !titleOptions.verticalAlign) {
11845
				titleOffset = title.getBBox().height;
11846
			}
11847
		}
11848
		if (subtitle) {
11849
			subtitle
11850
				.css({ width: (subtitleOptions.width || autoWidth) + PX })
11851
				.align(extend({
11852
					y: titleOffset + (titleOptions.margin - 13) + renderer.fontMetrics(subtitleOptions.style.fontSize, title).b
11853
				}, subtitleOptions), false, 'spacingBox');
11854
 
11855
			if (!subtitleOptions.floating && !subtitleOptions.verticalAlign) {
11856
				titleOffset = mathCeil(titleOffset + subtitle.getBBox().height);
11857
			}
11858
		}
11859
 
11860
		requiresDirtyBox = this.titleOffset !== titleOffset;
11861
		this.titleOffset = titleOffset; // used in getMargins
11862
 
11863
		if (!this.isDirtyBox && requiresDirtyBox) {
11864
			this.isDirtyBox = requiresDirtyBox;
11865
			// Redraw if necessary (#2719, #2744)
11866
			if (this.hasRendered && pick(redraw, true) && this.isDirtyBox) {
11867
				this.redraw();
11868
			}
11869
		}
11870
	},
11871
 
11872
	/**
11873
	 * Get chart width and height according to options and container size
11874
	 */
11875
	getChartSize: function () {
11876
		var chart = this,
11877
			optionsChart = chart.options.chart,
11878
			widthOption = optionsChart.width,
11879
			heightOption = optionsChart.height,
11880
			renderTo = chart.renderToClone || chart.renderTo;
11881
 
11882
		// get inner width and height from jQuery (#824)
11883
		if (!defined(widthOption)) {
11884
			chart.containerWidth = adapterRun(renderTo, 'width');
11885
		}
11886
		if (!defined(heightOption)) {
11887
			chart.containerHeight = adapterRun(renderTo, 'height');
11888
		}
11889
 
11890
		chart.chartWidth = mathMax(0, widthOption || chart.containerWidth || 600); // #1393, 1460
11891
		chart.chartHeight = mathMax(0, pick(heightOption,
11892
			// the offsetHeight of an empty container is 0 in standard browsers, but 19 in IE7:
11893
			chart.containerHeight > 19 ? chart.containerHeight : 400));
11894
	},
11895
 
11896
	/**
11897
	 * Create a clone of the chart's renderTo div and place it outside the viewport to allow
11898
	 * size computation on chart.render and chart.redraw
11899
	 */
11900
	cloneRenderTo: function (revert) {
11901
		var clone = this.renderToClone,
11902
			container = this.container;
11903
 
11904
		// Destroy the clone and bring the container back to the real renderTo div
11905
		if (revert) {
11906
			if (clone) {
11907
				this.renderTo.appendChild(container);
11908
				discardElement(clone);
11909
				delete this.renderToClone;
11910
			}
11911
 
11912
		// Set up the clone
11913
		} else {
11914
			if (container && container.parentNode === this.renderTo) {
11915
				this.renderTo.removeChild(container); // do not clone this
11916
			}
11917
			this.renderToClone = clone = this.renderTo.cloneNode(0);
11918
			css(clone, {
11919
				position: ABSOLUTE,
11920
				top: '-9999px',
11921
				display: 'block' // #833
11922
			});
11923
			if (clone.style.setProperty) { // #2631
11924
				clone.style.setProperty('display', 'block', 'important');
11925
			}
11926
			doc.body.appendChild(clone);
11927
			if (container) {
11928
				clone.appendChild(container);
11929
			}
11930
		}
11931
	},
11932
 
11933
	/**
11934
	 * Get the containing element, determine the size and create the inner container
11935
	 * div to hold the chart
11936
	 */
11937
	getContainer: function () {
11938
		var chart = this,
11939
			container,
11940
			options = chart.options,
11941
			optionsChart = options.chart,
11942
			chartWidth,
11943
			chartHeight,
11944
			renderTo,
11945
			indexAttrName = 'data-highcharts-chart',
11946
			oldChartIndex,
11947
			Ren,
11948
			containerId;
11949
 
11950
		chart.renderTo = renderTo = optionsChart.renderTo;
11951
		containerId = PREFIX + idCounter++;
11952
 
11953
		if (isString(renderTo)) {
11954
			chart.renderTo = renderTo = doc.getElementById(renderTo);
11955
		}
11956
 
11957
		// Display an error if the renderTo is wrong
11958
		if (!renderTo) {
11959
			error(13, true);
11960
		}
11961
 
11962
		// If the container already holds a chart, destroy it. The check for hasRendered is there
11963
		// because web pages that are saved to disk from the browser, will preserve the data-highcharts-chart
11964
		// attribute and the SVG contents, but not an interactive chart. So in this case,
11965
		// charts[oldChartIndex] will point to the wrong chart if any (#2609).
11966
		oldChartIndex = pInt(attr(renderTo, indexAttrName));
11967
		if (!isNaN(oldChartIndex) && charts[oldChartIndex] && charts[oldChartIndex].hasRendered) {
11968
			charts[oldChartIndex].destroy();
11969
		}
11970
 
11971
		// Make a reference to the chart from the div
11972
		attr(renderTo, indexAttrName, chart.index);
11973
 
11974
		// remove previous chart
11975
		renderTo.innerHTML = '';
11976
 
11977
		// If the container doesn't have an offsetWidth, it has or is a child of a node
11978
		// that has display:none. We need to temporarily move it out to a visible
11979
		// state to determine the size, else the legend and tooltips won't render
11980
		// properly. The allowClone option is used in sparklines as a micro optimization,
11981
		// saving about 1-2 ms each chart.
11982
		if (!optionsChart.skipClone && !renderTo.offsetWidth) {
11983
			chart.cloneRenderTo();
11984
		}
11985
 
11986
		// get the width and height
11987
		chart.getChartSize();
11988
		chartWidth = chart.chartWidth;
11989
		chartHeight = chart.chartHeight;
11990
 
11991
		// create the inner container
11992
		chart.container = container = createElement(DIV, {
11993
				className: PREFIX + 'container' +
11994
					(optionsChart.className ? ' ' + optionsChart.className : ''),
11995
				id: containerId
11996
			}, extend({
11997
				position: RELATIVE,
11998
				overflow: HIDDEN, // needed for context menu (avoid scrollbars) and
11999
					// content overflow in IE
12000
				width: chartWidth + PX,
12001
				height: chartHeight + PX,
12002
				textAlign: 'left',
12003
				lineHeight: 'normal', // #427
12004
				zIndex: 0, // #1072
12005
				'-webkit-tap-highlight-color': 'rgba(0,0,0,0)'
12006
			}, optionsChart.style),
12007
			chart.renderToClone || renderTo
12008
		);
12009
 
12010
		// cache the cursor (#1650)
12011
		chart._cursor = container.style.cursor;
12012
 
12013
		// Initialize the renderer
12014
		Ren = Highcharts[optionsChart.renderer] || Renderer;
12015
		chart.renderer = new Ren(
12016
			container,
12017
			chartWidth,
12018
			chartHeight,
12019
			optionsChart.style,
12020
			optionsChart.forExport,
12021
			options.exporting && options.exporting.allowHTML
12022
		);
12023
 
12024
		if (useCanVG) {
12025
			// If we need canvg library, extend and configure the renderer
12026
			// to get the tracker for translating mouse events
12027
			chart.renderer.create(chart, container, chartWidth, chartHeight);
12028
		}
12029
		// Add a reference to the charts index
12030
		chart.renderer.chartIndex = chart.index;
12031
	},
12032
 
12033
	/**
12034
	 * Calculate margins by rendering axis labels in a preliminary position. Title,
12035
	 * subtitle and legend have already been rendered at this stage, but will be
12036
	 * moved into their final positions
12037
	 */
12038
	getMargins: function (skipAxes) {
12039
		var chart = this,
12040
			spacing = chart.spacing,
12041
			margin = chart.margin,
12042
			titleOffset = chart.titleOffset;
12043
 
12044
		chart.resetMargins();
12045
 
12046
		// Adjust for title and subtitle
12047
		if (titleOffset && !defined(margin[0])) {
12048
			chart.plotTop = mathMax(chart.plotTop, titleOffset + chart.options.title.margin + spacing[0]);
12049
		}
12050
 
12051
		// Adjust for legend
12052
		chart.legend.adjustMargins(margin, spacing);
12053
 
12054
		// adjust for scroller
12055
		if (chart.extraBottomMargin) {
12056
			chart.marginBottom += chart.extraBottomMargin;
12057
		}
12058
		if (chart.extraTopMargin) {
12059
			chart.plotTop += chart.extraTopMargin;
12060
		}
12061
		if (!skipAxes) {
12062
			this.getAxisMargins();
12063
		}
12064
	},
12065
 
12066
	getAxisMargins: function () {
12067
 
12068
		var chart = this,
12069
			axisOffset = chart.axisOffset = [0, 0, 0, 0], // top, right, bottom, left
12070
			margin = chart.margin;
12071
 
12072
		// pre-render axes to get labels offset width
12073
		if (chart.hasCartesianSeries) {
12074
			each(chart.axes, function (axis) {
12075
				if (axis.visible) {
12076
					axis.getOffset();
12077
				}
12078
			});
12079
		}
12080
 
12081
		// Add the axis offsets
12082
		each(marginNames, function (m, side) {
12083
			if (!defined(margin[side])) {
12084
				chart[m] += axisOffset[side];
12085
			}
12086
		});
12087
 
12088
		chart.setChartSize();
12089
 
12090
	},
12091
 
12092
	/**
12093
	 * Resize the chart to its container if size is not explicitly set
12094
	 */
12095
	reflow: function (e) {
12096
		var chart = this,
12097
			optionsChart = chart.options.chart,
12098
			renderTo = chart.renderTo,
12099
			width = optionsChart.width || adapterRun(renderTo, 'width'),
12100
			height = optionsChart.height || adapterRun(renderTo, 'height'),
12101
			target = e ? e.target : win, // #805 - MooTools doesn't supply e
12102
			doReflow = function () {
12103
				if (chart.container) { // It may have been destroyed in the meantime (#1257)
12104
					chart.setSize(width, height, false);
12105
					chart.hasUserSize = null;
12106
				}
12107
			};
12108
 
12109
		// Width and height checks for display:none. Target is doc in IE8 and Opera,
12110
		// win in Firefox, Chrome and IE9.
12111
		if (!chart.hasUserSize && !chart.isPrinting && width && height && (target === win || target === doc)) { // #1093
12112
			if (width !== chart.containerWidth || height !== chart.containerHeight) {
12113
				clearTimeout(chart.reflowTimeout);
12114
				if (e) { // Called from window.resize
12115
					chart.reflowTimeout = setTimeout(doReflow, 100);
12116
				} else { // Called directly (#2224)
12117
					doReflow();
12118
				}
12119
			}
12120
			chart.containerWidth = width;
12121
			chart.containerHeight = height;
12122
		}
12123
	},
12124
 
12125
	/**
12126
	 * Add the event handlers necessary for auto resizing
12127
	 */
12128
	initReflow: function () {
12129
		var chart = this,
12130
			reflow = function (e) {
12131
				chart.reflow(e);
12132
			};
12133
 
12134
 
12135
		addEvent(win, 'resize', reflow);
12136
		addEvent(chart, 'destroy', function () {
12137
			removeEvent(win, 'resize', reflow);
12138
		});
12139
	},
12140
 
12141
	/**
12142
	 * Resize the chart to a given width and height
12143
	 * @param {Number} width
12144
	 * @param {Number} height
12145
	 * @param {Object|Boolean} animation
12146
	 */
12147
	setSize: function (width, height, animation) {
12148
		var chart = this,
12149
			chartWidth,
12150
			chartHeight,
12151
			fireEndResize,
12152
			renderer = chart.renderer,
12153
			globalAnimation;
12154
 
12155
		// Handle the isResizing counter
12156
		chart.isResizing += 1;
12157
		fireEndResize = function () {
12158
			if (chart) {
12159
				fireEvent(chart, 'endResize', null, function () {
12160
					chart.isResizing -= 1;
12161
				});
12162
			}
12163
		};
12164
 
12165
		// set the animation for the current process
12166
		setAnimation(animation, chart);
12167
 
12168
		chart.oldChartHeight = chart.chartHeight;
12169
		chart.oldChartWidth = chart.chartWidth;
12170
		if (defined(width)) {
12171
			chart.chartWidth = chartWidth = mathMax(0, mathRound(width));
12172
			chart.hasUserSize = !!chartWidth;
12173
		}
12174
		if (defined(height)) {
12175
			chart.chartHeight = chartHeight = mathMax(0, mathRound(height));
12176
		}
12177
 
12178
		// Resize the container with the global animation applied if enabled (#2503)
12179
		globalAnimation = renderer.globalAnimation;
12180
		(globalAnimation ? animate : css)(chart.container, {
12181
			width: chartWidth + PX,
12182
			height: chartHeight + PX
12183
		}, globalAnimation);
12184
 
12185
		chart.setChartSize(true);
12186
		renderer.setSize(chartWidth, chartHeight, animation);
12187
 
12188
		// handle axes
12189
		chart.maxTicks = null;
12190
		each(chart.axes, function (axis) {
12191
			axis.isDirty = true;
12192
			axis.setScale();
12193
		});
12194
 
12195
		// make sure non-cartesian series are also handled
12196
		each(chart.series, function (serie) {
12197
			serie.isDirty = true;
12198
		});
12199
 
12200
		chart.isDirtyLegend = true; // force legend redraw
12201
		chart.isDirtyBox = true; // force redraw of plot and chart border
12202
 
12203
		chart.layOutTitles(); // #2857
12204
		chart.getMargins();
12205
 
12206
		chart.redraw(animation);
12207
 
12208
 
12209
		chart.oldChartHeight = null;
12210
		fireEvent(chart, 'resize');
12211
 
12212
		// Fire endResize and set isResizing back. If animation is disabled, fire without delay
12213
		globalAnimation = renderer.globalAnimation; // Reassign it before using it, it may have changed since the top of this function.
12214
		if (globalAnimation === false) {
12215
			fireEndResize();
12216
		} else { // else set a timeout with the animation duration
12217
			setTimeout(fireEndResize, (globalAnimation && globalAnimation.duration) || 500);
12218
		}
12219
	},
12220
 
12221
	/**
12222
	 * Set the public chart properties. This is done before and after the pre-render
12223
	 * to determine margin sizes
12224
	 */
12225
	setChartSize: function (skipAxes) {
12226
		var chart = this,
12227
			inverted = chart.inverted,
12228
			renderer = chart.renderer,
12229
			chartWidth = chart.chartWidth,
12230
			chartHeight = chart.chartHeight,
12231
			optionsChart = chart.options.chart,
12232
			spacing = chart.spacing,
12233
			clipOffset = chart.clipOffset,
12234
			clipX,
12235
			clipY,
12236
			plotLeft,
12237
			plotTop,
12238
			plotWidth,
12239
			plotHeight,
12240
			plotBorderWidth;
12241
 
12242
		chart.plotLeft = plotLeft = mathRound(chart.plotLeft);
12243
		chart.plotTop = plotTop = mathRound(chart.plotTop);
12244
		chart.plotWidth = plotWidth = mathMax(0, mathRound(chartWidth - plotLeft - chart.marginRight));
12245
		chart.plotHeight = plotHeight = mathMax(0, mathRound(chartHeight - plotTop - chart.marginBottom));
12246
 
12247
		chart.plotSizeX = inverted ? plotHeight : plotWidth;
12248
		chart.plotSizeY = inverted ? plotWidth : plotHeight;
12249
 
12250
		chart.plotBorderWidth = optionsChart.plotBorderWidth || 0;
12251
 
12252
		// Set boxes used for alignment
12253
		chart.spacingBox = renderer.spacingBox = {
12254
			x: spacing[3],
12255
			y: spacing[0],
12256
			width: chartWidth - spacing[3] - spacing[1],
12257
			height: chartHeight - spacing[0] - spacing[2]
12258
		};
12259
		chart.plotBox = renderer.plotBox = {
12260
			x: plotLeft,
12261
			y: plotTop,
12262
			width: plotWidth,
12263
			height: plotHeight
12264
		};
12265
 
12266
		plotBorderWidth = 2 * mathFloor(chart.plotBorderWidth / 2);
12267
		clipX = mathCeil(mathMax(plotBorderWidth, clipOffset[3]) / 2);
12268
		clipY = mathCeil(mathMax(plotBorderWidth, clipOffset[0]) / 2);
12269
		chart.clipBox = {
12270
			x: clipX,
12271
			y: clipY,
12272
			width: mathFloor(chart.plotSizeX - mathMax(plotBorderWidth, clipOffset[1]) / 2 - clipX),
12273
			height: mathMax(0, mathFloor(chart.plotSizeY - mathMax(plotBorderWidth, clipOffset[2]) / 2 - clipY))
12274
		};
12275
 
12276
		if (!skipAxes) {
12277
			each(chart.axes, function (axis) {
12278
				axis.setAxisSize();
12279
				axis.setAxisTranslation();
12280
			});
12281
		}
12282
	},
12283
 
12284
	/**
12285
	 * Initial margins before auto size margins are applied
12286
	 */
12287
	resetMargins: function () {
12288
		var chart = this;
12289
 
12290
		each(marginNames, function (m, side) {
12291
			chart[m] = pick(chart.margin[side], chart.spacing[side]);
12292
		});
12293
		chart.axisOffset = [0, 0, 0, 0]; // top, right, bottom, left
12294
		chart.clipOffset = [0, 0, 0, 0];
12295
	},
12296
 
12297
	/**
12298
	 * Draw the borders and backgrounds for chart and plot area
12299
	 */
12300
	drawChartBox: function () {
12301
		var chart = this,
12302
			optionsChart = chart.options.chart,
12303
			renderer = chart.renderer,
12304
			chartWidth = chart.chartWidth,
12305
			chartHeight = chart.chartHeight,
12306
			chartBackground = chart.chartBackground,
12307
			plotBackground = chart.plotBackground,
12308
			plotBorder = chart.plotBorder,
12309
			plotBGImage = chart.plotBGImage,
12310
			chartBorderWidth = optionsChart.borderWidth || 0,
12311
			chartBackgroundColor = optionsChart.backgroundColor,
12312
			plotBackgroundColor = optionsChart.plotBackgroundColor,
12313
			plotBackgroundImage = optionsChart.plotBackgroundImage,
12314
			plotBorderWidth = optionsChart.plotBorderWidth || 0,
12315
			mgn,
12316
			bgAttr,
12317
			plotLeft = chart.plotLeft,
12318
			plotTop = chart.plotTop,
12319
			plotWidth = chart.plotWidth,
12320
			plotHeight = chart.plotHeight,
12321
			plotBox = chart.plotBox,
12322
			clipRect = chart.clipRect,
12323
			clipBox = chart.clipBox;
12324
 
12325
		// Chart area
12326
		mgn = chartBorderWidth + (optionsChart.shadow ? 8 : 0);
12327
 
12328
		if (chartBorderWidth || chartBackgroundColor) {
12329
			if (!chartBackground) {
12330
 
12331
				bgAttr = {
12332
					fill: chartBackgroundColor || NONE
12333
				};
12334
				if (chartBorderWidth) { // #980
12335
					bgAttr.stroke = optionsChart.borderColor;
12336
					bgAttr['stroke-width'] = chartBorderWidth;
12337
				}
12338
				chart.chartBackground = renderer.rect(mgn / 2, mgn / 2, chartWidth - mgn, chartHeight - mgn,
12339
						optionsChart.borderRadius, chartBorderWidth)
12340
					.attr(bgAttr)
12341
					.addClass(PREFIX + 'background')
12342
					.add()
12343
					.shadow(optionsChart.shadow);
12344
 
12345
			} else { // resize
12346
				chartBackground.animate(
12347
					chartBackground.crisp({ width: chartWidth - mgn, height: chartHeight - mgn })
12348
				);
12349
			}
12350
		}
12351
 
12352
 
12353
		// Plot background
12354
		if (plotBackgroundColor) {
12355
			if (!plotBackground) {
12356
				chart.plotBackground = renderer.rect(plotLeft, plotTop, plotWidth, plotHeight, 0)
12357
					.attr({
12358
						fill: plotBackgroundColor
12359
					})
12360
					.add()
12361
					.shadow(optionsChart.plotShadow);
12362
			} else {
12363
				plotBackground.animate(plotBox);
12364
			}
12365
		}
12366
		if (plotBackgroundImage) {
12367
			if (!plotBGImage) {
12368
				chart.plotBGImage = renderer.image(plotBackgroundImage, plotLeft, plotTop, plotWidth, plotHeight)
12369
					.add();
12370
			} else {
12371
				plotBGImage.animate(plotBox);
12372
			}
12373
		}
12374
 
12375
		// Plot clip
12376
		if (!clipRect) {
12377
			chart.clipRect = renderer.clipRect(clipBox);
12378
		} else {
12379
			clipRect.animate({
12380
				width: clipBox.width,
12381
				height: clipBox.height
12382
			});
12383
		}
12384
 
12385
		// Plot area border
12386
		if (plotBorderWidth) {
12387
			if (!plotBorder) {
12388
				chart.plotBorder = renderer.rect(plotLeft, plotTop, plotWidth, plotHeight, 0, -plotBorderWidth)
12389
					.attr({
12390
						stroke: optionsChart.plotBorderColor,
12391
						'stroke-width': plotBorderWidth,
12392
						fill: NONE,
12393
						zIndex: 1
12394
					})
12395
					.add();
12396
			} else {
12397
				plotBorder.animate(
12398
					plotBorder.crisp({ x: plotLeft, y: plotTop, width: plotWidth, height: plotHeight, strokeWidth: -plotBorderWidth }) //#3282 plotBorder should be negative
12399
				);
12400
			}
12401
		}
12402
 
12403
		// reset
12404
		chart.isDirtyBox = false;
12405
	},
12406
 
12407
	/**
12408
	 * Detect whether a certain chart property is needed based on inspecting its options
12409
	 * and series. This mainly applies to the chart.invert property, and in extensions to
12410
	 * the chart.angular and chart.polar properties.
12411
	 */
12412
	propFromSeries: function () {
12413
		var chart = this,
12414
			optionsChart = chart.options.chart,
12415
			klass,
12416
			seriesOptions = chart.options.series,
12417
			i,
12418
			value;
12419
 
12420
 
12421
		each(['inverted', 'angular', 'polar'], function (key) {
12422
 
12423
			// The default series type's class
12424
			klass = seriesTypes[optionsChart.type || optionsChart.defaultSeriesType];
12425
 
12426
			// Get the value from available chart-wide properties
12427
			value = (
12428
				chart[key] || // 1. it is set before
12429
				optionsChart[key] || // 2. it is set in the options
12430
				(klass && klass.prototype[key]) // 3. it's default series class requires it
12431
			);
12432
 
12433
			// 4. Check if any the chart's series require it
12434
			i = seriesOptions && seriesOptions.length;
12435
			while (!value && i--) {
12436
				klass = seriesTypes[seriesOptions[i].type];
12437
				if (klass && klass.prototype[key]) {
12438
					value = true;
12439
				}
12440
			}
12441
 
12442
			// Set the chart property
12443
			chart[key] = value;
12444
		});
12445
 
12446
	},
12447
 
12448
	/**
12449
	 * Link two or more series together. This is done initially from Chart.render,
12450
	 * and after Chart.addSeries and Series.remove.
12451
	 */
12452
	linkSeries: function () {
12453
		var chart = this,
12454
			chartSeries = chart.series;
12455
 
12456
		// Reset links
12457
		each(chartSeries, function (series) {
12458
			series.linkedSeries.length = 0;
12459
		});
12460
 
12461
		// Apply new links
12462
		each(chartSeries, function (series) {
12463
			var linkedTo = series.options.linkedTo;
12464
			if (isString(linkedTo)) {
12465
				if (linkedTo === ':previous') {
12466
					linkedTo = chart.series[series.index - 1];
12467
				} else {
12468
					linkedTo = chart.get(linkedTo);
12469
				}
12470
				if (linkedTo) {
12471
					linkedTo.linkedSeries.push(series);
12472
					series.linkedParent = linkedTo;
12473
					series.visible = pick(series.options.visible, linkedTo.options.visible, series.visible); // #3879
12474
				}
12475
			}
12476
		});
12477
	},
12478
 
12479
	/**
12480
	 * Render series for the chart
12481
	 */
12482
	renderSeries: function () {
12483
		each(this.series, function (serie) {
12484
			serie.translate();
12485
			serie.render();
12486
		});
12487
	},
12488
 
12489
	/**
12490
	 * Render labels for the chart
12491
	 */
12492
	renderLabels: function () {
12493
		var chart = this,
12494
			labels = chart.options.labels;
12495
		if (labels.items) {
12496
			each(labels.items, function (label) {
12497
				var style = extend(labels.style, label.style),
12498
					x = pInt(style.left) + chart.plotLeft,
12499
					y = pInt(style.top) + chart.plotTop + 12;
12500
 
12501
				// delete to prevent rewriting in IE
12502
				delete style.left;
12503
				delete style.top;
12504
 
12505
				chart.renderer.text(
12506
					label.html,
12507
					x,
12508
					y
12509
				)
12510
				.attr({ zIndex: 2 })
12511
				.css(style)
12512
				.add();
12513
 
12514
			});
12515
		}
12516
	},
12517
 
12518
	/**
12519
	 * Render all graphics for the chart
12520
	 */
12521
	render: function () {
12522
		var chart = this,
12523
			axes = chart.axes,
12524
			renderer = chart.renderer,
12525
			options = chart.options,
12526
			tempWidth,
12527
			tempHeight,
12528
			redoHorizontal,
12529
			redoVertical;
12530
 
12531
		// Title
12532
		chart.setTitle();
12533
 
12534
 
12535
		// Legend
12536
		chart.legend = new Legend(chart, options.legend);
12537
 
12538
		// Get stacks
12539
		if (chart.getStacks) {
12540
			chart.getStacks();
12541
		}
12542
 
12543
		// Get chart margins
12544
		chart.getMargins(true);
12545
		chart.setChartSize();
12546
 
12547
		// Record preliminary dimensions for later comparison
12548
		tempWidth = chart.plotWidth;
12549
		tempHeight = chart.plotHeight = chart.plotHeight - 13; // 13 is the most common height of X axis labels
12550
 
12551
		// Get margins by pre-rendering axes
12552
		each(axes, function (axis) {
12553
			axis.setScale();
12554
		});
12555
		chart.getAxisMargins();
12556
 
12557
		// If the plot area size has changed significantly, calculate tick positions again
12558
		redoHorizontal = tempWidth / chart.plotWidth > 1.1;
12559
		redoVertical = tempHeight / chart.plotHeight > 1.1;
12560
 
12561
		if (redoHorizontal || redoVertical) {
12562
 
12563
			chart.maxTicks = null; // reset for second pass
12564
			each(axes, function (axis) {
12565
				if ((axis.horiz && redoHorizontal) || (!axis.horiz && redoVertical)) {
12566
					axis.setTickInterval(true); // update to reflect the new margins
12567
				}
12568
			});
12569
			chart.getMargins(); // second pass to check for new labels
12570
		}
12571
 
12572
		// Draw the borders and backgrounds
12573
		chart.drawChartBox();
12574
 
12575
 
12576
		// Axes
12577
		if (chart.hasCartesianSeries) {
12578
			each(axes, function (axis) {
12579
				if (axis.visible) {
12580
					axis.render();
12581
				}
12582
			});
12583
		}
12584
 
12585
		// The series
12586
		if (!chart.seriesGroup) {
12587
			chart.seriesGroup = renderer.g('series-group')
12588
				.attr({ zIndex: 3 })
12589
				.add();
12590
		}
12591
		chart.renderSeries();
12592
 
12593
		// Labels
12594
		chart.renderLabels();
12595
 
12596
		// Credits
12597
		chart.showCredits(options.credits);
12598
 
12599
		// Set flag
12600
		chart.hasRendered = true;
12601
 
12602
	},
12603
 
12604
	/**
12605
	 * Show chart credits based on config options
12606
	 */
12607
	showCredits: function (credits) {
12608
		if (credits.enabled && !this.credits) {
12609
			this.credits = this.renderer.text(
12610
				credits.text,
12611
				0,
12612
 
12613
			)
12614
			.on('click', function () {
12615
				if (credits.href) {
12616
					location.href = credits.href;
12617
				}
12618
			})
12619
			.attr({
12620
				align: credits.position.align,
12621
				zIndex: 8
12622
			})
12623
			.css(credits.style)
12624
			.add()
12625
			.align(credits.position);
12626
		}
12627
	},
12628
 
12629
	/**
12630
	 * Clean up memory usage
12631
	 */
12632
	destroy: function () {
12633
		var chart = this,
12634
			axes = chart.axes,
12635
			series = chart.series,
12636
			container = chart.container,
12637
			i,
12638
			parentNode = container && container.parentNode;
12639
 
12640
		// fire the chart.destoy event
12641
		fireEvent(chart, 'destroy');
12642
 
12643
		// Delete the chart from charts lookup array
12644
		charts[chart.index] = UNDEFINED;
12645
		chartCount--;
12646
		chart.renderTo.removeAttribute('data-highcharts-chart');
12647
 
12648
		// remove events
12649
		removeEvent(chart);
12650
 
12651
		// ==== Destroy collections:
12652
		// Destroy axes
12653
		i = axes.length;
12654
		while (i--) {
12655
			axes[i] = axes[i].destroy();
12656
		}
12657
 
12658
		// Destroy each series
12659
		i = series.length;
12660
		while (i--) {
12661
			series[i] = series[i].destroy();
12662
		}
12663
 
12664
		// ==== Destroy chart properties:
12665
		each(['title', 'subtitle', 'chartBackground', 'plotBackground', 'plotBGImage',
12666
				'plotBorder', 'seriesGroup', 'clipRect', 'credits', 'pointer', 'scroller',
12667
				'rangeSelector', 'legend', 'resetZoomButton', 'tooltip', 'renderer'], function (name) {
12668
			var prop = chart[name];
12669
 
12670
			if (prop && prop.destroy) {
12671
				chart[name] = prop.destroy();
12672
			}
12673
		});
12674
 
12675
		// remove container and all SVG
12676
		if (container) { // can break in IE when destroyed before finished loading
12677
			container.innerHTML = '';
12678
			removeEvent(container);
12679
			if (parentNode) {
12680
				discardElement(container);
12681
			}
12682
 
12683
		}
12684
 
12685
		// clean it all up
12686
		for (i in chart) {
12687
			delete chart[i];
12688
		}
12689
 
12690
	},
12691
 
12692
 
12693
	/**
12694
	 * VML namespaces can't be added until after complete. Listening
12695
	 * for Perini's doScroll hack is not enough.
12696
	 */
12697
	isReadyToRender: function () {
12698
		var chart = this;
12699
 
12700
		// Note: in spite of JSLint's complaints, win == win.top is required
12701
		/*jslint eqeq: true*/
12702
		if ((!hasSVG && (win == win.top && doc.readyState !== 'complete')) || (useCanVG && !win.canvg)) {
12703
		/*jslint eqeq: false*/
12704
			if (useCanVG) {
12705
				// Delay rendering until canvg library is downloaded and ready
12706
				CanVGController.push(function () { chart.firstRender(); }, chart.options.global.canvasToolsURL);
12707
			} else {
12708
				doc.attachEvent('onreadystatechange', function () {
12709
					doc.detachEvent('onreadystatechange', chart.firstRender);
12710
					if (doc.readyState === 'complete') {
12711
						chart.firstRender();
12712
					}
12713
				});
12714
			}
12715
			return false;
12716
		}
12717
		return true;
12718
	},
12719
 
12720
	/**
12721
	 * Prepare for first rendering after all data are loaded
12722
	 */
12723
	firstRender: function () {
12724
		var chart = this,
12725
			options = chart.options,
12726
			callback = chart.callback;
12727
 
12728
		// Check whether the chart is ready to render
12729
		if (!chart.isReadyToRender()) {
12730
			return;
12731
		}
12732
 
12733
		// Create the container
12734
		chart.getContainer();
12735
 
12736
		// Run an early event after the container and renderer are established
12737
		fireEvent(chart, 'init');
12738
 
12739
 
12740
		chart.resetMargins();
12741
		chart.setChartSize();
12742
 
12743
		// Set the common chart properties (mainly invert) from the given series
12744
		chart.propFromSeries();
12745
 
12746
		// get axes
12747
		chart.getAxes();
12748
 
12749
		// Initialize the series
12750
		each(options.series || [], function (serieOptions) {
12751
			chart.initSeries(serieOptions);
12752
		});
12753
 
12754
		chart.linkSeries();
12755
 
12756
		// Run an event after axes and series are initialized, but before render. At this stage,
12757
		// the series data is indexed and cached in the xData and yData arrays, so we can access
12758
		// those before rendering. Used in Highstock.
12759
		fireEvent(chart, 'beforeRender');
12760
 
12761
		// depends on inverted and on margins being set
12762
		if (Highcharts.Pointer) {
12763
			chart.pointer = new Pointer(chart, options);
12764
		}
12765
 
12766
		chart.render();
12767
 
12768
		// add canvas
12769
		chart.renderer.draw();
12770
		// run callbacks
12771
		if (callback) {
12772
			callback.apply(chart, [chart]);
12773
		}
12774
		each(chart.callbacks, function (fn) {
12775
			if (chart.index !== UNDEFINED) { // Chart destroyed in its own callback (#3600)
12776
				fn.apply(chart, [chart]);
12777
			}
12778
		});
12779
 
12780
		// Fire the load event
12781
		fireEvent(chart, 'load');
12782
 
12783
		// If the chart was rendered outside the top container, put it back in (#3679)
12784
		chart.cloneRenderTo(true);
12785
 
12786
	},
12787
 
12788
	/**
12789
	* Creates arrays for spacing and margin from given options.
12790
	*/
12791
	splashArray: function (target, options) {
12792
		var oVar = options[target],
12793
			tArray = isObject(oVar) ? oVar : [oVar, oVar, oVar, oVar];
12794
 
12795
		return [pick(options[target + 'Top'], tArray[0]),
12796
				pick(options[target + 'Right'], tArray[1]),
12797
				pick(options[target + 'Bottom'], tArray[2]),
12798
				pick(options[target + 'Left'], tArray[3])];
12799
	}
12800
}; // end Chart
12801
 
12802
var CenteredSeriesMixin = Highcharts.CenteredSeriesMixin = {
12803
	/**
12804
	 * Get the center of the pie based on the size and center options relative to the
12805
	 * plot area. Borrowed by the polar and gauge series types.
12806
	 */
12807
	getCenter: function () {
12808
 
12809
		var options = this.options,
12810
			chart = this.chart,
12811
			slicingRoom = 2 * (options.slicedOffset || 0),
12812
			handleSlicingRoom,
12813
			plotWidth = chart.plotWidth - 2 * slicingRoom,
12814
			plotHeight = chart.plotHeight - 2 * slicingRoom,
12815
			centerOption = options.center,
12816
			positions = [pick(centerOption[0], '50%'), pick(centerOption[1], '50%'), options.size || '100%', options.innerSize || 0],
12817
			smallestSize = mathMin(plotWidth, plotHeight),
12818
			i,
12819
			value;
12820
 
12821
		for (i = 0; i < 4; ++i) {
12822
			value = positions[i];
12823
			handleSlicingRoom = i < 2 || (i === 2 && /%$/.test(value));
12824
 
12825
			// i == 0: centerX, relative to width
12826
			// i == 1: centerY, relative to height
12827
			// i == 2: size, relative to smallestSize
12828
			// i == 3: innerSize, relative to size
12829
			positions[i] = relativeLength(value, [plotWidth, plotHeight, smallestSize, positions[2]][i]) +
12830
				(handleSlicingRoom ? slicingRoom : 0);
12831
 
12832
		}
12833
		// innerSize cannot be larger than size (#3632)
12834
		if (positions[3] > positions[2]) {
12835
			positions[3] = positions[2];
12836
		}
12837
		return positions;
12838
	}
12839
};
12840
 
12841
/**
12842
 * The Point object and prototype. Inheritable and used as base for PiePoint
12843
 */
12844
var Point = function () {};
12845
Point.prototype = {
12846
 
12847
	/**
12848
	 * Initialize the point
12849
	 * @param {Object} series The series object containing this point
12850
	 * @param {Object} options The data in either number, array or object format
12851
	 */
12852
	init: function (series, options, x) {
12853
 
12854
		var point = this,
12855
			colors;
12856
		point.series = series;
12857
		point.color = series.color; // #3445
12858
		point.applyOptions(options, x);
12859
		point.pointAttr = {};
12860
 
12861
		if (series.options.colorByPoint) {
12862
			colors = series.options.colors || series.chart.options.colors;
12863
			point.color = point.color || colors[series.colorCounter++];
12864
			// loop back to zero
12865
			if (series.colorCounter === colors.length) {
12866
				series.colorCounter = 0;
12867
			}
12868
		}
12869
 
12870
		series.chart.pointCount++;
12871
		return point;
12872
	},
12873
	/**
12874
	 * Apply the options containing the x and y data and possible some extra properties.
12875
	 * This is called on point init or from point.update.
12876
	 *
12877
	 * @param {Object} options
12878
	 */
12879
	applyOptions: function (options, x) {
12880
		var point = this,
12881
			series = point.series,
12882
			pointValKey = series.options.pointValKey || series.pointValKey;
12883
 
12884
		options = Point.prototype.optionsToObject.call(this, options);
12885
 
12886
		// copy options directly to point
12887
		extend(point, options);
12888
		point.options = point.options ? extend(point.options, options) : options;
12889
 
12890
		// For higher dimension series types. For instance, for ranges, point.y is mapped to point.low.
12891
		if (pointValKey) {
12892
			point.y = point[pointValKey];
12893
		}
12894
 
12895
		// If no x is set by now, get auto incremented value. All points must have an
12896
		// x value, however the y value can be null to create a gap in the series
12897
		if (point.x === UNDEFINED && series) {
12898
			point.x = x === UNDEFINED ? series.autoIncrement() : x;
12899
		}
12900
 
12901
		return point;
12902
	},
12903
 
12904
	/**
12905
	 * Transform number or array configs into objects
12906
	 */
12907
	optionsToObject: function (options) {
12908
		var ret = {},
12909
			series = this.series,
12910
			keys = series.options.keys,
12911
			pointArrayMap = keys || series.pointArrayMap || ['y'],
12912
			valueCount = pointArrayMap.length,
12913
			firstItemType,
12914
			i = 0,
12915
			j = 0;
12916
 
12917
		if (typeof options === 'number' || options === null) {
12918
			ret[pointArrayMap[0]] = options;
12919
 
12920
		} else if (isArray(options)) {
12921
			// with leading x value
12922
			if (!keys && options.length > valueCount) {
12923
				firstItemType = typeof options[0];
12924
				if (firstItemType === 'string') {
12925
					ret.name = options[0];
12926
				} else if (firstItemType === 'number') {
12927
					ret.x = options[0];
12928
				}
12929
				i++;
12930
			}
12931
			while (j < valueCount) {
12932
				if (!keys || options[i] !== undefined) { // Skip undefined positions for keys
12933
					ret[pointArrayMap[j]] = options[i];
12934
				}
12935
				i++;
12936
				j++;
12937
			}
12938
		} else if (typeof options === 'object') {
12939
			ret = options;
12940
 
12941
			// This is the fastest way to detect if there are individual point dataLabels that need
12942
			// to be considered in drawDataLabels. These can only occur in object configs.
12943
			if (options.dataLabels) {
12944
				series._hasPointLabels = true;
12945
			}
12946
 
12947
			// Same approach as above for markers
12948
			if (options.marker) {
12949
				series._hasPointMarkers = true;
12950
			}
12951
		}
12952
		return ret;
12953
	},
12954
 
12955
	/**
12956
	 * Destroy a point to clear memory. Its reference still stays in series.data.
12957
	 */
12958
	destroy: function () {
12959
		var point = this,
12960
			series = point.series,
12961
			chart = series.chart,
12962
			hoverPoints = chart.hoverPoints,
12963
			prop;
12964
 
12965
		chart.pointCount--;
12966
 
12967
		if (hoverPoints) {
12968
			point.setState();
12969
			erase(hoverPoints, point);
12970
			if (!hoverPoints.length) {
12971
				chart.hoverPoints = null;
12972
			}
12973
 
12974
		}
12975
		if (point === chart.hoverPoint) {
12976
			point.onMouseOut();
12977
		}
12978
 
12979
		// remove all events
12980
		if (point.graphic || point.dataLabel) { // removeEvent and destroyElements are performance expensive
12981
			removeEvent(point);
12982
			point.destroyElements();
12983
		}
12984
 
12985
		if (point.legendItem) { // pies have legend items
12986
			chart.legend.destroyItem(point);
12987
		}
12988
 
12989
		for (prop in point) {
12990
			point[prop] = null;
12991
		}
12992
 
12993
 
12994
	},
12995
 
12996
	/**
12997
	 * Destroy SVG elements associated with the point
12998
	 */
12999
	destroyElements: function () {
13000
		var point = this,
13001
			props = ['graphic', 'dataLabel', 'dataLabelUpper', 'connector', 'shadowGroup'],
13002
			prop,
13003
			i = 6;
13004
		while (i--) {
13005
			prop = props[i];
13006
			if (point[prop]) {
13007
				point[prop] = point[prop].destroy();
13008
			}
13009
		}
13010
	},
13011
 
13012
	/**
13013
	 * Return the configuration hash needed for the data label and tooltip formatters
13014
	 */
13015
	getLabelConfig: function () {
13016
		return {
13017
			x: this.category,
13018
			y: this.y,
13019
			color: this.color,
13020
			key: this.name || this.category,
13021
			series: this.series,
13022
			point: this,
13023
			percentage: this.percentage,
13024
			total: this.total || this.stackTotal
13025
		};
13026
	},
13027
 
13028
	/**
13029
	 * Extendable method for formatting each point's tooltip line
13030
	 *
13031
	 * @return {String} A string to be concatenated in to the common tooltip text
13032
	 */
13033
	tooltipFormatter: function (pointFormat) {
13034
 
13035
		// Insert options for valueDecimals, valuePrefix, and valueSuffix
13036
		var series = this.series,
13037
			seriesTooltipOptions = series.tooltipOptions,
13038
			valueDecimals = pick(seriesTooltipOptions.valueDecimals, ''),
13039
			valuePrefix = seriesTooltipOptions.valuePrefix || '',
13040
			valueSuffix = seriesTooltipOptions.valueSuffix || '';
13041
 
13042
		// Loop over the point array map and replace unformatted values with sprintf formatting markup
13043
		each(series.pointArrayMap || ['y'], function (key) {
13044
			key = '{point.' + key; // without the closing bracket
13045
			if (valuePrefix || valueSuffix) {
13046
				pointFormat = pointFormat.replace(key + '}', valuePrefix + key + '}' + valueSuffix);
13047
			}
13048
			pointFormat = pointFormat.replace(key + '}', key + ':,.' + valueDecimals + 'f}');
13049
		});
13050
 
13051
		return format(pointFormat, {
13052
			point: this,
13053
			series: this.series
13054
		});
13055
	},
13056
 
13057
	/**
13058
	 * Fire an event on the Point object. Must not be renamed to fireEvent, as this
13059
	 * causes a name clash in MooTools
13060
	 * @param {String} eventType
13061
	 * @param {Object} eventArgs Additional event arguments
13062
	 * @param {Function} defaultFunction Default event handler
13063
	 */
13064
	firePointEvent: function (eventType, eventArgs, defaultFunction) {
13065
		var point = this,
13066
			series = this.series,
13067
			seriesOptions = series.options;
13068
 
13069
		// load event handlers on demand to save time on mouseover/out
13070
		if (seriesOptions.point.events[eventType] || (point.options && point.options.events && point.options.events[eventType])) {
13071
			this.importEvents();
13072
		}
13073
 
13074
		// add default handler if in selection mode
13075
		if (eventType === 'click' && seriesOptions.allowPointSelect) {
13076
			defaultFunction = function (event) {
13077
				// Control key is for Windows, meta (= Cmd key) for Mac, Shift for Opera
13078
				if (point.select) { // Could be destroyed by prior event handlers (#2911)
13079
					point.select(null, event.ctrlKey || event.metaKey || event.shiftKey);
13080
				}
13081
			};
13082
		}
13083
 
13084
		fireEvent(this, eventType, eventArgs, defaultFunction);
13085
	},
13086
	visible: true
13087
};/**
13088
 * @classDescription The base function which all other series types inherit from. The data in the series is stored
13089
 * in various arrays.
13090
 *
13091
 * - First, series.options.data contains all the original config options for
13092
 * each point whether added by options or methods like series.addPoint.
13093
 * - Next, series.data contains those values converted to points, but in case the series data length
13094
 * exceeds the cropThreshold, or if the data is grouped, series.data doesn't contain all the points. It
13095
 * only contains the points that have been created on demand.
13096
 * - Then there's series.points that contains all currently visible point objects. In case of cropping,
13097
 * the cropped-away points are not part of this array. The series.points array starts at series.cropStart
13098
 * compared to series.data and series.options.data. If however the series data is grouped, these can't
13099
 * be correlated one to one.
13100
 * - series.xData and series.processedXData contain clean x values, equivalent to series.data and series.points.
13101
 * - series.yData and series.processedYData contain clean x values, equivalent to series.data and series.points.
13102
 *
13103
 * @param {Object} chart
13104
 * @param {Object} options
13105
 */
13106
var Series = Highcharts.Series = function () {};
13107
 
13108
Series.prototype = {
13109
 
13110
	isCartesian: true,
13111
	type: 'line',
13112
	pointClass: Point,
13113
	sorted: true, // requires the data to be sorted
13114
	requireSorting: true,
13115
	pointAttrToOptions: { // mapping between SVG attributes and the corresponding options
13116
		stroke: 'lineColor',
13117
		'stroke-width': 'lineWidth',
13118
		fill: 'fillColor',
13119
		r: 'radius'
13120
	},
13121
	directTouch: false,
13122
	axisTypes: ['xAxis', 'yAxis'],
13123
	colorCounter: 0,
13124
	parallelArrays: ['x', 'y'], // each point's x and y values are stored in this.xData and this.yData
13125
	init: function (chart, options) {
13126
		var series = this,
13127
			eventType,
13128
			events,
13129
			chartSeries = chart.series,
13130
			sortByIndex = function (a, b) {
13131
				return pick(a.options.index, a._i) - pick(b.options.index, b._i);
13132
			};
13133
 
13134
		series.chart = chart;
13135
		series.options = options = series.setOptions(options); // merge with plotOptions
13136
		series.linkedSeries = [];
13137
 
13138
		// bind the axes
13139
		series.bindAxes();
13140
 
13141
		// set some variables
13142
		extend(series, {
13143
			name: options.name,
13144
			state: NORMAL_STATE,
13145
			pointAttr: {},
13146
			visible: options.visible !== false, // true by default
13147
			selected: options.selected === true // false by default
13148
		});
13149
 
13150
		// special
13151
		if (useCanVG) {
13152
			options.animation = false;
13153
		}
13154
 
13155
		// register event listeners
13156
		events = options.events;
13157
		for (eventType in events) {
13158
			addEvent(series, eventType, events[eventType]);
13159
		}
13160
		if (
13161
			(events && events.click) ||
13162
			(options.point && options.point.events && options.point.events.click) ||
13163
			options.allowPointSelect
13164
		) {
13165
			chart.runTrackerClick = true;
13166
		}
13167
 
13168
		series.getColor();
13169
		series.getSymbol();
13170
 
13171
		// Set the data
13172
		each(series.parallelArrays, function (key) {
13173
			series[key + 'Data'] = [];
13174
		});
13175
		series.setData(options.data, false);
13176
 
13177
		// Mark cartesian
13178
		if (series.isCartesian) {
13179
			chart.hasCartesianSeries = true;
13180
		}
13181
 
13182
		// Register it in the chart
13183
		chartSeries.push(series);
13184
		series._i = chartSeries.length - 1;
13185
 
13186
		// Sort series according to index option (#248, #1123, #2456)
13187
		stableSort(chartSeries, sortByIndex);
13188
		if (this.yAxis) {
13189
			stableSort(this.yAxis.series, sortByIndex);
13190
		}
13191
 
13192
		each(chartSeries, function (series, i) {
13193
			series.index = i;
13194
			series.name = series.name || 'Series ' + (i + 1);
13195
		});
13196
 
13197
	},
13198
 
13199
	/**
13200
	 * Set the xAxis and yAxis properties of cartesian series, and register the series
13201
	 * in the axis.series array
13202
	 */
13203
	bindAxes: function () {
13204
		var series = this,
13205
			seriesOptions = series.options,
13206
			chart = series.chart,
13207
			axisOptions;
13208
 
13209
		each(series.axisTypes || [], function (AXIS) { // repeat for xAxis and yAxis
13210
 
13211
			each(chart[AXIS], function (axis) { // loop through the chart's axis objects
13212
				axisOptions = axis.options;
13213
 
13214
				// apply if the series xAxis or yAxis option mathches the number of the
13215
				// axis, or if undefined, use the first axis
13216
				if ((seriesOptions[AXIS] === axisOptions.index) ||
13217
						(seriesOptions[AXIS] !== UNDEFINED && seriesOptions[AXIS] === axisOptions.id) ||
13218
						(seriesOptions[AXIS] === UNDEFINED && axisOptions.index === 0)) {
13219
 
13220
					// register this series in the axis.series lookup
13221
					axis.series.push(series);
13222
 
13223
					// set this series.xAxis or series.yAxis reference
13224
					series[AXIS] = axis;
13225
 
13226
					// mark dirty for redraw
13227
					axis.isDirty = true;
13228
				}
13229
			});
13230
 
13231
			// The series needs an X and an Y axis
13232
			if (!series[AXIS] && series.optionalAxis !== AXIS) {
13233
				error(18, true);
13234
			}
13235
 
13236
		});
13237
	},
13238
 
13239
	/**
13240
	 * For simple series types like line and column, the data values are held in arrays like
13241
	 * xData and yData for quick lookup to find extremes and more. For multidimensional series
13242
	 * like bubble and map, this can be extended with arrays like zData and valueData by
13243
	 * adding to the series.parallelArrays array.
13244
	 */
13245
	updateParallelArrays: function (point, i) {
13246
		var series = point.series,
13247
			args = arguments,
13248
			fn = typeof i === 'number' ?
13249
				 // Insert the value in the given position
13250
				function (key) {
13251
					var val = key === 'y' && series.toYData ? series.toYData(point) : point[key];
13252
					series[key + 'Data'][i] = val;
13253
				} :
13254
				// Apply the method specified in i with the following arguments as arguments
13255
				function (key) {
13256
					Array.prototype[i].apply(series[key + 'Data'], Array.prototype.slice.call(args, 2));
13257
				};
13258
 
13259
		each(series.parallelArrays, fn);
13260
	},
13261
 
13262
	/**
13263
	 * Return an auto incremented x value based on the pointStart and pointInterval options.
13264
	 * This is only used if an x value is not given for the point that calls autoIncrement.
13265
	 */
13266
	autoIncrement: function () {
13267
 
13268
		var options = this.options,
13269
			xIncrement = this.xIncrement,
13270
			date,
13271
			pointInterval,
13272
			pointIntervalUnit = options.pointIntervalUnit;
13273
 
13274
		xIncrement = pick(xIncrement, options.pointStart, 0);
13275
 
13276
		this.pointInterval = pointInterval = pick(this.pointInterval, options.pointInterval, 1);
13277
 
13278
		// Added code for pointInterval strings
13279
		if (pointIntervalUnit === 'month' || pointIntervalUnit === 'year') {
13280
			date = new Date(xIncrement);
13281
			date = (pointIntervalUnit === 'month') ?
13282
				+date[setMonth](date[getMonth]() + pointInterval) :
13283
				+date[setFullYear](date[getFullYear]() + pointInterval);
13284
			pointInterval = date - xIncrement;
13285
		}
13286
 
13287
		this.xIncrement = xIncrement + pointInterval;
13288
		return xIncrement;
13289
	},
13290
 
13291
	/**
13292
	 * Divide the series data into segments divided by null values.
13293
	 */
13294
	getSegments: function () {
13295
		var series = this,
13296
			lastNull = -1,
13297
			segments = [],
13298
			i,
13299
			points = series.points,
13300
			pointsLength = points.length;
13301
 
13302
		if (pointsLength) { // no action required for []
13303
 
13304
			// if connect nulls, just remove null points
13305
			if (series.options.connectNulls) {
13306
				i = pointsLength;
13307
				while (i--) {
13308
					if (points[i].y === null) {
13309
						points.splice(i, 1);
13310
					}
13311
				}
13312
				if (points.length) {
13313
					segments = [points];
13314
				}
13315
 
13316
			// else, split on null points
13317
			} else {
13318
				each(points, function (point, i) {
13319
					if (point.y === null) {
13320
						if (i > lastNull + 1) {
13321
							segments.push(points.slice(lastNull + 1, i));
13322
						}
13323
						lastNull = i;
13324
					} else if (i === pointsLength - 1) { // last value
13325
						segments.push(points.slice(lastNull + 1, i + 1));
13326
					}
13327
				});
13328
			}
13329
		}
13330
 
13331
		// register it
13332
		series.segments = segments;
13333
	},
13334
 
13335
	/**
13336
	 * Set the series options by merging from the options tree
13337
	 * @param {Object} itemOptions
13338
	 */
13339
	setOptions: function (itemOptions) {
13340
		var chart = this.chart,
13341
			chartOptions = chart.options,
13342
			plotOptions = chartOptions.plotOptions,
13343
			userOptions = chart.userOptions || {},
13344
			userPlotOptions = userOptions.plotOptions || {},
13345
			typeOptions = plotOptions[this.type],
13346
			options,
13347
			zones;
13348
 
13349
		this.userOptions = itemOptions;
13350
 
13351
		// General series options take precedence over type options because otherwise, default
13352
		// type options like column.animation would be overwritten by the general option.
13353
		// But issues have been raised here (#3881), and the solution may be to distinguish
13354
		// between default option and userOptions like in the tooltip below.
13355
		options = merge(
13356
			typeOptions,
13357
			plotOptions.series,
13358
			itemOptions
13359
		);
13360
 
13361
		// The tooltip options are merged between global and series specific options
13362
		this.tooltipOptions = merge(
13363
			defaultOptions.tooltip,
13364
			defaultOptions.plotOptions[this.type].tooltip,
13365
			userOptions.tooltip,
13366
			userPlotOptions.series && userPlotOptions.series.tooltip,
13367
			userPlotOptions[this.type] && userPlotOptions[this.type].tooltip,
13368
			itemOptions.tooltip
13369
		);
13370
 
13371
		// Delete marker object if not allowed (#1125)
13372
		if (typeOptions.marker === null) {
13373
			delete options.marker;
13374
		}
13375
 
13376
		// Handle color zones
13377
		this.zoneAxis = options.zoneAxis;
13378
		zones = this.zones = (options.zones || []).slice();
13379
		if ((options.negativeColor || options.negativeFillColor) && !options.zones) {
13380
			zones.push({
13381
				value: options[this.zoneAxis + 'Threshold'] || options.threshold || 0,
13382
				color: options.negativeColor,
13383
				fillColor: options.negativeFillColor
13384
			});
13385
		}
13386
		if (zones.length) { // Push one extra zone for the rest
13387
			if (defined(zones[zones.length - 1].value)) {
13388
				zones.push({
13389
					color: this.color,
13390
					fillColor: this.fillColor
13391
				});
13392
			}
13393
		}
13394
		return options;
13395
	},
13396
 
13397
	getCyclic: function (prop, value, defaults) {
13398
		var i,
13399
			userOptions = this.userOptions,
13400
			indexName = '_' + prop + 'Index',
13401
			counterName = prop + 'Counter';
13402
 
13403
		if (!value) {
13404
			if (defined(userOptions[indexName])) { // after Series.update()
13405
				i = userOptions[indexName];
13406
			} else {
13407
				userOptions[indexName] = i = this.chart[counterName] % defaults.length;
13408
				this.chart[counterName] += 1;
13409
			}
13410
			value = defaults[i];
13411
		}
13412
		this[prop] = value;
13413
	},
13414
 
13415
	/**
13416
	 * Get the series' color
13417
	 */
13418
	getColor: function () {
13419
		if (this.options.colorByPoint) {
13420
			this.options.color = null; // #4359, selected slice got series.color even when colorByPoint was set.
13421
		} else {
13422
			this.getCyclic('color', this.options.color || defaultPlotOptions[this.type].color, this.chart.options.colors);
13423
		}
13424
	},
13425
	/**
13426
	 * Get the series' symbol
13427
	 */
13428
	getSymbol: function () {
13429
		var seriesMarkerOption = this.options.marker;
13430
 
13431
		this.getCyclic('symbol', seriesMarkerOption.symbol, this.chart.options.symbols);
13432
 
13433
		// don't substract radius in image symbols (#604)
13434
		if (/^url/.test(this.symbol)) {
13435
			seriesMarkerOption.radius = 0;
13436
		}
13437
	},
13438
 
13439
	drawLegendSymbol: LegendSymbolMixin.drawLineMarker,
13440
 
13441
	/**
13442
	 * Replace the series data with a new set of data
13443
	 * @param {Object} data
13444
	 * @param {Object} redraw
13445
	 */
13446
	setData: function (data, redraw, animation, updatePoints) {
13447
		var series = this,
13448
			oldData = series.points,
13449
			oldDataLength = (oldData && oldData.length) || 0,
13450
			dataLength,
13451
			options = series.options,
13452
			chart = series.chart,
13453
			firstPoint = null,
13454
			xAxis = series.xAxis,
13455
			hasCategories = xAxis && !!xAxis.categories,
13456
			i,
13457
			turboThreshold = options.turboThreshold,
13458
			pt,
13459
			xData = this.xData,
13460
			yData = this.yData,
13461
			pointArrayMap = series.pointArrayMap,
13462
			valueCount = pointArrayMap && pointArrayMap.length;
13463
 
13464
		data = data || [];
13465
		dataLength = data.length;
13466
		redraw = pick(redraw, true);
13467
 
13468
		// If the point count is the same as is was, just run Point.update which is
13469
		// cheaper, allows animation, and keeps references to points.
13470
		if (updatePoints !== false && dataLength && oldDataLength === dataLength && !series.cropped && !series.hasGroupedData && series.visible) {
13471
			each(data, function (point, i) {
13472
				if (oldData[i].update) { // Linked, previously hidden series (#3709)
13473
					oldData[i].update(point, false, null, false);
13474
				}
13475
			});
13476
 
13477
		} else {
13478
 
13479
			// Reset properties
13480
			series.xIncrement = null;
13481
			series.pointRange = hasCategories ? 1 : options.pointRange;
13482
 
13483
			series.colorCounter = 0; // for series with colorByPoint (#1547)
13484
 
13485
			// Update parallel arrays
13486
			each(this.parallelArrays, function (key) {
13487
				series[key + 'Data'].length = 0;
13488
			});
13489
 
13490
			// In turbo mode, only one- or twodimensional arrays of numbers are allowed. The
13491
			// first value is tested, and we assume that all the rest are defined the same
13492
			// way. Although the 'for' loops are similar, they are repeated inside each
13493
			// if-else conditional for max performance.
13494
			if (turboThreshold && dataLength > turboThreshold) {
13495
 
13496
				// find the first non-null point
13497
				i = 0;
13498
				while (firstPoint === null && i < dataLength) {
13499
					firstPoint = data[i];
13500
					i++;
13501
				}
13502
 
13503
 
13504
				if (isNumber(firstPoint)) { // assume all points are numbers
13505
					var x = pick(options.pointStart, 0),
13506
						pointInterval = pick(options.pointInterval, 1);
13507
 
13508
					for (i = 0; i < dataLength; i++) {
13509
						xData[i] = x;
13510
						yData[i] = data[i];
13511
						x += pointInterval;
13512
					}
13513
					series.xIncrement = x;
13514
				} else if (isArray(firstPoint)) { // assume all points are arrays
13515
					if (valueCount) { // [x, low, high] or [x, o, h, l, c]
13516
						for (i = 0; i < dataLength; i++) {
13517
							pt = data[i];
13518
							xData[i] = pt[0];
13519
							yData[i] = pt.slice(1, valueCount + 1);
13520
						}
13521
					} else { // [x, y]
13522
						for (i = 0; i < dataLength; i++) {
13523
							pt = data[i];
13524
							xData[i] = pt[0];
13525
							yData[i] = pt[1];
13526
						}
13527
					}
13528
				} else {
13529
					error(12); // Highcharts expects configs to be numbers or arrays in turbo mode
13530
				}
13531
			} else {
13532
				for (i = 0; i < dataLength; i++) {
13533
					if (data[i] !== UNDEFINED) { // stray commas in oldIE
13534
						pt = { series: series };
13535
						series.pointClass.prototype.applyOptions.apply(pt, [data[i]]);
13536
						series.updateParallelArrays(pt, i);
13537
						if (hasCategories && defined(pt.name)) { // #4401
13538
							xAxis.names[pt.x] = pt.name; // #2046
13539
						}
13540
					}
13541
				}
13542
			}
13543
 
13544
			// Forgetting to cast strings to numbers is a common caveat when handling CSV or JSON
13545
			if (isString(yData[0])) {
13546
				error(14, true);
13547
			}
13548
 
13549
			series.data = [];
13550
			series.options.data = data;
13551
			//series.zData = zData;
13552
 
13553
			// destroy old points
13554
			i = oldDataLength;
13555
			while (i--) {
13556
				if (oldData[i] && oldData[i].destroy) {
13557
					oldData[i].destroy();
13558
				}
13559
			}
13560
 
13561
			// reset minRange (#878)
13562
			if (xAxis) {
13563
				xAxis.minRange = xAxis.userMinRange;
13564
			}
13565
 
13566
			// redraw
13567
			series.isDirty = series.isDirtyData = chart.isDirtyBox = true;
13568
			animation = false;
13569
		}
13570
 
13571
		// Typically for pie series, points need to be processed and generated
13572
		// prior to rendering the legend
13573
		if (options.legendType === 'point') { // docs: legendType now supported on more series types (at least column and pie)
13574
			this.processData();
13575
			this.generatePoints();
13576
		}
13577
 
13578
		if (redraw) {
13579
			chart.redraw(animation);
13580
		}
13581
	},
13582
 
13583
	/**
13584
	 * Process the data by cropping away unused data points if the series is longer
13585
	 * than the crop threshold. This saves computing time for lage series.
13586
	 */
13587
	processData: function (force) {
13588
		var series = this,
13589
			processedXData = series.xData, // copied during slice operation below
13590
			processedYData = series.yData,
13591
			dataLength = processedXData.length,
13592
			croppedData,
13593
			cropStart = 0,
13594
			cropped,
13595
			distance,
13596
			closestPointRange,
13597
			xAxis = series.xAxis,
13598
			i, // loop variable
13599
			options = series.options,
13600
			cropThreshold = options.cropThreshold,
13601
			getExtremesFromAll = series.getExtremesFromAll || options.getExtremesFromAll, // #4599
13602
			isCartesian = series.isCartesian,
13603
			xExtremes,
13604
			min,
13605
			max;
13606
 
13607
		// If the series data or axes haven't changed, don't go through this. Return false to pass
13608
		// the message on to override methods like in data grouping.
13609
		if (isCartesian && !series.isDirty && !xAxis.isDirty && !series.yAxis.isDirty && !force) {
13610
			return false;
13611
		}
13612
 
13613
		if (xAxis) {
13614
			xExtremes = xAxis.getExtremes(); // corrected for log axis (#3053)
13615
			min = xExtremes.min;
13616
			max = xExtremes.max;
13617
		}
13618
 
13619
		// optionally filter out points outside the plot area
13620
		if (isCartesian && series.sorted && !getExtremesFromAll && (!cropThreshold || dataLength > cropThreshold || series.forceCrop)) {
13621
 
13622
			// it's outside current extremes
13623
			if (processedXData[dataLength - 1] < min || processedXData[0] > max) {
13624
				processedXData = [];
13625
				processedYData = [];
13626
 
13627
			// only crop if it's actually spilling out
13628
			} else if (processedXData[0] < min || processedXData[dataLength - 1] > max) {
13629
				croppedData = this.cropData(series.xData, series.yData, min, max);
13630
				processedXData = croppedData.xData;
13631
				processedYData = croppedData.yData;
13632
				cropStart = croppedData.start;
13633
				cropped = true;
13634
			}
13635
		}
13636
 
13637
 
13638
		// Find the closest distance between processed points
13639
		for (i = processedXData.length - 1; i >= 0; i--) {
13640
			distance = processedXData[i] - processedXData[i - 1];
13641
 
13642
			if (distance > 0 && (closestPointRange === UNDEFINED || distance < closestPointRange)) {
13643
				closestPointRange = distance;
13644
 
13645
			// Unsorted data is not supported by the line tooltip, as well as data grouping and
13646
			// navigation in Stock charts (#725) and width calculation of columns (#1900)
13647
			} else if (distance < 0 && series.requireSorting) {
13648
				error(15);
13649
			}
13650
		}
13651
 
13652
		// Record the properties
13653
		series.cropped = cropped; // undefined or true
13654
		series.cropStart = cropStart;
13655
		series.processedXData = processedXData;
13656
		series.processedYData = processedYData;
13657
 
13658
		if (options.pointRange === null) { // null means auto, as for columns, candlesticks and OHLC
13659
			series.pointRange = closestPointRange || 1;
13660
		}
13661
		series.closestPointRange = closestPointRange;
13662
 
13663
	},
13664
 
13665
	/**
13666
	 * Iterate over xData and crop values between min and max. Returns object containing crop start/end
13667
	 * cropped xData with corresponding part of yData, dataMin and dataMax within the cropped range
13668
	 */
13669
	cropData: function (xData, yData, min, max) {
13670
		var dataLength = xData.length,
13671
			cropStart = 0,
13672
			cropEnd = dataLength,
13673
			cropShoulder = pick(this.cropShoulder, 1), // line-type series need one point outside
13674
			i;
13675
 
13676
		// iterate up to find slice start
13677
		for (i = 0; i < dataLength; i++) {
13678
			if (xData[i] >= min) {
13679
				cropStart = mathMax(0, i - cropShoulder);
13680
				break;
13681
			}
13682
		}
13683
 
13684
		// proceed to find slice end
13685
		for (; i < dataLength; i++) {
13686
			if (xData[i] > max) {
13687
				cropEnd = i + cropShoulder;
13688
				break;
13689
			}
13690
		}
13691
 
13692
		return {
13693
			xData: xData.slice(cropStart, cropEnd),
13694
			yData: yData.slice(cropStart, cropEnd),
13695
			start: cropStart,
13696
			end: cropEnd
13697
		};
13698
	},
13699
 
13700
 
13701
	/**
13702
	 * Generate the data point after the data has been processed by cropping away
13703
	 * unused points and optionally grouped in Highcharts Stock.
13704
	 */
13705
	generatePoints: function () {
13706
		var series = this,
13707
			options = series.options,
13708
			dataOptions = options.data,
13709
			data = series.data,
13710
			dataLength,
13711
			processedXData = series.processedXData,
13712
			processedYData = series.processedYData,
13713
			pointClass = series.pointClass,
13714
			processedDataLength = processedXData.length,
13715
			cropStart = series.cropStart || 0,
13716
			cursor,
13717
			hasGroupedData = series.hasGroupedData,
13718
			point,
13719
			points = [],
13720
			i;
13721
 
13722
		if (!data && !hasGroupedData) {
13723
			var arr = [];
13724
			arr.length = dataOptions.length;
13725
			data = series.data = arr;
13726
		}
13727
 
13728
		for (i = 0; i < processedDataLength; i++) {
13729
			cursor = cropStart + i;
13730
			if (!hasGroupedData) {
13731
				if (data[cursor]) {
13732
					point = data[cursor];
13733
				} else if (dataOptions[cursor] !== UNDEFINED) { // #970
13734
					data[cursor] = point = (new pointClass()).init(series, dataOptions[cursor], processedXData[i]);
13735
				}
13736
				points[i] = point;
13737
			} else {
13738
				// splat the y data in case of ohlc data array
13739
				points[i] = (new pointClass()).init(series, [processedXData[i]].concat(splat(processedYData[i])));
13740
			}
13741
			points[i].index = cursor; // For faster access in Point.update
13742
		}
13743
 
13744
		// Hide cropped-away points - this only runs when the number of points is above cropThreshold, or when
13745
		// swithching view from non-grouped data to grouped data (#637)
13746
		if (data && (processedDataLength !== (dataLength = data.length) || hasGroupedData)) {
13747
			for (i = 0; i < dataLength; i++) {
13748
				if (i === cropStart && !hasGroupedData) { // when has grouped data, clear all points
13749
					i += processedDataLength;
13750
				}
13751
				if (data[i]) {
13752
					data[i].destroyElements();
13753
					data[i].plotX = UNDEFINED; // #1003
13754
				}
13755
			}
13756
		}
13757
 
13758
		series.data = data;
13759
		series.points = points;
13760
	},
13761
 
13762
	/**
13763
	 * Calculate Y extremes for visible data
13764
	 */
13765
	getExtremes: function (yData) {
13766
		var xAxis = this.xAxis,
13767
			yAxis = this.yAxis,
13768
			xData = this.processedXData,
13769
			yDataLength,
13770
			activeYData = [],
13771
			activeCounter = 0,
13772
			xExtremes = xAxis.getExtremes(), // #2117, need to compensate for log X axis
13773
			xMin = xExtremes.min,
13774
			xMax = xExtremes.max,
13775
			validValue,
13776
			withinRange,
13777
			x,
13778
			y,
13779
			i,
13780
			j;
13781
 
13782
		yData = yData || this.stackedYData || this.processedYData;
13783
		yDataLength = yData.length;
13784
 
13785
		for (i = 0; i < yDataLength; i++) {
13786
 
13787
			x = xData[i];
13788
			y = yData[i];
13789
 
13790
			// For points within the visible range, including the first point outside the
13791
			// visible range, consider y extremes
13792
			validValue = y !== null && y !== UNDEFINED && (!yAxis.isLog || (y.length || y > 0));
13793
			withinRange = this.getExtremesFromAll || this.options.getExtremesFromAll || this.cropped ||
13794
				((xData[i + 1] || x) >= xMin &&	(xData[i - 1] || x) <= xMax);
13795
 
13796
			if (validValue && withinRange) {
13797
 
13798
				j = y.length;
13799
				if (j) { // array, like ohlc or range data
13800
					while (j--) {
13801
						if (y[j] !== null) {
13802
							activeYData[activeCounter++] = y[j];
13803
						}
13804
					}
13805
				} else {
13806
					activeYData[activeCounter++] = y;
13807
				}
13808
			}
13809
		}
13810
		this.dataMin = arrayMin(activeYData);
13811
		this.dataMax = arrayMax(activeYData);
13812
	},
13813
 
13814
	/**
13815
	 * Translate data points from raw data values to chart specific positioning data
13816
	 * needed later in drawPoints, drawGraph and drawTracker.
13817
	 */
13818
	translate: function () {
13819
		if (!this.processedXData) { // hidden series
13820
			this.processData();
13821
		}
13822
		this.generatePoints();
13823
		var series = this,
13824
			options = series.options,
13825
			stacking = options.stacking,
13826
			xAxis = series.xAxis,
13827
			categories = xAxis.categories,
13828
			yAxis = series.yAxis,
13829
			points = series.points,
13830
			dataLength = points.length,
13831
			hasModifyValue = !!series.modifyValue,
13832
			i,
13833
			pointPlacement = options.pointPlacement,
13834
			dynamicallyPlaced = pointPlacement === 'between' || isNumber(pointPlacement),
13835
			threshold = options.threshold,
13836
			stackThreshold = options.startFromThreshold ? threshold : 0,
13837
			plotX,
13838
			plotY,
13839
			lastPlotX,
13840
			stackIndicator,
13841
			closestPointRangePx = Number.MAX_VALUE;
13842
 
13843
		// Translate each point
13844
		for (i = 0; i < dataLength; i++) {
13845
			var point = points[i],
13846
				xValue = point.x,
13847
				yValue = point.y,
13848
				yBottom = point.low,
13849
				stack = stacking && yAxis.stacks[(series.negStacks && yValue < (stackThreshold ? 0 : threshold) ? '-' : '') + series.stackKey],
13850
				pointStack,
13851
				stackValues;
13852
 
13853
			// Discard disallowed y values for log axes (#3434)
13854
			if (yAxis.isLog && yValue !== null && yValue <= 0) {
13855
				point.y = yValue = null;
13856
				error(10);
13857
			}
13858
 
13859
			// Get the plotX translation
13860
			point.plotX = plotX = mathMin(mathMax(-1e5, xAxis.translate(xValue, 0, 0, 0, 1, pointPlacement, this.type === 'flags')), 1e5); // #3923
13861
 
13862
 
13863
			// Calculate the bottom y value for stacked series
13864
			if (stacking && series.visible && stack && stack[xValue]) {
13865
				stackIndicator = series.getStackIndicator(stackIndicator, xValue, series.index);
13866
				pointStack = stack[xValue];
13867
				stackValues = pointStack.points[stackIndicator.key];
13868
				yBottom = stackValues[0];
13869
				yValue = stackValues[1];
13870
 
13871
				if (yBottom === stackThreshold) {
13872
					yBottom = pick(threshold, yAxis.min);
13873
				}
13874
				if (yAxis.isLog && yBottom <= 0) { // #1200, #1232
13875
					yBottom = null;
13876
				}
13877
 
13878
				point.total = point.stackTotal = pointStack.total;
13879
				point.percentage = pointStack.total && (point.y / pointStack.total * 100);
13880
				point.stackY = yValue;
13881
 
13882
				// Place the stack label
13883
				pointStack.setOffset(series.pointXOffset || 0, series.barW || 0);
13884
 
13885
			}
13886
 
13887
			// Set translated yBottom or remove it
13888
			point.yBottom = defined(yBottom) ?
13889
				yAxis.translate(yBottom, 0, 1, 0, 1) :
13890
				null;
13891
 
13892
			// general hook, used for Highstock compare mode
13893
			if (hasModifyValue) {
13894
				yValue = series.modifyValue(yValue, point);
13895
			}
13896
 
13897
			// Set the the plotY value, reset it for redraws
13898
			point.plotY = plotY = (typeof yValue === 'number' && yValue !== Infinity) ?
13899
				mathMin(mathMax(-1e5, yAxis.translate(yValue, 0, 1, 0, 1)), 1e5) : // #3201
13900
				UNDEFINED;
13901
			point.isInside = plotY !== UNDEFINED && plotY >= 0 && plotY <= yAxis.len && // #3519
13902
				plotX >= 0 && plotX <= xAxis.len;
13903
 
13904
 
13905
			// Set client related positions for mouse tracking
13906
			point.clientX = dynamicallyPlaced ? xAxis.translate(xValue, 0, 0, 0, 1) : plotX; // #1514
13907
 
13908
			point.negative = point.y < (threshold || 0);
13909
 
13910
			// some API data
13911
			point.category = categories && categories[point.x] !== UNDEFINED ?
13912
				categories[point.x] : point.x;
13913
 
13914
			// Determine auto enabling of markers (#3635)
13915
			if (i) {
13916
				closestPointRangePx = mathMin(closestPointRangePx, mathAbs(plotX - lastPlotX));
13917
			}
13918
			lastPlotX = plotX;
13919
 
13920
		}
13921
 
13922
		series.closestPointRangePx = closestPointRangePx;
13923
 
13924
		// now that we have the cropped data, build the segments
13925
		series.getSegments();
13926
	},
13927
 
13928
	/**
13929
	 * Set the clipping for the series. For animated series it is called twice, first to initiate
13930
	 * animating the clip then the second time without the animation to set the final clip.
13931
	 */
13932
	setClip: function (animation) {
13933
		var chart = this.chart,
13934
			options = this.options,
13935
			renderer = chart.renderer,
13936
			inverted = chart.inverted,
13937
			seriesClipBox = this.clipBox,
13938
			clipBox = seriesClipBox || chart.clipBox,
13939
			sharedClipKey = this.sharedClipKey || ['_sharedClip', animation && animation.duration, animation && animation.easing, clipBox.height, options.xAxis, options.yAxis].join(','), // #4526
13940
			clipRect = chart[sharedClipKey],
13941
			markerClipRect = chart[sharedClipKey + 'm'];
13942
 
13943
		// If a clipping rectangle with the same properties is currently present in the chart, use that.
13944
		if (!clipRect) {
13945
 
13946
			// When animation is set, prepare the initial positions
13947
			if (animation) {
13948
				clipBox.width = 0;
13949
 
13950
				chart[sharedClipKey + 'm'] = markerClipRect = renderer.clipRect(
13951
					-99, // include the width of the first marker
13952
					inverted ? -chart.plotLeft : -chart.plotTop,
13953
					99,
13954
					inverted ? chart.chartWidth : chart.chartHeight
13955
				);
13956
			}
13957
			chart[sharedClipKey] = clipRect = renderer.clipRect(clipBox);
13958
 
13959
		}
13960
		if (animation) {
13961
			clipRect.count += 1;
13962
		}
13963
 
13964
		if (options.clip !== false) {
13965
			this.group.clip(animation || seriesClipBox ? clipRect : chart.clipRect);
13966
			this.markerGroup.clip(markerClipRect);
13967
			this.sharedClipKey = sharedClipKey;
13968
		}
13969
 
13970
		// Remove the shared clipping rectangle when all series are shown
13971
		if (!animation) {
13972
			clipRect.count -= 1;
13973
			if (clipRect.count <= 0 && sharedClipKey && chart[sharedClipKey]) {
13974
				if (!seriesClipBox) {
13975
					chart[sharedClipKey] = chart[sharedClipKey].destroy();
13976
				}
13977
				if (chart[sharedClipKey + 'm']) {
13978
					chart[sharedClipKey + 'm'] = chart[sharedClipKey + 'm'].destroy();
13979
				}
13980
			}
13981
		}
13982
	},
13983
 
13984
	/**
13985
	 * Animate in the series
13986
	 */
13987
	animate: function (init) {
13988
		var series = this,
13989
			chart = series.chart,
13990
			clipRect,
13991
			animation = series.options.animation,
13992
			sharedClipKey;
13993
 
13994
		// Animation option is set to true
13995
		if (animation && !isObject(animation)) {
13996
			animation = defaultPlotOptions[series.type].animation;
13997
		}
13998
 
13999
		// Initialize the animation. Set up the clipping rectangle.
14000
		if (init) {
14001
 
14002
			series.setClip(animation);
14003
 
14004
		// Run the animation
14005
		} else {
14006
			sharedClipKey = this.sharedClipKey;
14007
			clipRect = chart[sharedClipKey];
14008
			if (clipRect) {
14009
				clipRect.animate({
14010
					width: chart.plotSizeX
14011
				}, animation);
14012
			}
14013
			if (chart[sharedClipKey + 'm']) {
14014
				chart[sharedClipKey + 'm'].animate({
14015
					width: chart.plotSizeX + 99
14016
				}, animation);
14017
			}
14018
 
14019
			// Delete this function to allow it only once
14020
			series.animate = null;
14021
 
14022
		}
14023
	},
14024
 
14025
	/**
14026
	 * This runs after animation to land on the final plot clipping
14027
	 */
14028
	afterAnimate: function () {
14029
		this.setClip();
14030
		fireEvent(this, 'afterAnimate');
14031
	},
14032
 
14033
	/**
14034
	 * Draw the markers
14035
	 */
14036
	drawPoints: function () {
14037
		var series = this,
14038
			pointAttr,
14039
			points = series.points,
14040
			chart = series.chart,
14041
			plotX,
14042
			plotY,
14043
			i,
14044
			point,
14045
			radius,
14046
			symbol,
14047
			isImage,
14048
			graphic,
14049
			options = series.options,
14050
			seriesMarkerOptions = options.marker,
14051
			seriesPointAttr = series.pointAttr[''],
14052
			pointMarkerOptions,
14053
			hasPointMarker,
14054
			enabled,
14055
			isInside,
14056
			markerGroup = series.markerGroup,
14057
			xAxis = series.xAxis,
14058
			globallyEnabled = pick(
14059
				seriesMarkerOptions.enabled,
14060
				xAxis.isRadial,
14061
				series.closestPointRangePx > 2 * seriesMarkerOptions.radius
14062
			);
14063
 
14064
		if (seriesMarkerOptions.enabled !== false || series._hasPointMarkers) {
14065
 
14066
			i = points.length;
14067
			while (i--) {
14068
				point = points[i];
14069
				plotX = mathFloor(point.plotX); // #1843
14070
				plotY = point.plotY;
14071
				graphic = point.graphic;
14072
				pointMarkerOptions = point.marker || {};
14073
				hasPointMarker = !!point.marker;
14074
				enabled = (globallyEnabled && pointMarkerOptions.enabled === UNDEFINED) || pointMarkerOptions.enabled;
14075
				isInside = point.isInside;
14076
 
14077
				// only draw the point if y is defined
14078
				if (enabled && plotY !== UNDEFINED && !isNaN(plotY) && point.y !== null) {
14079
 
14080
					// shortcuts
14081
					pointAttr = point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE] || seriesPointAttr;
14082
					radius = pointAttr.r;
14083
					symbol = pick(pointMarkerOptions.symbol, series.symbol);
14084
					isImage = symbol.indexOf('url') === 0;
14085
 
14086
					if (graphic) { // update
14087
						graphic[isInside ? 'show' : 'hide'](true) // Since the marker group isn't clipped, each individual marker must be toggled
14088
							.animate(extend({
14089
								x: plotX - radius,
14090
								y: plotY - radius
14091
							}, graphic.symbolName ? { // don't apply to image symbols #507
14092
								width: 2 * radius,
14093
								height: 2 * radius
14094
							} : {}));
14095
					} else if (isInside && (radius > 0 || isImage)) {
14096
						point.graphic = graphic = chart.renderer.symbol(
14097
							symbol,
14098
							plotX - radius,
14099
							plotY - radius,
14100
							2 * radius,
14101
							2 * radius,
14102
							hasPointMarker ? pointMarkerOptions : seriesMarkerOptions
14103
						)
14104
						.attr(pointAttr)
14105
						.add(markerGroup);
14106
					}
14107
 
14108
				} else if (graphic) {
14109
					point.graphic = graphic.destroy(); // #1269
14110
				}
14111
			}
14112
		}
14113
 
14114
	},
14115
 
14116
	/**
14117
	 * Convert state properties from API naming conventions to SVG attributes
14118
	 *
14119
	 * @param {Object} options API options object
14120
	 * @param {Object} base1 SVG attribute object to inherit from
14121
	 * @param {Object} base2 Second level SVG attribute object to inherit from
14122
	 */
14123
	convertAttribs: function (options, base1, base2, base3) {
14124
		var conversion = this.pointAttrToOptions,
14125
			attr,
14126
			option,
14127
			obj = {};
14128
 
14129
		options = options || {};
14130
		base1 = base1 || {};
14131
		base2 = base2 || {};
14132
		base3 = base3 || {};
14133
 
14134
		for (attr in conversion) {
14135
			option = conversion[attr];
14136
			obj[attr] = pick(options[option], base1[attr], base2[attr], base3[attr]);
14137
		}
14138
		return obj;
14139
	},
14140
 
14141
	/**
14142
	 * Get the state attributes. Each series type has its own set of attributes
14143
	 * that are allowed to change on a point's state change. Series wide attributes are stored for
14144
	 * all series, and additionally point specific attributes are stored for all
14145
	 * points with individual marker options. If such options are not defined for the point,
14146
	 * a reference to the series wide attributes is stored in point.pointAttr.
14147
	 */
14148
	getAttribs: function () {
14149
		var series = this,
14150
			seriesOptions = series.options,
14151
			normalOptions = defaultPlotOptions[series.type].marker ? seriesOptions.marker : seriesOptions,
14152
			stateOptions = normalOptions.states,
14153
			stateOptionsHover = stateOptions[HOVER_STATE],
14154
			pointStateOptionsHover,
14155
			seriesColor = series.color,
14156
			seriesNegativeColor = series.options.negativeColor,
14157
			normalDefaults = {
14158
				stroke: seriesColor,
14159
				fill: seriesColor
14160
			},
14161
			points = series.points || [], // #927
14162
			i,
14163
			j,
14164
			threshold,
14165
			point,
14166
			seriesPointAttr = [],
14167
			pointAttr,
14168
			pointAttrToOptions = series.pointAttrToOptions,
14169
			hasPointSpecificOptions = series.hasPointSpecificOptions,
14170
			defaultLineColor = normalOptions.lineColor,
14171
			defaultFillColor = normalOptions.fillColor,
14172
			turboThreshold = seriesOptions.turboThreshold,
14173
			zones = series.zones,
14174
			zoneAxis = series.zoneAxis || 'y',
14175
			attr,
14176
			key;
14177
 
14178
		// series type specific modifications
14179
		if (seriesOptions.marker) { // line, spline, area, areaspline, scatter
14180
 
14181
			// if no hover radius is given, default to normal radius + 2
14182
			stateOptionsHover.radius = stateOptionsHover.radius || normalOptions.radius + stateOptionsHover.radiusPlus;
14183
			stateOptionsHover.lineWidth = stateOptionsHover.lineWidth || normalOptions.lineWidth + stateOptionsHover.lineWidthPlus;
14184
 
14185
		} else { // column, bar, pie
14186
 
14187
			// if no hover color is given, brighten the normal color
14188
			stateOptionsHover.color = stateOptionsHover.color ||
14189
				Color(stateOptionsHover.color || seriesColor)
14190
					.brighten(stateOptionsHover.brightness).get();
14191
 
14192
			// if no hover negativeColor is given, brighten the normal negativeColor
14193
			stateOptionsHover.negativeColor = stateOptionsHover.negativeColor ||
14194
				Color(stateOptionsHover.negativeColor || seriesNegativeColor)
14195
					.brighten(stateOptionsHover.brightness).get();
14196
		}
14197
 
14198
		// general point attributes for the series normal state
14199
		seriesPointAttr[NORMAL_STATE] = series.convertAttribs(normalOptions, normalDefaults);
14200
 
14201
		// HOVER_STATE and SELECT_STATE states inherit from normal state except the default radius
14202
		each([HOVER_STATE, SELECT_STATE], function (state) {
14203
			seriesPointAttr[state] =
14204
					series.convertAttribs(stateOptions[state], seriesPointAttr[NORMAL_STATE]);
14205
		});
14206
 
14207
		// set it
14208
		series.pointAttr = seriesPointAttr;
14209
 
14210
 
14211
		// Generate the point-specific attribute collections if specific point
14212
		// options are given. If not, create a referance to the series wide point
14213
		// attributes
14214
		i = points.length;
14215
		if (!turboThreshold || i < turboThreshold || hasPointSpecificOptions) {
14216
			while (i--) {
14217
				point = points[i];
14218
				normalOptions = (point.options && point.options.marker) || point.options;
14219
				if (normalOptions && normalOptions.enabled === false) {
14220
					normalOptions.radius = 0;
14221
				}
14222
 
14223
				if (zones.length) {
14224
					j = 0;
14225
					threshold = zones[j];
14226
					while (point[zoneAxis] >= threshold.value) {
14227
						threshold = zones[++j];
14228
					}
14229
 
14230
					point.color = point.fillColor = pick(threshold.color, series.color); // #3636, #4267, #4430 - inherit color from series, when color is undefined
14231
 
14232
				}
14233
 
14234
				hasPointSpecificOptions = seriesOptions.colorByPoint || point.color; // #868
14235
 
14236
				// check if the point has specific visual options
14237
				if (point.options) {
14238
					for (key in pointAttrToOptions) {
14239
						if (defined(normalOptions[pointAttrToOptions[key]])) {
14240
							hasPointSpecificOptions = true;
14241
						}
14242
					}
14243
				}
14244
 
14245
				// a specific marker config object is defined for the individual point:
14246
				// create it's own attribute collection
14247
				if (hasPointSpecificOptions) {
14248
					normalOptions = normalOptions || {};
14249
					pointAttr = [];
14250
					stateOptions = normalOptions.states || {}; // reassign for individual point
14251
					pointStateOptionsHover = stateOptions[HOVER_STATE] = stateOptions[HOVER_STATE] || {};
14252
 
14253
					// Handle colors for column and pies
14254
					if (!seriesOptions.marker || (point.negative && !pointStateOptionsHover.fillColor && !stateOptionsHover.fillColor)) { // column, bar, point or negative threshold for series with markers (#3636)
14255
						// If no hover color is given, brighten the normal color. #1619, #2579
14256
						pointStateOptionsHover[series.pointAttrToOptions.fill] = pointStateOptionsHover.color || (!point.options.color && stateOptionsHover[(point.negative && seriesNegativeColor ? 'negativeColor' : 'color')]) ||
14257
							Color(point.color)
14258
								.brighten(pointStateOptionsHover.brightness || stateOptionsHover.brightness)
14259
								.get();
14260
					}
14261
 
14262
					// normal point state inherits series wide normal state
14263
					attr = { color: point.color }; // #868
14264
					if (!defaultFillColor) { // Individual point color or negative color markers (#2219)
14265
						attr.fillColor = point.color;
14266
					}
14267
					if (!defaultLineColor) {
14268
						attr.lineColor = point.color; // Bubbles take point color, line markers use white
14269
					}
14270
					// Color is explicitly set to null or undefined (#1288, #4068)
14271
					if (normalOptions.hasOwnProperty('color') && !normalOptions.color) {
14272
						delete normalOptions.color;
14273
					}
14274
					pointAttr[NORMAL_STATE] = series.convertAttribs(extend(attr, normalOptions), seriesPointAttr[NORMAL_STATE]);
14275
 
14276
					// inherit from point normal and series hover
14277
					pointAttr[HOVER_STATE] = series.convertAttribs(
14278
						stateOptions[HOVER_STATE],
14279
						seriesPointAttr[HOVER_STATE],
14280
						pointAttr[NORMAL_STATE]
14281
					);
14282
 
14283
					// inherit from point normal and series hover
14284
					pointAttr[SELECT_STATE] = series.convertAttribs(
14285
						stateOptions[SELECT_STATE],
14286
						seriesPointAttr[SELECT_STATE],
14287
						pointAttr[NORMAL_STATE]
14288
					);
14289
 
14290
 
14291
				// no marker config object is created: copy a reference to the series-wide
14292
				// attribute collection
14293
				} else {
14294
					pointAttr = seriesPointAttr;
14295
				}
14296
 
14297
				point.pointAttr = pointAttr;
14298
			}
14299
		}
14300
	},
14301
 
14302
	/**
14303
	 * Clear DOM objects and free up memory
14304
	 */
14305
	destroy: function () {
14306
		var series = this,
14307
			chart = series.chart,
14308
			issue134 = /AppleWebKit\/533/.test(userAgent),
14309
			destroy,
14310
			i,
14311
			data = series.data || [],
14312
			point,
14313
			prop,
14314
			axis;
14315
 
14316
		// add event hook
14317
		fireEvent(series, 'destroy');
14318
 
14319
		// remove all events
14320
		removeEvent(series);
14321
 
14322
		// erase from axes
14323
		each(series.axisTypes || [], function (AXIS) {
14324
			axis = series[AXIS];
14325
			if (axis) {
14326
				erase(axis.series, series);
14327
				axis.isDirty = axis.forceRedraw = true;
14328
			}
14329
		});
14330
 
14331
		// remove legend items
14332
		if (series.legendItem) {
14333
			series.chart.legend.destroyItem(series);
14334
		}
14335
 
14336
		// destroy all points with their elements
14337
		i = data.length;
14338
		while (i--) {
14339
			point = data[i];
14340
			if (point && point.destroy) {
14341
				point.destroy();
14342
			}
14343
		}
14344
		series.points = null;
14345
 
14346
		// Clear the animation timeout if we are destroying the series during initial animation
14347
		clearTimeout(series.animationTimeout);
14348
 
14349
		// Destroy all SVGElements associated to the series
14350
		for (prop in series) {
14351
			if (series[prop] instanceof SVGElement && !series[prop].survive) { // Survive provides a hook for not destroying
14352
 
14353
				// issue 134 workaround
14354
				destroy = issue134 && prop === 'group' ?
14355
					'hide' :
14356
					'destroy';
14357
 
14358
				series[prop][destroy]();
14359
			}
14360
		}
14361
 
14362
		// remove from hoverSeries
14363
		if (chart.hoverSeries === series) {
14364
			chart.hoverSeries = null;
14365
		}
14366
		erase(chart.series, series);
14367
 
14368
		// clear all members
14369
		for (prop in series) {
14370
			delete series[prop];
14371
		}
14372
	},
14373
 
14374
	/**
14375
	 * Return the graph path of a segment
14376
	 */
14377
	getSegmentPath: function (segment) {
14378
		var series = this,
14379
			segmentPath = [],
14380
			step = series.options.step;
14381
 
14382
		// build the segment line
14383
		each(segment, function (point, i) {
14384
 
14385
			var plotX = point.plotX,
14386
				plotY = point.plotY,
14387
				lastPoint;
14388
 
14389
			if (series.getPointSpline) { // generate the spline as defined in the SplineSeries object
14390
				segmentPath.push.apply(segmentPath, series.getPointSpline(segment, point, i));
14391
 
14392
			} else {
14393
 
14394
				// moveTo or lineTo
14395
				segmentPath.push(i ? L : M);
14396
 
14397
				// step line?
14398
				if (step && i) {
14399
					lastPoint = segment[i - 1];
14400
					if (step === 'right') {
14401
						segmentPath.push(
14402
							lastPoint.plotX,
14403
							plotY,
14404
							L
14405
						);
14406
 
14407
					} else if (step === 'center') {
14408
						segmentPath.push(
14409
							(lastPoint.plotX + plotX) / 2,
14410
							lastPoint.plotY,
14411
							L,
14412
							(lastPoint.plotX + plotX) / 2,
14413
							plotY,
14414
							L
14415
						);
14416
 
14417
					} else {
14418
						segmentPath.push(
14419
							plotX,
14420
							lastPoint.plotY,
14421
							L
14422
						);
14423
					}
14424
				}
14425
 
14426
				// normal line to next point
14427
				segmentPath.push(
14428
					point.plotX,
14429
					point.plotY
14430
				);
14431
			}
14432
		});
14433
 
14434
		return segmentPath;
14435
	},
14436
 
14437
	/**
14438
	 * Get the graph path
14439
	 */
14440
	getGraphPath: function () {
14441
		var series = this,
14442
			graphPath = [],
14443
			segmentPath,
14444
			singlePoints = []; // used in drawTracker
14445
 
14446
		// Divide into segments and build graph and area paths
14447
		each(series.segments, function (segment) {
14448
 
14449
			segmentPath = series.getSegmentPath(segment);
14450
 
14451
			// add the segment to the graph, or a single point for tracking
14452
			if (segment.length > 1) {
14453
				graphPath = graphPath.concat(segmentPath);
14454
			} else {
14455
				singlePoints.push(segment[0]);
14456
			}
14457
		});
14458
 
14459
		// Record it for use in drawGraph and drawTracker, and return graphPath
14460
		series.singlePoints = singlePoints;
14461
		series.graphPath = graphPath;
14462
 
14463
		return graphPath;
14464
 
14465
	},
14466
 
14467
	/**
14468
	 * Draw the actual graph
14469
	 */
14470
	drawGraph: function () {
14471
		var series = this,
14472
			options = this.options,
14473
			props = [['graph', options.lineColor || this.color, options.dashStyle]],
14474
			lineWidth = options.lineWidth,
14475
			roundCap = options.linecap !== 'square',
14476
			graphPath = this.getGraphPath(),
14477
			fillColor = (this.fillGraph && this.color) || NONE, // polygon series use filled graph
14478
			zones = this.zones;
14479
 
14480
		each(zones, function (threshold, i) {
14481
			props.push(['zoneGraph' + i, threshold.color || series.color, threshold.dashStyle || options.dashStyle]);
14482
		});
14483
 
14484
		// Draw the graph
14485
		each(props, function (prop, i) {
14486
			var graphKey = prop[0],
14487
				graph = series[graphKey],
14488
				attribs;
14489
 
14490
			if (graph) {
14491
				graph.animate({ d: graphPath });
14492
 
14493
			} else if ((lineWidth || fillColor) && graphPath.length) { // #1487
14494
				attribs = {
14495
					stroke: prop[1],
14496
					'stroke-width': lineWidth,
14497
					fill: fillColor,
14498
					zIndex: 1 // #1069
14499
				};
14500
				if (prop[2]) {
14501
					attribs.dashstyle = prop[2];
14502
				} else if (roundCap) {
14503
					attribs['stroke-linecap'] = attribs['stroke-linejoin'] = 'round';
14504
				}
14505
 
14506
				series[graphKey] = series.chart.renderer.path(graphPath)
14507
					.attr(attribs)
14508
					.add(series.group)
14509
					.shadow((i < 2) && options.shadow); // add shadow to normal series (0) or to first zone (1) #3932
14510
			}
14511
		});
14512
	},
14513
 
14514
	/**
14515
	 * Clip the graphs into the positive and negative coloured graphs
14516
	 */
14517
	applyZones: function () {
14518
		var series = this,
14519
			chart = this.chart,
14520
			renderer = chart.renderer,
14521
			zones = this.zones,
14522
			translatedFrom,
14523
			translatedTo,
14524
			clips = this.clips || [],
14525
			clipAttr,
14526
			graph = this.graph,
14527
			area = this.area,
14528
			chartSizeMax = mathMax(chart.chartWidth, chart.chartHeight),
14529
			axis = this[(this.zoneAxis || 'y') + 'Axis'],
14530
			extremes,
14531
			reversed = axis.reversed,
14532
			inverted = chart.inverted,
14533
			horiz = axis.horiz,
14534
			pxRange,
14535
			pxPosMin,
14536
			pxPosMax,
14537
			ignoreZones = false;
14538
 
14539
		if (zones.length && (graph || area) && axis.min !== UNDEFINED) {
14540
			// The use of the Color Threshold assumes there are no gaps
14541
			// so it is safe to hide the original graph and area
14542
			if (graph) {
14543
				graph.hide();
14544
			}
14545
			if (area) {
14546
				area.hide();
14547
			}
14548
 
14549
			// Create the clips
14550
			extremes = axis.getExtremes();
14551
			each(zones, function (threshold, i) {
14552
 
14553
				translatedFrom = reversed ?
14554
					(horiz ? chart.plotWidth : 0) :
14555
					(horiz ? 0 : axis.toPixels(extremes.min));
14556
				translatedFrom = mathMin(mathMax(pick(translatedTo, translatedFrom), 0), chartSizeMax);
14557
				translatedTo = mathMin(mathMax(mathRound(axis.toPixels(pick(threshold.value, extremes.max), true)), 0), chartSizeMax);
14558
 
14559
				if (ignoreZones) {
14560
					translatedFrom = translatedTo = axis.toPixels(extremes.max);
14561
				}
14562
 
14563
				pxRange = Math.abs(translatedFrom - translatedTo);
14564
				pxPosMin = mathMin(translatedFrom, translatedTo);
14565
				pxPosMax = mathMax(translatedFrom, translatedTo);
14566
				if (axis.isXAxis) {
14567
					clipAttr = {
14568
						x: inverted ? pxPosMax : pxPosMin,
14569
						y: 0,
14570
						width: pxRange,
14571
						height: chartSizeMax
14572
					};
14573
					if (!horiz) {
14574
						clipAttr.x = chart.plotHeight - clipAttr.x;
14575
					}
14576
				} else {
14577
					clipAttr = {
14578
						x: 0,
14579
						y: inverted ? pxPosMax : pxPosMin,
14580
						width: chartSizeMax,
14581
						height: pxRange
14582
					};
14583
					if (horiz) {
14584
						clipAttr.y = chart.plotWidth - clipAttr.y;
14585
					}
14586
				}
14587
 
14588
				/// VML SUPPPORT
14589
				if (chart.inverted && renderer.isVML) {
14590
					if (axis.isXAxis) {
14591
						clipAttr = {
14592
							x: 0,
14593
							y: reversed ? pxPosMin : pxPosMax,
14594
							height: clipAttr.width,
14595
							width: chart.chartWidth
14596
						};
14597
					} else {
14598
						clipAttr = {
14599
							x: clipAttr.y - chart.plotLeft - chart.spacingBox.x,
14600
							y: 0,
14601
							width: clipAttr.height,
14602
							height: chart.chartHeight
14603
						};
14604
					}
14605
				}
14606
				/// END OF VML SUPPORT
14607
 
14608
				if (clips[i]) {
14609
					clips[i].animate(clipAttr);
14610
				} else {
14611
					clips[i] = renderer.clipRect(clipAttr);
14612
 
14613
					if (graph) {
14614
						series['zoneGraph' + i].clip(clips[i]);
14615
					}
14616
 
14617
					if (area) {
14618
						series['zoneArea' + i].clip(clips[i]);
14619
					}
14620
				}
14621
				// if this zone extends out of the axis, ignore the others
14622
				ignoreZones = threshold.value > extremes.max;
14623
			});
14624
			this.clips = clips;
14625
		}
14626
	},
14627
 
14628
	/**
14629
	 * Initialize and perform group inversion on series.group and series.markerGroup
14630
	 */
14631
	invertGroups: function () {
14632
		var series = this,
14633
			chart = series.chart;
14634
 
14635
		// Pie, go away (#1736)
14636
		if (!series.xAxis) {
14637
			return;
14638
		}
14639
 
14640
		// A fixed size is needed for inversion to work
14641
		function setInvert() {
14642
			var size = {
14643
				width: series.yAxis.len,
14644
				height: series.xAxis.len
14645
			};
14646
 
14647
			each(['group', 'markerGroup'], function (groupName) {
14648
				if (series[groupName]) {
14649
					series[groupName].attr(size).invert();
14650
				}
14651
			});
14652
		}
14653
 
14654
		addEvent(chart, 'resize', setInvert); // do it on resize
14655
		addEvent(series, 'destroy', function () {
14656
			removeEvent(chart, 'resize', setInvert);
14657
		});
14658
 
14659
		// Do it now
14660
		setInvert(); // do it now
14661
 
14662
		// On subsequent render and redraw, just do setInvert without setting up events again
14663
		series.invertGroups = setInvert;
14664
	},
14665
 
14666
	/**
14667
	 * General abstraction for creating plot groups like series.group, series.dataLabelsGroup and
14668
	 * series.markerGroup. On subsequent calls, the group will only be adjusted to the updated plot size.
14669
	 */
14670
	plotGroup: function (prop, name, visibility, zIndex, parent) {
14671
		var group = this[prop],
14672
			isNew = !group;
14673
 
14674
		// Generate it on first call
14675
		if (isNew) {
14676
			this[prop] = group = this.chart.renderer.g(name)
14677
				.attr({
14678
					visibility: visibility,
14679
					zIndex: zIndex || 0.1 // IE8 needs this
14680
				})
14681
				.add(parent);
14682
 
14683
			group.addClass('highcharts-series-' + this.index);
14684
		}
14685
 
14686
		// Place it on first and subsequent (redraw) calls
14687
		group[isNew ? 'attr' : 'animate'](this.getPlotBox());
14688
		return group;
14689
	},
14690
 
14691
	/**
14692
	 * Get the translation and scale for the plot area of this series
14693
	 */
14694
	getPlotBox: function () {
14695
		var chart = this.chart,
14696
			xAxis = this.xAxis,
14697
			yAxis = this.yAxis;
14698
 
14699
		// Swap axes for inverted (#2339)
14700
		if (chart.inverted) {
14701
			xAxis = yAxis;
14702
			yAxis = this.xAxis;
14703
		}
14704
		return {
14705
			translateX: xAxis ? xAxis.left : chart.plotLeft,
14706
			translateY: yAxis ? yAxis.top : chart.plotTop,
14707
			scaleX: 1, // #1623
14708
			scaleY: 1
14709
		};
14710
	},
14711
 
14712
	/**
14713
	 * Render the graph and markers
14714
	 */
14715
	render: function () {
14716
		var series = this,
14717
			chart = series.chart,
14718
			group,
14719
			options = series.options,
14720
			animation = options.animation,
14721
			// Animation doesn't work in IE8 quirks when the group div is hidden,
14722
			// and looks bad in other oldIE
14723
			animDuration = (animation && !!series.animate && chart.renderer.isSVG && pick(animation.duration, 500)) || 0,
14724
			visibility = series.visible ? VISIBLE : HIDDEN,
14725
			zIndex = options.zIndex,
14726
			hasRendered = series.hasRendered,
14727
			chartSeriesGroup = chart.seriesGroup;
14728
 
14729
		// the group
14730
		group = series.plotGroup(
14731
			'group',
14732
			'series',
14733
			visibility,
14734
			zIndex,
14735
			chartSeriesGroup
14736
		);
14737
 
14738
		series.markerGroup = series.plotGroup(
14739
			'markerGroup',
14740
			'markers',
14741
			visibility,
14742
			zIndex,
14743
			chartSeriesGroup
14744
		);
14745
 
14746
		// initiate the animation
14747
		if (animDuration) {
14748
			series.animate(true);
14749
		}
14750
 
14751
		// cache attributes for shapes
14752
		series.getAttribs();
14753
 
14754
		// SVGRenderer needs to know this before drawing elements (#1089, #1795)
14755
		group.inverted = series.isCartesian ? chart.inverted : false;
14756
 
14757
		// draw the graph if any
14758
		if (series.drawGraph) {
14759
			series.drawGraph();
14760
			series.applyZones();
14761
		}
14762
 
14763
		each(series.points, function (point) {
14764
			if (point.redraw) {
14765
				point.redraw();
14766
			}
14767
		});
14768
 
14769
		// draw the data labels (inn pies they go before the points)
14770
		if (series.drawDataLabels) {
14771
			series.drawDataLabels();
14772
		}
14773
 
14774
		// draw the points
14775
		if (series.visible) {
14776
			series.drawPoints();
14777
		}
14778
 
14779
 
14780
		// draw the mouse tracking area
14781
		if (series.drawTracker && series.options.enableMouseTracking !== false) {
14782
			series.drawTracker();
14783
		}
14784
 
14785
		// Handle inverted series and tracker groups
14786
		if (chart.inverted) {
14787
			series.invertGroups();
14788
		}
14789
 
14790
		// Initial clipping, must be defined after inverting groups for VML. Applies to columns etc. (#3839).
14791
		if (options.clip !== false && !series.sharedClipKey && !hasRendered) {
14792
			group.clip(chart.clipRect);
14793
		}
14794
 
14795
		// Run the animation
14796
		if (animDuration) {
14797
			series.animate();
14798
		}
14799
 
14800
		// Call the afterAnimate function on animation complete (but don't overwrite the animation.complete option
14801
		// which should be available to the user).
14802
		if (!hasRendered) {
14803
			if (animDuration) {
14804
				series.animationTimeout = setTimeout(function () {
14805
					series.afterAnimate();
14806
				}, animDuration);
14807
			} else {
14808
				series.afterAnimate();
14809
			}
14810
		}
14811
 
14812
		series.isDirty = series.isDirtyData = false; // means data is in accordance with what you see
14813
		// (See #322) series.isDirty = series.isDirtyData = false; // means data is in accordance with what you see
14814
		series.hasRendered = true;
14815
	},
14816
 
14817
	/**
14818
	 * Redraw the series after an update in the axes.
14819
	 */
14820
	redraw: function () {
14821
		var series = this,
14822
			chart = series.chart,
14823
			wasDirtyData = series.isDirtyData, // cache it here as it is set to false in render, but used after
14824
			wasDirty = series.isDirty,
14825
			group = series.group,
14826
			xAxis = series.xAxis,
14827
			yAxis = series.yAxis;
14828
 
14829
		// reposition on resize
14830
		if (group) {
14831
			if (chart.inverted) {
14832
				group.attr({
14833
					width: chart.plotWidth,
14834
					height: chart.plotHeight
14835
				});
14836
			}
14837
 
14838
			group.animate({
14839
				translateX: pick(xAxis && xAxis.left, chart.plotLeft),
14840
				translateY: pick(yAxis && yAxis.top, chart.plotTop)
14841
			});
14842
		}
14843
 
14844
		series.translate();
14845
		series.render();
14846
		if (wasDirtyData) {
14847
			fireEvent(series, 'updatedData');
14848
		}
14849
		if (wasDirty || wasDirtyData) {			// #3945 recalculate the kdtree when dirty
14850
			delete this.kdTree; // #3868 recalculate the kdtree with dirty data
14851
		}
14852
	},
14853
 
14854
	/**
14855
	 * KD Tree && PointSearching Implementation
14856
	 */
14857
 
14858
	kdDimensions: 1,
14859
	kdAxisArray: ['clientX', 'plotY'],
14860
 
14861
	searchPoint: function (e, compareX) {
14862
		var series = this,
14863
			xAxis = series.xAxis,
14864
			yAxis = series.yAxis,
14865
			inverted = series.chart.inverted;
14866
 
14867
		return this.searchKDTree({
14868
			clientX: inverted ? xAxis.len - e.chartY + xAxis.pos : e.chartX - xAxis.pos,
14869
			plotY: inverted ? yAxis.len - e.chartX + yAxis.pos : e.chartY - yAxis.pos
14870
		}, compareX);
14871
	},
14872
 
14873
	buildKDTree: function () {
14874
		var series = this,
14875
			dimensions = series.kdDimensions;
14876
 
14877
		// Internal function
14878
		function _kdtree(points, depth, dimensions) {
14879
			var axis, median, length = points && points.length;
14880
 
14881
			if (length) {
14882
 
14883
				// alternate between the axis
14884
				axis = series.kdAxisArray[depth % dimensions];
14885
 
14886
				// sort point array
14887
				points.sort(function(a, b) {
14888
					return a[axis] - b[axis];
14889
				});
14890
 
14891
				median = Math.floor(length / 2);
14892
 
14893
				// build and return nod
14894
				return {
14895
					point: points[median],
14896
					left: _kdtree(points.slice(0, median), depth + 1, dimensions),
14897
					right: _kdtree(points.slice(median + 1), depth + 1, dimensions)
14898
				};
14899
 
14900
			}
14901
		}
14902
 
14903
		// Start the recursive build process with a clone of the points array and null points filtered out (#3873)
14904
		function startRecursive() {
14905
			var points = grep(series.points || [], function (point) { // #4390
14906
				return point.y !== null;
14907
			});
14908
 
14909
			series.kdTree = _kdtree(points, dimensions, dimensions);
14910
		}
14911
		delete series.kdTree;
14912
 
14913
		if (series.options.kdSync) {  // For testing tooltips, don't build async
14914
			startRecursive();
14915
		} else {
14916
			setTimeout(startRecursive);
14917
		}
14918
	},
14919
 
14920
	searchKDTree: function (point, compareX) {
14921
		var series = this,
14922
			kdX = this.kdAxisArray[0],
14923
			kdY = this.kdAxisArray[1],
14924
			kdComparer = compareX ? 'distX' : 'dist';
14925
 
14926
		// Set the one and two dimensional distance on the point object
14927
		function setDistance(p1, p2) {
14928
			var x = (defined(p1[kdX]) && defined(p2[kdX])) ? Math.pow(p1[kdX] - p2[kdX], 2) : null,
14929
				y = (defined(p1[kdY]) && defined(p2[kdY])) ? Math.pow(p1[kdY] - p2[kdY], 2) : null,
14930
				r = (x || 0) + (y || 0);
14931
 
14932
			p2.dist = defined(r) ? Math.sqrt(r) : Number.MAX_VALUE;
14933
			p2.distX = defined(x) ? Math.sqrt(x) : Number.MAX_VALUE;
14934
		}
14935
		function _search(search, tree, depth, dimensions) {
14936
			var point = tree.point,
14937
				axis = series.kdAxisArray[depth % dimensions],
14938
				tdist,
14939
				sideA,
14940
				sideB,
14941
				ret = point,
14942
				nPoint1,
14943
				nPoint2;
14944
 
14945
			setDistance(search, point);
14946
 
14947
			// Pick side based on distance to splitting point
14948
			tdist = search[axis] - point[axis];
14949
			sideA = tdist < 0 ? 'left' : 'right';
14950
			sideB = tdist < 0 ? 'right' : 'left';
14951
 
14952
			// End of tree
14953
			if (tree[sideA]) {
14954
				nPoint1 =_search(search, tree[sideA], depth + 1, dimensions);
14955
 
14956
				ret = (nPoint1[kdComparer] < ret[kdComparer] ? nPoint1 : point);
14957
			}
14958
			if (tree[sideB]) {
14959
				// compare distance to current best to splitting point to decide wether to check side B or not
14960
				if (Math.sqrt(tdist * tdist) < ret[kdComparer]) {
14961
					nPoint2 = _search(search, tree[sideB], depth + 1, dimensions);
14962
					ret = (nPoint2[kdComparer] < ret[kdComparer] ? nPoint2 : ret);
14963
				}
14964
			}
14965
 
14966
			return ret;
14967
		}
14968
 
14969
		if (!this.kdTree) {
14970
			this.buildKDTree();
14971
		}
14972
 
14973
		if (this.kdTree) {
14974
			return _search(point,
14975
				this.kdTree, this.kdDimensions, this.kdDimensions);
14976
		}
14977
	}
14978
 
14979
}; // end Series prototype
14980
 
14981
/**
14982
 * The class for stack items
14983
 */
14984
function StackItem(axis, options, isNegative, x, stackOption) {
14985
 
14986
	var inverted = axis.chart.inverted;
14987
 
14988
	this.axis = axis;
14989
 
14990
	// Tells if the stack is negative
14991
	this.isNegative = isNegative;
14992
 
14993
	// Save the options to be able to style the label
14994
	this.options = options;
14995
 
14996
	// Save the x value to be able to position the label later
14997
	this.x = x;
14998
 
14999
	// Initialize total value
15000
	this.total = null;
15001
 
15002
	// This will keep each points' extremes stored by series.index and point index
15003
	this.points = {};
15004
 
15005
	// Save the stack option on the series configuration object, and whether to treat it as percent
15006
	this.stack = stackOption;
15007
 
15008
	// The align options and text align varies on whether the stack is negative and
15009
	// if the chart is inverted or not.
15010
	// First test the user supplied value, then use the dynamic.
15011
	this.alignOptions = {
15012
		align: options.align || (inverted ? (isNegative ? 'left' : 'right') : 'center'),
15013
		verticalAlign: options.verticalAlign || (inverted ? 'middle' : (isNegative ? 'bottom' : 'top')),
15014
		y: pick(options.y, inverted ? 4 : (isNegative ? 14 : -6)),
15015
		x: pick(options.x, inverted ? (isNegative ? -6 : 6) : 0)
15016
	};
15017
 
15018
	this.textAlign = options.textAlign || (inverted ? (isNegative ? 'right' : 'left') : 'center');
15019
}
15020
 
15021
StackItem.prototype = {
15022
	destroy: function () {
15023
		destroyObjectProperties(this, this.axis);
15024
	},
15025
 
15026
	/**
15027
	 * Renders the stack total label and adds it to the stack label group.
15028
	 */
15029
	render: function (group) {
15030
		var options = this.options,
15031
			formatOption = options.format,
15032
			str = formatOption ?
15033
				format(formatOption, this) :
15034
				options.formatter.call(this);  // format the text in the label
15035
 
15036
		// Change the text to reflect the new total and set visibility to hidden in case the serie is hidden
15037
		if (this.label) {
15038
			this.label.attr({text: str, visibility: HIDDEN});
15039
		// Create new label
15040
		} else {
15041
			this.label =
15042
				this.axis.chart.renderer.text(str, null, null, options.useHTML)		// dummy positions, actual position updated with setOffset method in columnseries
15043
					.css(options.style)				// apply style
15044
					.attr({
15045
						align: this.textAlign,				// fix the text-anchor
15046
						rotation: options.rotation,	// rotation
15047
						visibility: HIDDEN					// hidden until setOffset is called
15048
					})
15049
					.add(group);							// add to the labels-group
15050
		}
15051
	},
15052
 
15053
	/**
15054
	 * Sets the offset that the stack has from the x value and repositions the label.
15055
	 */
15056
	setOffset: function (xOffset, xWidth) {
15057
		var stackItem = this,
15058
			axis = stackItem.axis,
15059
			chart = axis.chart,
15060
			inverted = chart.inverted,
15061
			reversed = axis.reversed,
15062
			neg = (this.isNegative && !reversed) || (!this.isNegative && reversed), // #4056
15063
			y = axis.translate(axis.usePercentage ? 100 : this.total, 0, 0, 0, 1), // stack value translated mapped to chart coordinates
15064
			yZero = axis.translate(0),						// stack origin
15065
			h = mathAbs(y - yZero),							// stack height
15066
			x = chart.xAxis[0].translate(this.x) + xOffset,	// stack x position
15067
			plotHeight = chart.plotHeight,
15068
			stackBox = {	// this is the box for the complete stack
15069
				x: inverted ? (neg ? y : y - h) : x,
15070
				y: inverted ? plotHeight - x - xWidth : (neg ? (plotHeight - y - h) : plotHeight - y),
15071
				width: inverted ? h : xWidth,
15072
				height: inverted ? xWidth : h
15073
			},
15074
			label = this.label,
15075
			alignAttr;
15076
 
15077
		if (label) {
15078
			label.align(this.alignOptions, null, stackBox);	// align the label to the box
15079
 
15080
			// Set visibility (#678)
15081
			alignAttr = label.alignAttr;
15082
			label[this.options.crop === false || chart.isInsidePlot(alignAttr.x, alignAttr.y) ? 'show' : 'hide'](true);
15083
		}
15084
	}
15085
};
15086
 
15087
/**
15088
 * Generate stacks for each series and calculate stacks total values
15089
 */
15090
Chart.prototype.getStacks = function () {
15091
	var chart = this;
15092
 
15093
	// reset stacks for each yAxis
15094
	each(chart.yAxis, function (axis) {
15095
		if (axis.stacks && axis.hasVisibleSeries) {
15096
			axis.oldStacks = axis.stacks;
15097
		}
15098
	});
15099
 
15100
	each(chart.series, function (series) {
15101
		if (series.options.stacking && (series.visible === true || chart.options.chart.ignoreHiddenSeries === false)) {
15102
			series.stackKey = series.type + pick(series.options.stack, '');
15103
		}
15104
	});
15105
};
15106
 
15107
 
15108
// Stacking methods defined on the Axis prototype
15109
 
15110
/**
15111
 * Build the stacks from top down
15112
 */
15113
Axis.prototype.buildStacks = function () {
15114
	var series = this.series,
15115
		reversedStacks = pick(this.options.reversedStacks, true),
15116
		i = series.length;
15117
	if (!this.isXAxis) {
15118
		this.usePercentage = false;
15119
		while (i--) {
15120
			series[reversedStacks ? i : series.length - i - 1].setStackedPoints();
15121
		}
15122
		// Loop up again to compute percent stack
15123
		if (this.usePercentage) {
15124
			for (i = 0; i < series.length; i++) {
15125
				series[i].setPercentStacks();
15126
			}
15127
		}
15128
	}
15129
};
15130
 
15131
Axis.prototype.renderStackTotals = function () {
15132
	var axis = this,
15133
		chart = axis.chart,
15134
		renderer = chart.renderer,
15135
		stacks = axis.stacks,
15136
		stackKey,
15137
		oneStack,
15138
		stackCategory,
15139
		stackTotalGroup = axis.stackTotalGroup;
15140
 
15141
	// Create a separate group for the stack total labels
15142
	if (!stackTotalGroup) {
15143
		axis.stackTotalGroup = stackTotalGroup =
15144
			renderer.g('stack-labels')
15145
				.attr({
15146
					visibility: VISIBLE,
15147
					zIndex: 6
15148
				})
15149
				.add();
15150
	}
15151
 
15152
	// plotLeft/Top will change when y axis gets wider so we need to translate the
15153
	// stackTotalGroup at every render call. See bug #506 and #516
15154
	stackTotalGroup.translate(chart.plotLeft, chart.plotTop);
15155
 
15156
	// Render each stack total
15157
	for (stackKey in stacks) {
15158
		oneStack = stacks[stackKey];
15159
		for (stackCategory in oneStack) {
15160
			oneStack[stackCategory].render(stackTotalGroup);
15161
		}
15162
	}
15163
};
15164
 
15165
/**
15166
 * Set all the stacks to initial states and destroy unused ones.
15167
 */
15168
Axis.prototype.resetStacks = function () {
15169
	var stacks = this.stacks,
15170
		type,
15171
		i;
15172
	if (!this.isXAxis) {
15173
		for (type in stacks) {
15174
			for (i in stacks[type]) {
15175
 
15176
				// Clean up memory after point deletion (#1044, #4320)
15177
				if (stacks[type][i].touched < this.stacksTouched) {
15178
					stacks[type][i].destroy();
15179
					delete stacks[type][i];
15180
 
15181
				// Reset stacks
15182
				} else {
15183
					stacks[type][i].total = null;
15184
					stacks[type][i].cum = 0;
15185
				}
15186
			}
15187
		}
15188
	}
15189
};
15190
 
15191
Axis.prototype.cleanStacks = function () {
15192
	var stacks, type, i;
15193
 
15194
	if (!this.isXAxis) {
15195
		if (this.oldStacks) {
15196
			stacks = this.stacks = this.oldStacks;
15197
		}
15198
 
15199
		// reset stacks
15200
		for (type in stacks) {
15201
			for (i in stacks[type]) {
15202
				stacks[type][i].cum = stacks[type][i].total;
15203
			}
15204
		}
15205
	}
15206
};
15207
 
15208
 
15209
// Stacking methods defnied for Series prototype
15210
 
15211
/**
15212
 * Adds series' points value to corresponding stack
15213
 */
15214
Series.prototype.setStackedPoints = function () {
15215
	if (!this.options.stacking || (this.visible !== true && this.chart.options.chart.ignoreHiddenSeries !== false)) {
15216
		return;
15217
	}
15218
 
15219
	var series = this,
15220
		xData = series.processedXData,
15221
		yData = series.processedYData,
15222
		stackedYData = [],
15223
		yDataLength = yData.length,
15224
		seriesOptions = series.options,
15225
		threshold = seriesOptions.threshold,
15226
		stackThreshold = seriesOptions.startFromThreshold ? threshold : 0,
15227
		stackOption = seriesOptions.stack,
15228
		stacking = seriesOptions.stacking,
15229
		stackKey = series.stackKey,
15230
		negKey = '-' + stackKey,
15231
		negStacks = series.negStacks,
15232
		yAxis = series.yAxis,
15233
		stacks = yAxis.stacks,
15234
		oldStacks = yAxis.oldStacks,
15235
		stackIndicator,
15236
		isNegative,
15237
		stack,
15238
		other,
15239
		key,
15240
		pointKey,
15241
		i,
15242
		x,
15243
		y;
15244
 
15245
 
15246
	yAxis.stacksTouched += 1;
15247
 
15248
	// loop over the non-null y values and read them into a local array
15249
	for (i = 0; i < yDataLength; i++) {
15250
		x = xData[i];
15251
		y = yData[i];
15252
		stackIndicator = series.getStackIndicator(stackIndicator, x, series.index);
15253
		pointKey = stackIndicator.key;
15254
		// Read stacked values into a stack based on the x value,
15255
		// the sign of y and the stack key. Stacking is also handled for null values (#739)
15256
		isNegative = negStacks && y < (stackThreshold ? 0 : threshold);
15257
		key = isNegative ? negKey : stackKey;
15258
 
15259
		// Create empty object for this stack if it doesn't exist yet
15260
		if (!stacks[key]) {
15261
			stacks[key] = {};
15262
		}
15263
 
15264
		// Initialize StackItem for this x
15265
		if (!stacks[key][x]) {
15266
			if (oldStacks[key] && oldStacks[key][x]) {
15267
				stacks[key][x] = oldStacks[key][x];
15268
				stacks[key][x].total = null;
15269
			} else {
15270
				stacks[key][x] = new StackItem(yAxis, yAxis.options.stackLabels, isNegative, x, stackOption);
15271
			}
15272
		}
15273
 
15274
		// If the StackItem doesn't exist, create it first
15275
		stack = stacks[key][x];
15276
		//stack.points[pointKey] = [stack.cum || stackThreshold];
15277
		stack.points[pointKey] = [pick(stack.cum, stackThreshold)];
15278
		stack.touched = yAxis.stacksTouched;
15279
 
15280
 
15281
		// Add value to the stack total
15282
		if (stacking === 'percent') {
15283
 
15284
			// Percent stacked column, totals are the same for the positive and negative stacks
15285
			other = isNegative ? stackKey : negKey;
15286
			if (negStacks && stacks[other] && stacks[other][x]) {
15287
				other = stacks[other][x];
15288
				stack.total = other.total = mathMax(other.total, stack.total) + mathAbs(y) || 0;
15289
 
15290
			// Percent stacked areas
15291
			} else {
15292
				stack.total = correctFloat(stack.total + (mathAbs(y) || 0));
15293
			}
15294
		} else {
15295
			stack.total = correctFloat(stack.total + (y || 0));
15296
		}
15297
 
15298
		stack.cum = pick(stack.cum, stackThreshold) + (y || 0);
15299
 
15300
		stack.points[pointKey].push(stack.cum);
15301
		stackedYData[i] = stack.cum;
15302
 
15303
	}
15304
 
15305
	if (stacking === 'percent') {
15306
		yAxis.usePercentage = true;
15307
	}
15308
 
15309
	this.stackedYData = stackedYData; // To be used in getExtremes
15310
 
15311
	// Reset old stacks
15312
	yAxis.oldStacks = {};
15313
};
15314
 
15315
/**
15316
 * Iterate over all stacks and compute the absolute values to percent
15317
 */
15318
Series.prototype.setPercentStacks = function () {
15319
	var series = this,
15320
		stackKey = series.stackKey,
15321
		stacks = series.yAxis.stacks,
15322
		processedXData = series.processedXData,
15323
		stackIndicator;
15324
 
15325
	each([stackKey, '-' + stackKey], function (key) {
15326
		var i = processedXData.length,
15327
			x,
15328
			stack,
15329
			pointExtremes,
15330
			totalFactor;
15331
 
15332
		while (i--) {
15333
			x = processedXData[i];
15334
			stackIndicator = series.getStackIndicator(stackIndicator, x, series.index);
15335
			stack = stacks[key] && stacks[key][x];
15336
			pointExtremes = stack && stack.points[stackIndicator.key];
15337
			if (pointExtremes) {
15338
				totalFactor = stack.total ? 100 / stack.total : 0;
15339
				pointExtremes[0] = correctFloat(pointExtremes[0] * totalFactor); // Y bottom value
15340
				pointExtremes[1] = correctFloat(pointExtremes[1] * totalFactor); // Y value
15341
				series.stackedYData[i] = pointExtremes[1];
15342
			}
15343
		}
15344
	});
15345
};
15346
 
15347
/**
15348
* Get stack indicator, according to it's x-value, to determine points with the same x-value
15349
*/
15350
Series.prototype.getStackIndicator = function(stackIndicator, x, index) {
15351
	if (!defined(stackIndicator) || stackIndicator.x !== x) {
15352
		stackIndicator = {
15353
			x: x,
15354
			index: 0
15355
		};
15356
	} else {
15357
		stackIndicator.index++;
15358
	}
15359
 
15360
	stackIndicator.key = [index, x, stackIndicator.index].join(',');
15361
 
15362
	return stackIndicator;
15363
};
15364
 
15365
// Extend the Chart prototype for dynamic methods
15366
extend(Chart.prototype, {
15367
 
15368
	/**
15369
	 * Add a series dynamically after  time
15370
	 *
15371
	 * @param {Object} options The config options
15372
	 * @param {Boolean} redraw Whether to redraw the chart after adding. Defaults to true.
15373
	 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
15374
	 *    configuration
15375
	 *
15376
	 * @return {Object} series The newly created series object
15377
	 */
15378
	addSeries: function (options, redraw, animation) {
15379
		var series,
15380
			chart = this;
15381
 
15382
		if (options) {
15383
			redraw = pick(redraw, true); // defaults to true
15384
 
15385
			fireEvent(chart, 'addSeries', { options: options }, function () {
15386
				series = chart.initSeries(options);
15387
 
15388
				chart.isDirtyLegend = true; // the series array is out of sync with the display
15389
				chart.linkSeries();
15390
				if (redraw) {
15391
					chart.redraw(animation);
15392
				}
15393
			});
15394
		}
15395
 
15396
		return series;
15397
	},
15398
 
15399
	/**
15400
     * Add an axis to the chart
15401
     * @param {Object} options The axis option
15402
     * @param {Boolean} isX Whether it is an X axis or a value axis
15403
     */
15404
	addAxis: function (options, isX, redraw, animation) {
15405
		var key = isX ? 'xAxis' : 'yAxis',
15406
			chartOptions = this.options,
15407
			axis;
15408
 
15409
		/*jslint unused: false*/
15410
		axis = new Axis(this, merge(options, {
15411
			index: this[key].length,
15412
			isX: isX
15413
		}));
15414
		/*jslint unused: true*/
15415
 
15416
		// Push the new axis options to the chart options
15417
		chartOptions[key] = splat(chartOptions[key] || {});
15418
		chartOptions[key].push(options);
15419
 
15420
		if (pick(redraw, true)) {
15421
			this.redraw(animation);
15422
		}
15423
	},
15424
 
15425
	/**
15426
	 * Dim the chart and show a loading text or symbol
15427
	 * @param {String} str An optional text to show in the loading label instead of the default one
15428
	 */
15429
	showLoading: function (str) {
15430
		var chart = this,
15431
			options = chart.options,
15432
			loadingDiv = chart.loadingDiv,
15433
			loadingOptions = options.loading,
15434
			setLoadingSize = function () {
15435
				if (loadingDiv) {
15436
					css(loadingDiv, {
15437
						left: chart.plotLeft + PX,
15438
						top: chart.plotTop + PX,
15439
						width: chart.plotWidth + PX,
15440
						height: chart.plotHeight + PX
15441
					});
15442
				}
15443
			};
15444
 
15445
		// create the layer at the first call
15446
		if (!loadingDiv) {
15447
			chart.loadingDiv = loadingDiv = createElement(DIV, {
15448
				className: PREFIX + 'loading'
15449
			}, extend(loadingOptions.style, {
15450
				zIndex: 10,
15451
				display: NONE
15452
			}), chart.container);
15453
 
15454
			chart.loadingSpan = createElement(
15455
				'span',
15456
				null,
15457
				loadingOptions.labelStyle,
15458
				loadingDiv
15459
			);
15460
			addEvent(chart, 'redraw', setLoadingSize); // #1080
15461
		}
15462
 
15463
		// update text
15464
		chart.loadingSpan.innerHTML = str || options.lang.loading;
15465
 
15466
		// show it
15467
		if (!chart.loadingShown) {
15468
			css(loadingDiv, {
15469
				opacity: 0,
15470
				display: ''
15471
			});
15472
			animate(loadingDiv, {
15473
				opacity: loadingOptions.style.opacity
15474
			}, {
15475
				duration: loadingOptions.showDuration || 0
15476
			});
15477
			chart.loadingShown = true;
15478
		}
15479
		setLoadingSize();
15480
	},
15481
 
15482
	/**
15483
	 * Hide the loading layer
15484
	 */
15485
	hideLoading: function () {
15486
		var options = this.options,
15487
			loadingDiv = this.loadingDiv;
15488
 
15489
		if (loadingDiv) {
15490
			animate(loadingDiv, {
15491
				opacity: 0
15492
			}, {
15493
				duration: options.loading.hideDuration || 100,
15494
				complete: function () {
15495
					css(loadingDiv, { display: NONE });
15496
				}
15497
			});
15498
		}
15499
		this.loadingShown = false;
15500
	}
15501
});
15502
 
15503
// extend the Point prototype for dynamic methods
15504
extend(Point.prototype, {
15505
	/**
15506
	 * Update the point with new options (typically x/y data) and optionally redraw the series.
15507
	 *
15508
	 * @param {Object} options Point options as defined in the series.data array
15509
	 * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call
15510
	 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
15511
	 *    configuration
15512
	 *
15513
	 */
15514
	update: function (options, redraw, animation, runEvent) {
15515
		var point = this,
15516
			series = point.series,
15517
			graphic = point.graphic,
15518
			i,
15519
			chart = series.chart,
15520
			seriesOptions = series.options,
15521
			names = series.xAxis && series.xAxis.names;
15522
 
15523
		redraw = pick(redraw, true);
15524
 
15525
		function update() {
15526
 
15527
			point.applyOptions(options);
15528
 
15529
			// Update visuals
15530
			if (point.y === null && graphic) { // #4146
15531
				point.graphic = graphic.destroy();
15532
			}
15533
			if (isObject(options) && !isArray(options)) {
15534
				// Defer the actual redraw until getAttribs has been called (#3260)
15535
				point.redraw = function () {
15536
					if (graphic && graphic.element) {
15537
						if (options && options.marker && options.marker.symbol) {
15538
							point.graphic = graphic.destroy();
15539
						}
15540
					}
15541
					if (options && options.dataLabels && point.dataLabel) { // #2468
15542
						point.dataLabel = point.dataLabel.destroy();
15543
					}
15544
					point.redraw = null;
15545
				};
15546
			}
15547
 
15548
			// record changes in the parallel arrays
15549
			i = point.index;
15550
			series.updateParallelArrays(point, i);
15551
			if (names && point.name) {
15552
				names[point.x] = point.name;
15553
			}
15554
 
15555
			seriesOptions.data[i] = point.options;
15556
 
15557
			// redraw
15558
			series.isDirty = series.isDirtyData = true;
15559
			if (!series.fixedBox && series.hasCartesianSeries) { // #1906, #2320
15560
				chart.isDirtyBox = true;
15561
			}
15562
 
15563
			if (seriesOptions.legendType === 'point') { // #1831, #1885
15564
				chart.isDirtyLegend = true;
15565
			}
15566
			if (redraw) {
15567
				chart.redraw(animation);
15568
			}
15569
		}
15570
 
15571
		// Fire the event with a default handler of doing the update
15572
		if (runEvent === false) { // When called from setData
15573
			update();
15574
		} else {
15575
			point.firePointEvent('update', { options: options }, update);
15576
		}
15577
	},
15578
 
15579
	/**
15580
	 * Remove a point and optionally redraw the series and if necessary the axes
15581
	 * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call
15582
	 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
15583
	 *    configuration
15584
	 */
15585
	remove: function (redraw, animation) {
15586
		this.series.removePoint(inArray(this, this.series.data), redraw, animation);
15587
	}
15588
});
15589
 
15590
// Extend the series prototype for dynamic methods
15591
extend(Series.prototype, {
15592
	/**
15593
	 * Add a point dynamically after chart load time
15594
	 * @param {Object} options Point options as given in series.data
15595
	 * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call
15596
	 * @param {Boolean} shift If shift is true, a point is shifted off the start
15597
	 *    of the series as one is appended to the end.
15598
	 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
15599
	 *    configuration
15600
	 */
15601
	addPoint: function (options, redraw, shift, animation) {
15602
		var series = this,
15603
			seriesOptions = series.options,
15604
			data = series.data,
15605
			graph = series.graph,
15606
			area = series.area,
15607
			chart = series.chart,
15608
			names = series.xAxis && series.xAxis.names,
15609
			currentShift = (graph && graph.shift) || 0,
15610
			shiftShapes = ['graph', 'area'],
15611
			dataOptions = seriesOptions.data,
15612
			point,
15613
			isInTheMiddle,
15614
			xData = series.xData,
15615
			i,
15616
			x;
15617
 
15618
		setAnimation(animation, chart);
15619
 
15620
		// Make graph animate sideways
15621
		if (shift) {
15622
			i = series.zones.length;
15623
			while (i--) {
15624
				shiftShapes.push('zoneGraph' + i, 'zoneArea' + i);
15625
			}
15626
			each(shiftShapes, function (shape) {
15627
				if (series[shape]) {
15628
					series[shape].shift = currentShift + (seriesOptions.step ? 2 : 1);
15629
				}
15630
			});
15631
		}
15632
		if (area) {
15633
			area.isArea = true; // needed in animation, both with and without shift
15634
		}
15635
 
15636
		// Optional redraw, defaults to true
15637
		redraw = pick(redraw, true);
15638
 
15639
		// Get options and push the point to xData, yData and series.options. In series.generatePoints
15640
		// the Point instance will be created on demand and pushed to the series.data array.
15641
		point = { series: series };
15642
		series.pointClass.prototype.applyOptions.apply(point, [options]);
15643
		x = point.x;
15644
 
15645
		// Get the insertion point
15646
		i = xData.length;
15647
		if (series.requireSorting && x < xData[i - 1]) {
15648
			isInTheMiddle = true;
15649
			while (i && xData[i - 1] > x) {
15650
				i--;
15651
			}
15652
		}
15653
 
15654
		series.updateParallelArrays(point, 'splice', i, 0, 0); // insert undefined item
15655
		series.updateParallelArrays(point, i); // update it
15656
 
15657
		if (names && point.name) {
15658
			names[x] = point.name;
15659
		}
15660
		dataOptions.splice(i, 0, options);
15661
 
15662
		if (isInTheMiddle) {
15663
			series.data.splice(i, 0, null);
15664
			series.processData();
15665
		}
15666
 
15667
		// Generate points to be added to the legend (#1329)
15668
		if (seriesOptions.legendType === 'point') {
15669
			series.generatePoints();
15670
		}
15671
 
15672
		// Shift the first point off the parallel arrays
15673
		// todo: consider series.removePoint(i) method
15674
		if (shift) {
15675
			if (data[0] && data[0].remove) {
15676
				data[0].remove(false);
15677
			} else {
15678
				data.shift();
15679
				series.updateParallelArrays(point, 'shift');
15680
 
15681
				dataOptions.shift();
15682
			}
15683
		}
15684
 
15685
		// redraw
15686
		series.isDirty = true;
15687
		series.isDirtyData = true;
15688
		if (redraw) {
15689
			series.getAttribs(); // #1937
15690
			chart.redraw();
15691
		}
15692
	},
15693
 
15694
	/**
15695
	 * Remove a point (rendered or not), by index
15696
	 */
15697
	removePoint: function (i, redraw, animation) {
15698
 
15699
		var series = this,
15700
			data = series.data,
15701
			point = data[i],
15702
			points = series.points,
15703
			chart = series.chart,
15704
			remove = function () {
15705
 
15706
				if (data.length === points.length) {
15707
					points.splice(i, 1);
15708
				}
15709
				data.splice(i, 1);
15710
				series.options.data.splice(i, 1);
15711
				series.updateParallelArrays(point || { series: series }, 'splice', i, 1);
15712
 
15713
				if (point) {
15714
					point.destroy();
15715
				}
15716
 
15717
				// redraw
15718
				series.isDirty = true;
15719
				series.isDirtyData = true;
15720
				if (redraw) {
15721
					chart.redraw();
15722
				}
15723
			};
15724
 
15725
		setAnimation(animation, chart);
15726
		redraw = pick(redraw, true);
15727
 
15728
		// Fire the event with a default handler of removing the point
15729
		if (point) {
15730
			point.firePointEvent('remove', null, remove);
15731
		} else {
15732
			remove();
15733
		}
15734
	},
15735
 
15736
	/**
15737
	 * Remove a series and optionally redraw the chart
15738
	 *
15739
	 * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call
15740
	 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
15741
	 *    configuration
15742
	 */
15743
 
15744
	remove: function (redraw, animation) {
15745
		var series = this,
15746
			chart = series.chart;
15747
		redraw = pick(redraw, true);
15748
 
15749
		if (!series.isRemoving) {  /* prevent triggering native event in jQuery
15750
				(calling the remove function from the remove event) */
15751
			series.isRemoving = true;
15752
 
15753
			// fire the event with a default handler of removing the point
15754
			fireEvent(series, 'remove', null, function () {
15755
 
15756
 
15757
				// destroy elements
15758
				series.destroy();
15759
 
15760
 
15761
				// redraw
15762
				chart.isDirtyLegend = chart.isDirtyBox = true;
15763
				chart.linkSeries();
15764
 
15765
				if (redraw) {
15766
					chart.redraw(animation);
15767
				}
15768
			});
15769
 
15770
		}
15771
		series.isRemoving = false;
15772
	},
15773
 
15774
	/**
15775
	 * Update the series with a new set of options
15776
	 */
15777
	update: function (newOptions, redraw) {
15778
		var series = this,
15779
			chart = this.chart,
15780
			// must use user options when changing type because this.options is merged
15781
			// in with type specific plotOptions
15782
			oldOptions = this.userOptions,
15783
			oldType = this.type,
15784
			proto = seriesTypes[oldType].prototype,
15785
			preserve = ['group', 'markerGroup', 'dataLabelsGroup'],
15786
			n;
15787
 
15788
		// If we're changing type or zIndex, create new groups (#3380, #3404)
15789
		if ((newOptions.type && newOptions.type !== oldType) || newOptions.zIndex !== undefined) {
15790
			preserve.length = 0;
15791
		}
15792
 
15793
		// Make sure groups are not destroyed (#3094)
15794
		each(preserve, function (prop) {
15795
			preserve[prop] = series[prop];
15796
			delete series[prop];
15797
		});
15798
 
15799
		// Do the merge, with some forced options
15800
		newOptions = merge(oldOptions, {
15801
			animation: false,
15802
			index: this.index,
15803
			pointStart: this.xData[0] // when updating after addPoint
15804
		}, { data: this.options.data }, newOptions);
15805
 
15806
		// Destroy the series and delete all properties. Reinsert all methods
15807
		// and properties from the new type prototype (#2270, #3719)
15808
		this.remove(false);
15809
		for (n in proto) {
15810
			this[n] = UNDEFINED;
15811
		}
15812
		extend(this, seriesTypes[newOptions.type || oldType].prototype);
15813
 
15814
		// Re-register groups (#3094)
15815
		each(preserve, function (prop) {
15816
			series[prop] = preserve[prop];
15817
		});
15818
 
15819
		this.init(chart, newOptions);
15820
		chart.linkSeries(); // Links are lost in this.remove (#3028)
15821
		if (pick(redraw, true)) {
15822
			chart.redraw(false);
15823
		}
15824
	}
15825
});
15826
 
15827
// Extend the Axis.prototype for dynamic methods
15828
extend(Axis.prototype, {
15829
 
15830
	/**
15831
	 * Update the axis with a new options structure
15832
	 */
15833
	update: function (newOptions, redraw) {
15834
		var chart = this.chart;
15835
 
15836
		newOptions = chart.options[this.coll][this.options.index] = merge(this.userOptions, newOptions);
15837
 
15838
		this.destroy(true);
15839
		this._addedPlotLB = this.chart._labelPanes = UNDEFINED; // #1611, #2887, #4314
15840
 
15841
		this.init(chart, extend(newOptions, { events: UNDEFINED }));
15842
 
15843
		chart.isDirtyBox = true;
15844
		if (pick(redraw, true)) {
15845
			chart.redraw();
15846
		}
15847
	},
15848
 
15849
	/**
15850
     * Remove the axis from the chart
15851
     */
15852
	remove: function (redraw) {
15853
		var chart = this.chart,
15854
			key = this.coll, // xAxis or yAxis
15855
			axisSeries = this.series,
15856
			i = axisSeries.length;
15857
 
15858
		// Remove associated series (#2687)
15859
		while (i--) {
15860
			if (axisSeries[i]) {
15861
				axisSeries[i].remove(false);
15862
			}
15863
		}
15864
 
15865
		// Remove the axis
15866
		erase(chart.axes, this);
15867
		erase(chart[key], this);
15868
		chart.options[key].splice(this.options.index, 1);
15869
		each(chart[key], function (axis, i) { // Re-index, #1706
15870
			axis.options.index = i;
15871
		});
15872
		this.destroy();
15873
		chart.isDirtyBox = true;
15874
 
15875
		if (pick(redraw, true)) {
15876
			chart.redraw();
15877
		}
15878
	},
15879
 
15880
	/**
15881
	 * Update the axis title by options
15882
	 */
15883
	setTitle: function (newTitleOptions, redraw) {
15884
		this.update({ title: newTitleOptions }, redraw);
15885
	},
15886
 
15887
	/**
15888
	 * Set new axis categories and optionally redraw
15889
	 * @param {Array} categories
15890
	 * @param {Boolean} redraw
15891
	 */
15892
	setCategories: function (categories, redraw) {
15893
		this.update({ categories: categories }, redraw);
15894
	}
15895
 
15896
});
15897
 
15898
 
15899
/**
15900
 * LineSeries object
15901
 */
15902
var LineSeries = extendClass(Series);
15903
seriesTypes.line = LineSeries;
15904
 
15905
/**
15906
 * Set the default options for area
15907
 */
15908
defaultPlotOptions.area = merge(defaultSeriesOptions, {
15909
	softThreshold: false,
15910
	threshold: 0
15911
	// trackByArea: false,
15912
	// lineColor: null, // overrides color, but lets fillColor be unaltered
15913
	// fillOpacity: 0.75,
15914
	// fillColor: null
15915
});
15916
 
15917
/**
15918
 * AreaSeries object
15919
 */
15920
var AreaSeries = extendClass(Series, {
15921
	type: 'area',
15922
	/**
15923
	 * For stacks, don't split segments on null values. Instead, draw null values with
15924
	 * no marker. Also insert dummy points for any X position that exists in other series
15925
	 * in the stack.
15926
	 */
15927
	getSegments: function () {
15928
		var series = this,
15929
			segments = [],
15930
			segment = [],
15931
			keys = [],
15932
			xAxis = this.xAxis,
15933
			yAxis = this.yAxis,
15934
			stack = yAxis.stacks[this.stackKey],
15935
			pointMap = {},
15936
			plotX,
15937
			plotY,
15938
			points = this.points,
15939
			connectNulls = this.options.connectNulls,
15940
			stackIndicator,
15941
			i,
15942
			x;
15943
 
15944
		if (this.options.stacking && !this.cropped) { // cropped causes artefacts in Stock, and perf issue
15945
			// Create a map where we can quickly look up the points by their X value.
15946
			for (i = 0; i < points.length; i++) {
15947
				pointMap[points[i].x] = points[i];
15948
			}
15949
 
15950
			// Sort the keys (#1651)
15951
			for (x in stack) {
15952
				if (stack[x].total !== null) { // nulled after switching between grouping and not (#1651, #2336)
15953
					keys.push(+x);
15954
				}
15955
			}
15956
			keys.sort(function (a, b) {
15957
				return a - b;
15958
			});
15959
 
15960
			each(keys, function (x) {
15961
				var threshold = null,
15962
					stackPoint;
15963
 
15964
				if (connectNulls && (!pointMap[x] || pointMap[x].y === null)) { // #1836
15965
					return;
15966
 
15967
				// The point exists, push it to the segment
15968
				} else if (pointMap[x]) {
15969
					segment.push(pointMap[x]);
15970
 
15971
				// There is no point for this X value in this series, so we
15972
				// insert a dummy point in order for the areas to be drawn
15973
				// correctly.
15974
				} else {
15975
 
15976
					// Loop down the stack to find the series below this one that has
15977
					// a value (#1991)
15978
					for (i = series.index; i <= yAxis.series.length; i++) {
15979
						stackIndicator = series.getStackIndicator(null, x, i);
15980
						stackPoint = stack[x].points[stackIndicator.key];
15981
						if (stackPoint) {
15982
							threshold = stackPoint[1];
15983
							break;
15984
						}
15985
					}
15986
 
15987
					plotX = xAxis.translate(x);
15988
					plotY = yAxis.getThreshold(threshold);
15989
					segment.push({
15990
						y: null,
15991
						plotX: plotX,
15992
						clientX: plotX,
15993
						plotY: plotY,
15994
						yBottom: plotY,
15995
						onMouseOver: noop
15996
					});
15997
				}
15998
			});
15999
 
16000
			if (segment.length) {
16001
				segments.push(segment);
16002
			}
16003
 
16004
		} else {
16005
			Series.prototype.getSegments.call(this);
16006
			segments = this.segments;
16007
		}
16008
 
16009
		this.segments = segments;
16010
	},
16011
 
16012
	/**
16013
	 * Extend the base Series getSegmentPath method by adding the path for the area.
16014
	 * This path is pushed to the series.areaPath property.
16015
	 */
16016
	getSegmentPath: function (segment) {
16017
 
16018
		var segmentPath = Series.prototype.getSegmentPath.call(this, segment), // call base method
16019
			areaSegmentPath = [].concat(segmentPath), // work on a copy for the area path
16020
			i,
16021
			options = this.options,
16022
			segLength = segmentPath.length,
16023
			translatedThreshold = this.yAxis.getThreshold(options.threshold), // #2181
16024
			yBottom;
16025
 
16026
		if (segLength === 3) { // for animation from 1 to two points
16027
			areaSegmentPath.push(L, segmentPath[1], segmentPath[2]);
16028
		}
16029
		if (options.stacking && !this.closedStacks) {
16030
 
16031
			// Follow stack back. Todo: implement areaspline. A general solution could be to
16032
			// reverse the entire graphPath of the previous series, though may be hard with
16033
			// splines and with series with different extremes
16034
			for (i = segment.length - 1; i >= 0; i--) {
16035
 
16036
				yBottom = pick(segment[i].yBottom, translatedThreshold);
16037
 
16038
				// step line?
16039
				if (i < segment.length - 1 && options.step) {
16040
					areaSegmentPath.push(segment[i + 1].plotX, yBottom);
16041
				}
16042
 
16043
				areaSegmentPath.push(segment[i].plotX, yBottom);
16044
			}
16045
 
16046
		} else { // follow zero line back
16047
			this.closeSegment(areaSegmentPath, segment, translatedThreshold);
16048
		}
16049
		this.areaPath = this.areaPath.concat(areaSegmentPath);
16050
		return segmentPath;
16051
	},
16052
 
16053
	/**
16054
	 * Extendable method to close the segment path of an area. This is overridden in polar
16055
	 * charts.
16056
	 */
16057
	closeSegment: function (path, segment, translatedThreshold) {
16058
		path.push(
16059
			L,
16060
			segment[segment.length - 1].plotX,
16061
			translatedThreshold,
16062
			L,
16063
			segment[0].plotX,
16064
			translatedThreshold
16065
		);
16066
	},
16067
 
16068
	/**
16069
	 * Draw the graph and the underlying area. This method calls the Series base
16070
	 * function and adds the area. The areaPath is calculated in the getSegmentPath
16071
	 * method called from Series.prototype.drawGraph.
16072
	 */
16073
	drawGraph: function () {
16074
 
16075
		// Define or reset areaPath
16076
		this.areaPath = [];
16077
 
16078
		// Call the base method
16079
		Series.prototype.drawGraph.apply(this);
16080
 
16081
		// Define local variables
16082
		var series = this,
16083
			areaPath = this.areaPath,
16084
			options = this.options,
16085
			zones = this.zones,
16086
			props = [['area', this.color, options.fillColor]]; // area name, main color, fill color
16087
 
16088
		each(zones, function (threshold, i) {
16089
			props.push(['zoneArea' + i, threshold.color || series.color, threshold.fillColor || options.fillColor]);
16090
		});
16091
		each(props, function (prop) {
16092
			var areaKey = prop[0],
16093
				area = series[areaKey];
16094
 
16095
			// Create or update the area
16096
			if (area) { // update
16097
				area.animate({ d: areaPath });
16098
 
16099
			} else { // create
16100
				series[areaKey] = series.chart.renderer.path(areaPath)
16101
					.attr({
16102
						fill: pick(
16103
							prop[2],
16104
							Color(prop[1]).setOpacity(pick(options.fillOpacity, 0.75)).get()
16105
						),
16106
						zIndex: 0 // #1069
16107
					}).add(series.group);
16108
			}
16109
		});
16110
	},
16111
 
16112
	drawLegendSymbol: LegendSymbolMixin.drawRectangle
16113
});
16114
 
16115
seriesTypes.area = AreaSeries;
16116
/**
16117
 * Set the default options for spline
16118
 */
16119
defaultPlotOptions.spline = merge(defaultSeriesOptions);
16120
 
16121
/**
16122
 * SplineSeries object
16123
 */
16124
var SplineSeries = extendClass(Series, {
16125
	type: 'spline',
16126
 
16127
	/**
16128
	 * Get the spline segment from a given point's previous neighbour to the given point
16129
	 */
16130
	getPointSpline: function (segment, point, i) {
16131
		var smoothing = 1.5, // 1 means control points midway between points, 2 means 1/3 from the point, 3 is 1/4 etc
16132
			denom = smoothing + 1,
16133
			plotX = point.plotX,
16134
			plotY = point.plotY,
16135
			lastPoint = segment[i - 1],
16136
			nextPoint = segment[i + 1],
16137
			leftContX,
16138
			leftContY,
16139
			rightContX,
16140
			rightContY,
16141
			ret;
16142
 
16143
		// find control points
16144
		if (lastPoint && nextPoint) {
16145
 
16146
			var lastX = lastPoint.plotX,
16147
				lastY = lastPoint.plotY,
16148
				nextX = nextPoint.plotX,
16149
				nextY = nextPoint.plotY,
16150
				correction;
16151
 
16152
			leftContX = (smoothing * plotX + lastX) / denom;
16153
			leftContY = (smoothing * plotY + lastY) / denom;
16154
			rightContX = (smoothing * plotX + nextX) / denom;
16155
			rightContY = (smoothing * plotY + nextY) / denom;
16156
 
16157
			// have the two control points make a straight line through main point
16158
			correction = ((rightContY - leftContY) * (rightContX - plotX)) /
16159
				(rightContX - leftContX) + plotY - rightContY;
16160
 
16161
			leftContY += correction;
16162
			rightContY += correction;
16163
 
16164
			// to prevent false extremes, check that control points are between
16165
			// neighbouring points' y values
16166
			if (leftContY > lastY && leftContY > plotY) {
16167
				leftContY = mathMax(lastY, plotY);
16168
				rightContY = 2 * plotY - leftContY; // mirror of left control point
16169
			} else if (leftContY < lastY && leftContY < plotY) {
16170
				leftContY = mathMin(lastY, plotY);
16171
				rightContY = 2 * plotY - leftContY;
16172
			}
16173
			if (rightContY > nextY && rightContY > plotY) {
16174
				rightContY = mathMax(nextY, plotY);
16175
				leftContY = 2 * plotY - rightContY;
16176
			} else if (rightContY < nextY && rightContY < plotY) {
16177
				rightContY = mathMin(nextY, plotY);
16178
				leftContY = 2 * plotY - rightContY;
16179
			}
16180
 
16181
			// record for drawing in next point
16182
			point.rightContX = rightContX;
16183
			point.rightContY = rightContY;
16184
 
16185
		}
16186
 
16187
		// Visualize control points for debugging
16188
		/*
16189
		if (leftContX) {
16190
			this.chart.renderer.circle(leftContX + this.chart.plotLeft, leftContY + this.chart.plotTop, 2)
16191
				.attr({
16192
					stroke: 'red',
16193
					'stroke-width': 1,
16194
					fill: 'none'
16195
				})
16196
				.add();
16197
			this.chart.renderer.path(['M', leftContX + this.chart.plotLeft, leftContY + this.chart.plotTop,
16198
				'L', plotX + this.chart.plotLeft, plotY + this.chart.plotTop])
16199
				.attr({
16200
					stroke: 'red',
16201
					'stroke-width': 1
16202
				})
16203
				.add();
16204
			this.chart.renderer.circle(rightContX + this.chart.plotLeft, rightContY + this.chart.plotTop, 2)
16205
				.attr({
16206
					stroke: 'green',
16207
					'stroke-width': 1,
16208
					fill: 'none'
16209
				})
16210
				.add();
16211
			this.chart.renderer.path(['M', rightContX + this.chart.plotLeft, rightContY + this.chart.plotTop,
16212
				'L', plotX + this.chart.plotLeft, plotY + this.chart.plotTop])
16213
				.attr({
16214
					stroke: 'green',
16215
					'stroke-width': 1
16216
				})
16217
				.add();
16218
		}
16219
		*/
16220
 
16221
		// moveTo or lineTo
16222
		if (!i) {
16223
			ret = [M, plotX, plotY];
16224
		} else { // curve from last point to this
16225
			ret = [
16226
				'C',
16227
				lastPoint.rightContX || lastPoint.plotX,
16228
				lastPoint.rightContY || lastPoint.plotY,
16229
				leftContX || plotX,
16230
				leftContY || plotY,
16231
				plotX,
16232
				plotY
16233
			];
16234
			lastPoint.rightContX = lastPoint.rightContY = null; // reset for updating series later
16235
		}
16236
		return ret;
16237
	}
16238
});
16239
seriesTypes.spline = SplineSeries;
16240
 
16241
/**
16242
 * Set the default options for areaspline
16243
 */
16244
defaultPlotOptions.areaspline = merge(defaultPlotOptions.area);
16245
 
16246
/**
16247
 * AreaSplineSeries object
16248
 */
16249
var areaProto = AreaSeries.prototype,
16250
	AreaSplineSeries = extendClass(SplineSeries, {
16251
		type: 'areaspline',
16252
		closedStacks: true, // instead of following the previous graph back, follow the threshold back
16253
 
16254
		// Mix in methods from the area series
16255
		getSegmentPath: areaProto.getSegmentPath,
16256
		closeSegment: areaProto.closeSegment,
16257
		drawGraph: areaProto.drawGraph,
16258
		drawLegendSymbol: LegendSymbolMixin.drawRectangle
16259
	});
16260
 
16261
seriesTypes.areaspline = AreaSplineSeries;
16262
 
16263
/**
16264
 * Set the default options for column
16265
 */
16266
defaultPlotOptions.column = merge(defaultSeriesOptions, {
16267
	borderColor: '#FFFFFF',
16268
	//borderWidth: 1,
16269
	borderRadius: 0,
16270
	//colorByPoint: undefined,
16271
	groupPadding: 0.2,
16272
	//grouping: true,
16273
	marker: null, // point options are specified in the base options
16274
	pointPadding: 0.1,
16275
	//pointWidth: null,
16276
	minPointLength: 0,
16277
	cropThreshold: 50, // when there are more points, they will not animate out of the chart on xAxis.setExtremes
16278
	pointRange: null, // null means auto, meaning 1 in a categorized axis and least distance between points if not categories
16279
	states: {
16280
		hover: {
16281
			brightness: 0.1,
16282
			shadow: false,
16283
			halo: false
16284
		},
16285
		select: {
16286
			color: '#C0C0C0',
16287
			borderColor: '#000000',
16288
			shadow: false
16289
		}
16290
	},
16291
	dataLabels: {
16292
		align: null, // auto
16293
		verticalAlign: null, // auto
16294
		y: null
16295
	},
16296
	softThreshold: false,
16297
	startFromThreshold: true, // docs (but false doesn't work well): http://jsfiddle.net/highcharts/hz8fopan/14/
16298
	stickyTracking: false,
16299
	tooltip: {
16300
		distance: 6
16301
	},
16302
	threshold: 0
16303
});
16304
 
16305
/**
16306
 * ColumnSeries object
16307
 */
16308
var ColumnSeries = extendClass(Series, {
16309
	type: 'column',
16310
	pointAttrToOptions: { // mapping between SVG attributes and the corresponding options
16311
		stroke: 'borderColor',
16312
		fill: 'color',
16313
		r: 'borderRadius'
16314
	},
16315
	cropShoulder: 0,
16316
	directTouch: true, // When tooltip is not shared, this series (and derivatives) requires direct touch/hover. KD-tree does not apply.
16317
	trackerGroups: ['group', 'dataLabelsGroup'],
16318
	negStacks: true, // use separate negative stacks, unlike area stacks where a negative
16319
		// point is substracted from previous (#1910)
16320
 
16321
	/**
16322
	 * Initialize the series
16323
	 */
16324
	init: function () {
16325
		Series.prototype.init.apply(this, arguments);
16326
 
16327
		var series = this,
16328
			chart = series.chart;
16329
 
16330
		// if the series is added dynamically, force redraw of other
16331
		// series affected by a new column
16332
		if (chart.hasRendered) {
16333
			each(chart.series, function (otherSeries) {
16334
				if (otherSeries.type === series.type) {
16335
					otherSeries.isDirty = true;
16336
				}
16337
			});
16338
		}
16339
	},
16340
 
16341
	/**
16342
	 * Return the width and x offset of the columns adjusted for grouping, groupPadding, pointPadding,
16343
	 * pointWidth etc.
16344
	 */
16345
	getColumnMetrics: function () {
16346
 
16347
		var series = this,
16348
			options = series.options,
16349
			xAxis = series.xAxis,
16350
			yAxis = series.yAxis,
16351
			reversedXAxis = xAxis.reversed,
16352
			stackKey,
16353
			stackGroups = {},
16354
			columnIndex,
16355
			columnCount = 0;
16356
 
16357
		// Get the total number of column type series.
16358
		// This is called on every series. Consider moving this logic to a
16359
		// chart.orderStacks() function and call it on init, addSeries and removeSeries
16360
		if (options.grouping === false) {
16361
			columnCount = 1;
16362
		} else {
16363
			each(series.chart.series, function (otherSeries) {
16364
				var otherOptions = otherSeries.options,
16365
					otherYAxis = otherSeries.yAxis;
16366
				if (otherSeries.type === series.type && otherSeries.visible &&
16367
						yAxis.len === otherYAxis.len && yAxis.pos === otherYAxis.pos) {  // #642, #2086
16368
					if (otherOptions.stacking) {
16369
						stackKey = otherSeries.stackKey;
16370
						if (stackGroups[stackKey] === UNDEFINED) {
16371
							stackGroups[stackKey] = columnCount++;
16372
						}
16373
						columnIndex = stackGroups[stackKey];
16374
					} else if (otherOptions.grouping !== false) { // #1162
16375
						columnIndex = columnCount++;
16376
					}
16377
					otherSeries.columnIndex = columnIndex;
16378
				}
16379
			});
16380
		}
16381
 
16382
		var categoryWidth = mathMin(
16383
				mathAbs(xAxis.transA) * (xAxis.ordinalSlope || options.pointRange || xAxis.closestPointRange || xAxis.tickInterval || 1), // #2610
16384
				xAxis.len // #1535
16385
			),
16386
			groupPadding = categoryWidth * options.groupPadding,
16387
			groupWidth = categoryWidth - 2 * groupPadding,
16388
			pointOffsetWidth = groupWidth / columnCount,
16389
			pointWidth = mathMin(
16390
				options.maxPointWidth || xAxis.len,
16391
				pick(options.pointWidth, pointOffsetWidth * (1 - 2 * options.pointPadding))
16392
			),
16393
			pointPadding = (pointOffsetWidth - pointWidth) / 2,
16394
			colIndex = (reversedXAxis ?
16395
				columnCount - (series.columnIndex || 0) : // #1251
16396
				series.columnIndex) || 0,
16397
			pointXOffset = pointPadding + (groupPadding + colIndex *
16398
				pointOffsetWidth - (categoryWidth / 2)) *
16399
				(reversedXAxis ? -1 : 1);
16400
 
16401
		// Save it for reading in linked series (Error bars particularly)
16402
		return (series.columnMetrics = {
16403
			width: pointWidth,
16404
			offset: pointXOffset
16405
		});
16406
 
16407
	},
16408
 
16409
	/**
16410
	 * Make the columns crisp. The edges are rounded to the nearest full pixel.
16411
	 */
16412
	crispCol: function (x, y, w, h) {
16413
		var chart = this.chart,
16414
			borderWidth = this.borderWidth,
16415
			xCrisp = -(borderWidth % 2 ? 0.5 : 0),
16416
			yCrisp = borderWidth % 2 ? 0.5 : 1,
16417
			right,
16418
			bottom,
16419
			fromTop;
16420
 
16421
		if (chart.inverted && chart.renderer.isVML) {
16422
			yCrisp += 1;
16423
		}
16424
 
16425
		// Horizontal. We need to first compute the exact right edge, then round it
16426
		// and compute the width from there.
16427
		right = Math.round(x + w) + xCrisp;
16428
		x = Math.round(x) + xCrisp;
16429
		w = right - x;
16430
 
16431
		// Vertical
16432
		fromTop = mathAbs(y) <= 0.5; // #4504
16433
		bottom = Math.round(y + h) + yCrisp;
16434
		y = Math.round(y) + yCrisp;
16435
		h = bottom - y;
16436
 
16437
		// Top edges are exceptions
16438
		if (fromTop) {
16439
			y -= 1;
16440
			h += 1;
16441
		}
16442
 
16443
		return {
16444
			x: x,
16445
			y: y,
16446
			width: w,
16447
			height: h
16448
		};
16449
	},
16450
 
16451
	/**
16452
	 * Translate each point to the plot area coordinate system and find shape positions
16453
	 */
16454
	translate: function () {
16455
		var series = this,
16456
			chart = series.chart,
16457
			options = series.options,
16458
			borderWidth = series.borderWidth = pick(
16459
				options.borderWidth,
16460
				series.closestPointRange * series.xAxis.transA < 2 ? 0 : 1 // #3635
16461
			),
16462
			yAxis = series.yAxis,
16463
			threshold = options.threshold,
16464
			translatedThreshold = series.translatedThreshold = yAxis.getThreshold(threshold),
16465
			minPointLength = pick(options.minPointLength, 5),
16466
			metrics = series.getColumnMetrics(),
16467
			pointWidth = metrics.width,
16468
			seriesBarW = series.barW = mathMax(pointWidth, 1 + 2 * borderWidth), // postprocessed for border width
16469
			pointXOffset = series.pointXOffset = metrics.offset;
16470
 
16471
		if (chart.inverted) {
16472
			translatedThreshold -= 0.5; // #3355
16473
		}
16474
 
16475
		// When the pointPadding is 0, we want the columns to be packed tightly, so we allow individual
16476
		// columns to have individual sizes. When pointPadding is greater, we strive for equal-width
16477
		// columns (#2694).
16478
		if (options.pointPadding) {
16479
			seriesBarW = mathCeil(seriesBarW);
16480
		}
16481
 
16482
		Series.prototype.translate.apply(series);
16483
 
16484
		// Record the new values
16485
		each(series.points, function (point) {
16486
			var yBottom = mathMin(pick(point.yBottom, translatedThreshold), 9e4), // #3575
16487
				safeDistance = 999 + mathAbs(yBottom),
16488
				plotY = mathMin(mathMax(-safeDistance, point.plotY), yAxis.len + safeDistance), // Don't draw too far outside plot area (#1303, #2241, #4264)
16489
				barX = point.plotX + pointXOffset,
16490
				barW = seriesBarW,
16491
				barY = mathMin(plotY, yBottom),
16492
				up,
16493
				barH = mathMax(plotY, yBottom) - barY;
16494
 
16495
			// Handle options.minPointLength
16496
			if (mathAbs(barH) < minPointLength) {
16497
				if (minPointLength) {
16498
					barH = minPointLength;
16499
					up = (!yAxis.reversed && !point.negative) || (yAxis.reversed && point.negative);
16500
					barY = mathAbs(barY - translatedThreshold) > minPointLength ? // stacked
16501
							yBottom - minPointLength : // keep position
16502
							translatedThreshold - (up ? minPointLength : 0); // #1485, #4051
16503
				}
16504
			}
16505
 
16506
			// Cache for access in polar
16507
			point.barX = barX;
16508
			point.pointWidth = pointWidth;
16509
 
16510
			// Fix the tooltip on center of grouped columns (#1216, #424, #3648)
16511
			point.tooltipPos = chart.inverted ?
16512
				[yAxis.len + yAxis.pos - chart.plotLeft - plotY, series.xAxis.len - barX - barW / 2, barH] :
16513
				[barX + barW / 2, plotY + yAxis.pos - chart.plotTop, barH];
16514
 
16515
			// Register shape type and arguments to be used in drawPoints
16516
			point.shapeType = 'rect';
16517
			point.shapeArgs = series.crispCol(barX, barY, barW, barH);
16518
		});
16519
 
16520
	},
16521
 
16522
	getSymbol: noop,
16523
 
16524
	/**
16525
	 * Use a solid rectangle like the area series types
16526
	 */
16527
	drawLegendSymbol: LegendSymbolMixin.drawRectangle,
16528
 
16529
 
16530
	/**
16531
	 * Columns have no graph
16532
	 */
16533
	drawGraph: noop,
16534
 
16535
	/**
16536
	 * Draw the columns. For bars, the series.group is rotated, so the same coordinates
16537
	 * apply for columns and bars. This method is inherited by scatter series.
16538
	 *
16539
	 */
16540
	drawPoints: function () {
16541
		var series = this,
16542
			chart = this.chart,
16543
			options = series.options,
16544
			renderer = chart.renderer,
16545
			animationLimit = options.animationLimit || 250,
16546
			shapeArgs,
16547
			pointAttr;
16548
 
16549
		// draw the columns
16550
		each(series.points, function (point) {
16551
			var plotY = point.plotY,
16552
				graphic = point.graphic,
16553
				borderAttr;
16554
 
16555
			if (plotY !== UNDEFINED && !isNaN(plotY) && point.y !== null) {
16556
				shapeArgs = point.shapeArgs;
16557
 
16558
				borderAttr = defined(series.borderWidth) ? {
16559
					'stroke-width': series.borderWidth
16560
				} : {};
16561
 
16562
				pointAttr = point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE] || series.pointAttr[NORMAL_STATE];
16563
 
16564
				if (graphic) { // update
16565
					stop(graphic);
16566
					graphic.attr(borderAttr)[chart.pointCount < animationLimit ? 'animate' : 'attr'](merge(shapeArgs));
16567
 
16568
				} else {
16569
					point.graphic = graphic = renderer[point.shapeType](shapeArgs)
16570
						.attr(borderAttr)
16571
						.attr(pointAttr)
16572
						.add(point.group || series.group)
16573
						.shadow(options.shadow, null, options.stacking && !options.borderRadius);
16574
				}
16575
 
16576
			} else if (graphic) {
16577
				point.graphic = graphic.destroy(); // #1269
16578
			}
16579
		});
16580
	},
16581
 
16582
	/**
16583
	 * Animate the column heights one by one from zero
16584
	 * @param {Boolean} init Whether to initialize the animation or run it
16585
	 */
16586
	animate: function (init) {
16587
		var series = this,
16588
			yAxis = this.yAxis,
16589
			options = series.options,
16590
			inverted = this.chart.inverted,
16591
			attr = {},
16592
			translatedThreshold;
16593
 
16594
		if (hasSVG) { // VML is too slow anyway
16595
			if (init) {
16596
				attr.scaleY = 0.001;
16597
				translatedThreshold = mathMin(yAxis.pos + yAxis.len, mathMax(yAxis.pos, yAxis.toPixels(options.threshold)));
16598
				if (inverted) {
16599
					attr.translateX = translatedThreshold - yAxis.len;
16600
				} else {
16601
					attr.translateY = translatedThreshold;
16602
				}
16603
				series.group.attr(attr);
16604
 
16605
			} else { // run the animation
16606
 
16607
				attr.scaleY = 1;
16608
				attr[inverted ? 'translateX' : 'translateY'] = yAxis.pos;
16609
				series.group.animate(attr, series.options.animation);
16610
 
16611
				// delete this function to allow it only once
16612
				series.animate = null;
16613
			}
16614
		}
16615
	},
16616
 
16617
	/**
16618
	 * Remove this series from the chart
16619
	 */
16620
	remove: function () {
16621
		var series = this,
16622
			chart = series.chart;
16623
 
16624
		// column and bar series affects other series of the same type
16625
		// as they are either stacked or grouped
16626
		if (chart.hasRendered) {
16627
			each(chart.series, function (otherSeries) {
16628
				if (otherSeries.type === series.type) {
16629
					otherSeries.isDirty = true;
16630
				}
16631
			});
16632
		}
16633
 
16634
		Series.prototype.remove.apply(series, arguments);
16635
	}
16636
});
16637
seriesTypes.column = ColumnSeries;
16638
/**
16639
 * Set the default options for bar
16640
 */
16641
defaultPlotOptions.bar = merge(defaultPlotOptions.column);
16642
/**
16643
 * The Bar series class
16644
 */
16645
var BarSeries = extendClass(ColumnSeries, {
16646
	type: 'bar',
16647
	inverted: true
16648
});
16649
seriesTypes.bar = BarSeries;
16650
 
16651
/**
16652
 * Set the default options for scatter
16653
 */
16654
defaultPlotOptions.scatter = merge(defaultSeriesOptions, {
16655
	lineWidth: 0,
16656
	marker: {
16657
		enabled: true // Overrides auto-enabling in line series (#3647)
16658
	},
16659
	tooltip: {
16660
		headerFormat: '<span style="color:{point.color}">\u25CF</span> <span style="font-size: 10px;"> {series.name}</span><br/>',
16661
		pointFormat: 'x: <b>{point.x}</b><br/>y: <b>{point.y}</b><br/>'
16662
	}
16663
});
16664
 
16665
/**
16666
 * The scatter series class
16667
 */
16668
var ScatterSeries = extendClass(Series, {
16669
	type: 'scatter',
16670
	sorted: false,
16671
	requireSorting: false,
16672
	noSharedTooltip: true,
16673
	trackerGroups: ['group', 'markerGroup', 'dataLabelsGroup'],
16674
	takeOrdinalPosition: false, // #2342
16675
	kdDimensions: 2,
16676
	drawGraph: function () {
16677
		if (this.options.lineWidth) {
16678
			Series.prototype.drawGraph.call(this);
16679
		}
16680
	}
16681
});
16682
 
16683
seriesTypes.scatter = ScatterSeries;
16684
 
16685
/**
16686
 * Set the default options for pie
16687
 */
16688
defaultPlotOptions.pie = merge(defaultSeriesOptions, {
16689
	borderColor: '#FFFFFF',
16690
	borderWidth: 1,
16691
	center: [null, null],
16692
	clip: false,
16693
	colorByPoint: true, // always true for pies
16694
	dataLabels: {
16695
		// align: null,
16696
		// connectorWidth: 1,
16697
		// connectorColor: point.color,
16698
		// connectorPadding: 5,
16699
		distance: 30,
16700
		enabled: true,
16701
		formatter: function () { // #2945
16702
			return this.y === null ? undefined : this.point.name;
16703
		},
16704
		// softConnector: true,
16705
		x: 0
16706
		// y: 0
16707
	},
16708
	ignoreHiddenPoint: true,
16709
	//innerSize: 0,
16710
	legendType: 'point',
16711
	marker: null, // point options are specified in the base options
16712
	size: null,
16713
	showInLegend: false,
16714
	slicedOffset: 10,
16715
	states: {
16716
		hover: {
16717
			brightness: 0.1,
16718
			shadow: false
16719
		}
16720
	},
16721
	stickyTracking: false,
16722
	tooltip: {
16723
		followPointer: true
16724
	}
16725
});
16726
 
16727
/**
16728
 * Extended point object for pies
16729
 */
16730
var PiePoint = extendClass(Point, {
16731
	/**
16732
	 * Initiate the pie slice
16733
	 */
16734
	init: function () {
16735
 
16736
		Point.prototype.init.apply(this, arguments);
16737
 
16738
		var point = this,
16739
			toggleSlice;
16740
 
16741
		point.name = pick(point.name, 'Slice');
16742
 
16743
		// add event listener for select
16744
		toggleSlice = function (e) {
16745
			point.slice(e.type === 'select');
16746
		};
16747
		addEvent(point, 'select', toggleSlice);
16748
		addEvent(point, 'unselect', toggleSlice);
16749
 
16750
		return point;
16751
	},
16752
 
16753
	/**
16754
	 * Toggle the visibility of the pie slice
16755
	 * @param {Boolean} vis Whether to show the slice or not. If undefined, the
16756
	 *    visibility is toggled
16757
	 */
16758
	setVisible: function (vis, redraw) {
16759
		var point = this,
16760
			series = point.series,
16761
			chart = series.chart,
16762
			ignoreHiddenPoint = series.options.ignoreHiddenPoint;
16763
 
16764
		redraw = pick(redraw, ignoreHiddenPoint);
16765
 
16766
		if (vis !== point.visible) {
16767
 
16768
			// If called without an argument, toggle visibility
16769
			point.visible = point.options.visible = vis = vis === UNDEFINED ? !point.visible : vis;
16770
			series.options.data[inArray(point, series.data)] = point.options; // update userOptions.data
16771
 
16772
			// Show and hide associated elements. This is performed regardless of redraw or not,
16773
			// because chart.redraw only handles full series.
16774
			each(['graphic', 'dataLabel', 'connector', 'shadowGroup'], function (key) {
16775
				if (point[key]) {
16776
					point[key][vis ? 'show' : 'hide'](true);
16777
				}
16778
			});
16779
 
16780
			if (point.legendItem) {
16781
				chart.legend.colorizeItem(point, vis);
16782
			}
16783
 
16784
			// #4170, hide halo after hiding point
16785
			if (!vis && point.state === 'hover') {
16786
				point.setState('');
16787
			}
16788
 
16789
			// Handle ignore hidden slices
16790
			if (ignoreHiddenPoint) {
16791
				series.isDirty = true;
16792
			}
16793
 
16794
			if (redraw) {
16795
				chart.redraw();
16796
			}
16797
		}
16798
	},
16799
 
16800
	/**
16801
	 * Set or toggle whether the slice is cut out from the pie
16802
	 * @param {Boolean} sliced When undefined, the slice state is toggled
16803
	 * @param {Boolean} redraw Whether to redraw the chart. True by default.
16804
	 */
16805
	slice: function (sliced, redraw, animation) {
16806
		var point = this,
16807
			series = point.series,
16808
			chart = series.chart,
16809
			translation;
16810
 
16811
		setAnimation(animation, chart);
16812
 
16813
		// redraw is true by default
16814
		redraw = pick(redraw, true);
16815
 
16816
		// if called without an argument, toggle
16817
		point.sliced = point.options.sliced = sliced = defined(sliced) ? sliced : !point.sliced;
16818
		series.options.data[inArray(point, series.data)] = point.options; // update userOptions.data
16819
 
16820
		translation = sliced ? point.slicedTranslation : {
16821
			translateX: 0,
16822
			translateY: 0
16823
		};
16824
 
16825
		point.graphic.animate(translation);
16826
 
16827
		if (point.shadowGroup) {
16828
			point.shadowGroup.animate(translation);
16829
		}
16830
 
16831
	},
16832
 
16833
	haloPath: function (size) {
16834
		var shapeArgs = this.shapeArgs,
16835
			chart = this.series.chart;
16836
 
16837
		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, {
16838
			innerR: this.shapeArgs.r,
16839
			start: shapeArgs.start,
16840
			end: shapeArgs.end
16841
		});
16842
	}
16843
});
16844
 
16845
/**
16846
 * The Pie series class
16847
 */
16848
var PieSeries = {
16849
	type: 'pie',
16850
	isCartesian: false,
16851
	pointClass: PiePoint,
16852
	requireSorting: false,
16853
	directTouch: true,
16854
	noSharedTooltip: true,
16855
	trackerGroups: ['group', 'dataLabelsGroup'],
16856
	axisTypes: [],
16857
	pointAttrToOptions: { // mapping between SVG attributes and the corresponding options
16858
		stroke: 'borderColor',
16859
		'stroke-width': 'borderWidth',
16860
		fill: 'color'
16861
	},
16862
 
16863
	/**
16864
	 * Animate the pies in
16865
	 */
16866
	animate: function (init) {
16867
		var series = this,
16868
			points = series.points,
16869
			startAngleRad = series.startAngleRad;
16870
 
16871
		if (!init) {
16872
			each(points, function (point) {
16873
				var graphic = point.graphic,
16874
					args = point.shapeArgs;
16875
 
16876
				if (graphic) {
16877
					// start values
16878
					graphic.attr({
16879
						r: point.startR || (series.center[3] / 2), // animate from inner radius (#779)
16880
						start: startAngleRad,
16881
						end: startAngleRad
16882
					});
16883
 
16884
					// animate
16885
					graphic.animate({
16886
						r: args.r,
16887
						start: args.start,
16888
						end: args.end
16889
					}, series.options.animation);
16890
				}
16891
			});
16892
 
16893
			// delete this function to allow it only once
16894
			series.animate = null;
16895
		}
16896
	},
16897
 
16898
	/**
16899
	 * Recompute total chart sum and update percentages of points.
16900
	 */
16901
	updateTotals: function () {
16902
		var i,
16903
			total = 0,
16904
			points = this.points,
16905
			len = points.length,
16906
			point,
16907
			ignoreHiddenPoint = this.options.ignoreHiddenPoint;
16908
 
16909
		// Get the total sum
16910
		for (i = 0; i < len; i++) {
16911
			point = points[i];
16912
			total += (ignoreHiddenPoint && !point.visible) ? 0 : point.y;
16913
		}
16914
		this.total = total;
16915
 
16916
		// Set each point's properties
16917
		for (i = 0; i < len; i++) {
16918
			point = points[i];
16919
			point.percentage = (total > 0 && (point.visible || !ignoreHiddenPoint)) ? point.y / total * 100 : 0;
16920
			point.total = total;
16921
		}
16922
	},
16923
 
16924
	/**
16925
	 * Extend the generatePoints method by adding total and percentage properties to each point
16926
	 */
16927
	generatePoints: function () {
16928
		Series.prototype.generatePoints.call(this);
16929
		this.updateTotals();
16930
	},
16931
 
16932
	/**
16933
	 * Do translation for pie slices
16934
	 */
16935
	translate: function (positions) {
16936
		this.generatePoints();
16937
 
16938
		var series = this,
16939
			cumulative = 0,
16940
			precision = 1000, // issue #172
16941
			options = series.options,
16942
			slicedOffset = options.slicedOffset,
16943
			connectorOffset = slicedOffset + options.borderWidth,
16944
			start,
16945
			end,
16946
			angle,
16947
			startAngle = options.startAngle || 0,
16948
			startAngleRad = series.startAngleRad = mathPI / 180 * (startAngle - 90),
16949
			endAngleRad = series.endAngleRad = mathPI / 180 * ((pick(options.endAngle, startAngle + 360)) - 90),
16950
			circ = endAngleRad - startAngleRad, //2 * mathPI,
16951
			points = series.points,
16952
			radiusX, // the x component of the radius vector for a given point
16953
			radiusY,
16954
			labelDistance = options.dataLabels.distance,
16955
			ignoreHiddenPoint = options.ignoreHiddenPoint,
16956
			i,
16957
			len = points.length,
16958
			point;
16959
 
16960
		// Get positions - either an integer or a percentage string must be given.
16961
		// If positions are passed as a parameter, we're in a recursive loop for adjusting
16962
		// space for data labels.
16963
		if (!positions) {
16964
			series.center = positions = series.getCenter();
16965
		}
16966
 
16967
		// utility for getting the x value from a given y, used for anticollision logic in data labels
16968
		series.getX = function (y, left) {
16969
 
16970
			angle = math.asin(mathMin((y - positions[1]) / (positions[2] / 2 + labelDistance), 1));
16971
 
16972
			return positions[0] +
16973
				(left ? -1 : 1) *
16974
				(mathCos(angle) * (positions[2] / 2 + labelDistance));
16975
		};
16976
 
16977
		// Calculate the geometry for each point
16978
		for (i = 0; i < len; i++) {
16979
 
16980
			point = points[i];
16981
 
16982
			// set start and end angle
16983
			start = startAngleRad + (cumulative * circ);
16984
			if (!ignoreHiddenPoint || point.visible) {
16985
				cumulative += point.percentage / 100;
16986
			}
16987
			end = startAngleRad + (cumulative * circ);
16988
 
16989
			// set the shape
16990
			point.shapeType = 'arc';
16991
			point.shapeArgs = {
16992
				x: positions[0],
16993
				y: positions[1],
16994
				r: positions[2] / 2,
16995
				innerR: positions[3] / 2,
16996
				start: mathRound(start * precision) / precision,
16997
				end: mathRound(end * precision) / precision
16998
			};
16999
 
17000
			// The angle must stay within -90 and 270 (#2645)
17001
			angle = (end + start) / 2;
17002
			if (angle > 1.5 * mathPI) {
17003
				angle -= 2 * mathPI;
17004
			} else if (angle < -mathPI / 2) {
17005
				angle += 2 * mathPI;
17006
			}
17007
 
17008
			// Center for the sliced out slice
17009
			point.slicedTranslation = {
17010
				translateX: mathRound(mathCos(angle) * slicedOffset),
17011
				translateY: mathRound(mathSin(angle) * slicedOffset)
17012
			};
17013
 
17014
			// set the anchor point for tooltips
17015
			radiusX = mathCos(angle) * positions[2] / 2;
17016
			radiusY = mathSin(angle) * positions[2] / 2;
17017
			point.tooltipPos = [
17018
				positions[0] + radiusX * 0.7,
17019
				positions[1] + radiusY * 0.7
17020
			];
17021
 
17022
			point.half = angle < -mathPI / 2 || angle > mathPI / 2 ? 1 : 0;
17023
			point.angle = angle;
17024
 
17025
			// set the anchor point for data labels
17026
			connectorOffset = mathMin(connectorOffset, labelDistance / 2); // #1678
17027
			point.labelPos = [
17028
				positions[0] + radiusX + mathCos(angle) * labelDistance, // first break of connector
17029
				positions[1] + radiusY + mathSin(angle) * labelDistance, // a/a
17030
				positions[0] + radiusX + mathCos(angle) * connectorOffset, // second break, right outside pie
17031
				positions[1] + radiusY + mathSin(angle) * connectorOffset, // a/a
17032
				positions[0] + radiusX, // landing point for connector
17033
				positions[1] + radiusY, // a/a
17034
				labelDistance < 0 ? // alignment
17035
					'center' :
17036
					point.half ? 'right' : 'left', // alignment
17037
				angle // center angle
17038
			];
17039
 
17040
		}
17041
	},
17042
 
17043
	drawGraph: null,
17044
 
17045
	/**
17046
	 * Draw the data points
17047
	 */
17048
	drawPoints: function () {
17049
		var series = this,
17050
			chart = series.chart,
17051
			renderer = chart.renderer,
17052
			groupTranslation,
17053
			//center,
17054
			graphic,
17055
			//group,
17056
			shadow = series.options.shadow,
17057
			shadowGroup,
17058
			shapeArgs,
17059
			attr;
17060
 
17061
		if (shadow && !series.shadowGroup) {
17062
			series.shadowGroup = renderer.g('shadow')
17063
				.add(series.group);
17064
		}
17065
 
17066
		// draw the slices
17067
		each(series.points, function (point) {
17068
			if (point.y !== null) {
17069
				graphic = point.graphic;
17070
				shapeArgs = point.shapeArgs;
17071
				shadowGroup = point.shadowGroup;
17072
 
17073
				// put the shadow behind all points
17074
				if (shadow && !shadowGroup) {
17075
					shadowGroup = point.shadowGroup = renderer.g('shadow')
17076
						.add(series.shadowGroup);
17077
				}
17078
 
17079
				// if the point is sliced, use special translation, else use plot area traslation
17080
				groupTranslation = point.sliced ? point.slicedTranslation : {
17081
					translateX: 0,
17082
					translateY: 0
17083
				};
17084
 
17085
				//group.translate(groupTranslation[0], groupTranslation[1]);
17086
				if (shadowGroup) {
17087
					shadowGroup.attr(groupTranslation);
17088
				}
17089
 
17090
				// draw the slice
17091
				if (graphic) {
17092
					graphic
17093
						.setRadialReference(series.center)
17094
						.animate(extend(shapeArgs, groupTranslation));
17095
				} else {
17096
					attr = { 'stroke-linejoin': 'round' };
17097
					if (!point.visible) {
17098
						attr.visibility = 'hidden';
17099
					}
17100
 
17101
					point.graphic = graphic = renderer[point.shapeType](shapeArgs)
17102
						.setRadialReference(series.center)
17103
						.attr(
17104
							point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE]
17105
						)
17106
						.attr(attr)
17107
						.attr(groupTranslation)
17108
						.add(series.group)
17109
						.shadow(shadow, shadowGroup);
17110
				}
17111
			}
17112
		});
17113
 
17114
	},
17115
 
17116
 
17117
	searchPoint: noop,
17118
 
17119
	/**
17120
	 * Utility for sorting data labels
17121
	 */
17122
	sortByAngle: function (points, sign) {
17123
		points.sort(function (a, b) {
17124
			return a.angle !== undefined && (b.angle - a.angle) * sign;
17125
		});
17126
	},
17127
 
17128
	/**
17129
	 * Use a simple symbol from LegendSymbolMixin
17130
	 */
17131
	drawLegendSymbol: LegendSymbolMixin.drawRectangle,
17132
 
17133
	/**
17134
	 * Use the getCenter method from drawLegendSymbol
17135
	 */
17136
	getCenter: CenteredSeriesMixin.getCenter,
17137
 
17138
	/**
17139
	 * Pies don't have point marker symbols
17140
	 */
17141
	getSymbol: noop
17142
 
17143
};
17144
PieSeries = extendClass(Series, PieSeries);
17145
seriesTypes.pie = PieSeries;
17146
 
17147
/**
17148
 * Draw the data labels
17149
 */
17150
Series.prototype.drawDataLabels = function () {
17151
 
17152
	var series = this,
17153
		seriesOptions = series.options,
17154
		cursor = seriesOptions.cursor,
17155
		options = seriesOptions.dataLabels,
17156
		points = series.points,
17157
		pointOptions,
17158
		generalOptions,
17159
		hasRendered = series.hasRendered || 0,
17160
		str,
17161
		dataLabelsGroup,
17162
		renderer = series.chart.renderer;
17163
 
17164
	if (options.enabled || series._hasPointLabels) {
17165
 
17166
		// Process default alignment of data labels for columns
17167
		if (series.dlProcessOptions) {
17168
			series.dlProcessOptions(options);
17169
		}
17170
 
17171
		// Create a separate group for the data labels to avoid rotation
17172
		dataLabelsGroup = series.plotGroup(
17173
			'dataLabelsGroup',
17174
			'data-labels',
17175
			options.defer ? HIDDEN : VISIBLE,
17176
			options.zIndex || 6
17177
		);
17178
 
17179
		if (pick(options.defer, true)) {
17180
			dataLabelsGroup.attr({ opacity: +hasRendered }); // #3300
17181
			if (!hasRendered) {
17182
				addEvent(series, 'afterAnimate', function () {
17183
					if (series.visible) { // #3023, #3024
17184
						dataLabelsGroup.show();
17185
					}
17186
					dataLabelsGroup[seriesOptions.animation ? 'animate' : 'attr']({ opacity: 1 }, { duration: 200 });
17187
				});
17188
			}
17189
		}
17190
 
17191
		// Make the labels for each point
17192
		generalOptions = options;
17193
		each(points, function (point) {
17194
 
17195
			var enabled,
17196
				dataLabel = point.dataLabel,
17197
				labelConfig,
17198
				attr,
17199
				name,
17200
				rotation,
17201
				connector = point.connector,
17202
				isNew = true,
17203
				style,
17204
				moreStyle = {};
17205
 
17206
			// Determine if each data label is enabled
17207
			pointOptions = point.dlOptions || (point.options && point.options.dataLabels); // dlOptions is used in treemaps
17208
			enabled = pick(pointOptions && pointOptions.enabled, generalOptions.enabled); // #2282
17209
 
17210
 
17211
			// If the point is outside the plot area, destroy it. #678, #820
17212
			if (dataLabel && !enabled) {
17213
				point.dataLabel = dataLabel.destroy();
17214
 
17215
			// Individual labels are disabled if the are explicitly disabled
17216
			// in the point options, or if they fall outside the plot area.
17217
			} else if (enabled) {
17218
 
17219
				// Create individual options structure that can be extended without
17220
				// affecting others
17221
				options = merge(generalOptions, pointOptions);
17222
				style = options.style;
17223
 
17224
				rotation = options.rotation;
17225
 
17226
				// Get the string
17227
				labelConfig = point.getLabelConfig();
17228
				str = options.format ?
17229
					format(options.format, labelConfig) :
17230
					options.formatter.call(labelConfig, options);
17231
 
17232
				// Determine the color
17233
				style.color = pick(options.color, style.color, series.color, 'black');
17234
 
17235
 
17236
				// update existing label
17237
				if (dataLabel) {
17238
 
17239
					if (defined(str)) {
17240
						dataLabel
17241
							.attr({
17242
								text: str
17243
							});
17244
						isNew = false;
17245
 
17246
					} else { // #1437 - the label is shown conditionally
17247
						point.dataLabel = dataLabel = dataLabel.destroy();
17248
						if (connector) {
17249
							point.connector = connector.destroy();
17250
						}
17251
					}
17252
 
17253
				// create new label
17254
				} else if (defined(str)) {
17255
					attr = {
17256
						//align: align,
17257
						fill: options.backgroundColor,
17258
						stroke: options.borderColor,
17259
						'stroke-width': options.borderWidth,
17260
						r: options.borderRadius || 0,
17261
						rotation: rotation,
17262
						padding: options.padding,
17263
						zIndex: 1
17264
					};
17265
 
17266
					// Get automated contrast color
17267
					if (style.color === 'contrast') {
17268
						moreStyle.color = options.inside || options.distance < 0 || !!seriesOptions.stacking ?
17269
							renderer.getContrast(point.color || series.color) :
17270
							'#000000';
17271
					}
17272
					if (cursor) {
17273
						moreStyle.cursor = cursor;
17274
					}
17275
 
17276
 
17277
					// Remove unused attributes (#947)
17278
					for (name in attr) {
17279
						if (attr[name] === UNDEFINED) {
17280
							delete attr[name];
17281
						}
17282
					}
17283
 
17284
					dataLabel = point.dataLabel = renderer[rotation ? 'text' : 'label']( // labels don't support rotation
17285
						str,
17286
						0,
17287
						-999,
17288
						options.shape,
17289
						null,
17290
						null,
17291
						options.useHTML
17292
					)
17293
					.attr(attr)
17294
					.css(extend(style, moreStyle))
17295
					.add(dataLabelsGroup)
17296
					.shadow(options.shadow);
17297
 
17298
				}
17299
 
17300
				if (dataLabel) {
17301
					// Now the data label is created and placed at 0,0, so we need to align it
17302
					series.alignDataLabel(point, dataLabel, options, null, isNew);
17303
				}
17304
			}
17305
		});
17306
	}
17307
};
17308
 
17309
/**
17310
 * Align each individual data label
17311
 */
17312
Series.prototype.alignDataLabel = function (point, dataLabel, options, alignTo, isNew) {
17313
	var chart = this.chart,
17314
		inverted = chart.inverted,
17315
		plotX = pick(point.plotX, -999),
17316
		plotY = pick(point.plotY, -999),
17317
		bBox = dataLabel.getBBox(),
17318
		baseline = chart.renderer.fontMetrics(options.style.fontSize).b,
17319
		rotCorr, // rotation correction
17320
		// Math.round for rounding errors (#2683), alignTo to allow column labels (#2700)
17321
		visible = this.visible && (point.series.forceDL || chart.isInsidePlot(plotX, mathRound(plotY), inverted) ||
17322
			(alignTo && chart.isInsidePlot(plotX, inverted ? alignTo.x + 1 : alignTo.y + alignTo.height - 1, inverted))),
17323
		alignAttr; // the final position;
17324
 
17325
	if (visible) {
17326
 
17327
		// The alignment box is a singular point
17328
		alignTo = extend({
17329
			x: inverted ? chart.plotWidth - plotY : plotX,
17330
			y: mathRound(inverted ? chart.plotHeight - plotX : plotY),
17331
			width: 0,
17332
			height: 0
17333
		}, alignTo);
17334
 
17335
		// Add the text size for alignment calculation
17336
		extend(options, {
17337
			width: bBox.width,
17338
			height: bBox.height
17339
		});
17340
 
17341
		// Allow a hook for changing alignment in the last moment, then do the alignment
17342
		if (options.rotation) { // Fancy box alignment isn't supported for rotated text
17343
			rotCorr = chart.renderer.rotCorr(baseline, options.rotation); // #3723
17344
			dataLabel[isNew ? 'attr' : 'animate']({
17345
					x: alignTo.x + options.x + alignTo.width / 2 + rotCorr.x,
17346
					y: alignTo.y + options.y + alignTo.height / 2
17347
				})
17348
				.attr({ // #3003
17349
					align: options.align
17350
				});
17351
		} else {
17352
			dataLabel.align(options, null, alignTo);
17353
			alignAttr = dataLabel.alignAttr;
17354
 
17355
			// Handle justify or crop
17356
			if (pick(options.overflow, 'justify') === 'justify') {
17357
				this.justifyDataLabel(dataLabel, options, alignAttr, bBox, alignTo, isNew);
17358
 
17359
			} else if (pick(options.crop, true)) {
17360
				// Now check that the data label is within the plot area
17361
				visible = chart.isInsidePlot(alignAttr.x, alignAttr.y) && chart.isInsidePlot(alignAttr.x + bBox.width, alignAttr.y + bBox.height);
17362
 
17363
			}
17364
 
17365
			// When we're using a shape, make it possible with a connector or an arrow pointing to thie point
17366
			if (options.shape) {
17367
				dataLabel.attr({
17368
					anchorX: point.plotX,
17369
					anchorY: point.plotY
17370
				});
17371
			}
17372
 
17373
		}
17374
	}
17375
 
17376
	// Show or hide based on the final aligned position
17377
	if (!visible) {
17378
		stop(dataLabel);
17379
		dataLabel.attr({ y: -999 });
17380
		dataLabel.placed = false; // don't animate back in
17381
	}
17382
 
17383
};
17384
 
17385
/**
17386
 * If data labels fall partly outside the plot area, align them back in, in a way that
17387
 * doesn't hide the point.
17388
 */
17389
Series.prototype.justifyDataLabel = function (dataLabel, options, alignAttr, bBox, alignTo, isNew) {
17390
	var chart = this.chart,
17391
		align = options.align,
17392
		verticalAlign = options.verticalAlign,
17393
		off,
17394
		justified,
17395
		padding = dataLabel.box ? 0 : (dataLabel.padding || 0);
17396
 
17397
	// Off left
17398
	off = alignAttr.x + padding;
17399
	if (off < 0) {
17400
		if (align === 'right') {
17401
			options.align = 'left';
17402
		} else {
17403
			options.x = -off;
17404
		}
17405
		justified = true;
17406
	}
17407
 
17408
	// Off right
17409
	off = alignAttr.x + bBox.width - padding;
17410
	if (off > chart.plotWidth) {
17411
		if (align === 'left') {
17412
			options.align = 'right';
17413
		} else {
17414
			options.x = chart.plotWidth - off;
17415
		}
17416
		justified = true;
17417
	}
17418
 
17419
	// Off top
17420
	off = alignAttr.y + padding;
17421
	if (off < 0) {
17422
		if (verticalAlign === 'bottom') {
17423
			options.verticalAlign = 'top';
17424
		} else {
17425
			options.y = -off;
17426
		}
17427
		justified = true;
17428
	}
17429
 
17430
	// Off bottom
17431
	off = alignAttr.y + bBox.height - padding;
17432
	if (off > chart.plotHeight) {
17433
		if (verticalAlign === 'top') {
17434
			options.verticalAlign = 'bottom';
17435
		} else {
17436
			options.y = chart.plotHeight - off;
17437
		}
17438
		justified = true;
17439
	}
17440
 
17441
	if (justified) {
17442
		dataLabel.placed = !isNew;
17443
		dataLabel.align(options, null, alignTo);
17444
	}
17445
};
17446
 
17447
/**
17448
 * Override the base drawDataLabels method by pie specific functionality
17449
 */
17450
if (seriesTypes.pie) {
17451
	seriesTypes.pie.prototype.drawDataLabels = function () {
17452
		var series = this,
17453
			data = series.data,
17454
			point,
17455
			chart = series.chart,
17456
			options = series.options.dataLabels,
17457
			connectorPadding = pick(options.connectorPadding, 10),
17458
			connectorWidth = pick(options.connectorWidth, 1),
17459
			plotWidth = chart.plotWidth,
17460
			plotHeight = chart.plotHeight,
17461
			connector,
17462
			connectorPath,
17463
			softConnector = pick(options.softConnector, true),
17464
			distanceOption = options.distance,
17465
			seriesCenter = series.center,
17466
			radius = seriesCenter[2] / 2,
17467
			centerY = seriesCenter[1],
17468
			outside = distanceOption > 0,
17469
			dataLabel,
17470
			dataLabelWidth,
17471
			labelPos,
17472
			labelHeight,
17473
			halves = [// divide the points into right and left halves for anti collision
17474
				[], // right
17475
				[]  // left
17476
			],
17477
			x,
17478
			y,
17479
			visibility,
17480
			rankArr,
17481
			i,
17482
			j,
17483
			overflow = [0, 0, 0, 0], // top, right, bottom, left
17484
			sort = function (a, b) {
17485
				return b.y - a.y;
17486
			};
17487
 
17488
		// get out if not enabled
17489
		if (!series.visible || (!options.enabled && !series._hasPointLabels)) {
17490
			return;
17491
		}
17492
 
17493
		// run parent method
17494
		Series.prototype.drawDataLabels.apply(series);
17495
 
17496
		// arrange points for detection collision
17497
		each(data, function (point) {
17498
			if (point.dataLabel && point.visible) { // #407, #2510
17499
				halves[point.half].push(point);
17500
			}
17501
		});
17502
 
17503
		/* Loop over the points in each half, starting from the top and bottom
17504
		 * of the pie to detect overlapping labels.
17505
		 */
17506
		i = 2;
17507
		while (i--) {
17508
 
17509
			var slots = [],
17510
				slotsLength,
17511
				usedSlots = [],
17512
				points = halves[i],
17513
				pos,
17514
				bottom,
17515
				length = points.length,
17516
				slotIndex;
17517
 
17518
			if (!length) {
17519
				continue;
17520
			}
17521
 
17522
			// Sort by angle
17523
			series.sortByAngle(points, i - 0.5);
17524
 
17525
			// Assume equal label heights on either hemisphere (#2630)
17526
			j = labelHeight = 0;
17527
			while (!labelHeight && points[j]) { // #1569
17528
				labelHeight = points[j] && points[j].dataLabel && (points[j].dataLabel.getBBox().height || 21); // 21 is for #968
17529
				j++;
17530
			}
17531
 
17532
			// Only do anti-collision when we are outside the pie and have connectors (#856)
17533
			if (distanceOption > 0) {
17534
 
17535
				// Build the slots
17536
				bottom = mathMin(centerY + radius + distanceOption, chart.plotHeight);
17537
				for (pos = mathMax(0, centerY - radius - distanceOption); pos <= bottom; pos += labelHeight) {
17538
					slots.push(pos);
17539
				}
17540
				slotsLength = slots.length;
17541
 
17542
 
17543
				/* Visualize the slots
17544
				if (!series.slotElements) {
17545
					series.slotElements = [];
17546
				}
17547
				if (i === 1) {
17548
					series.slotElements.forEach(function (elem) {
17549
						elem.destroy();
17550
					});
17551
					series.slotElements.length = 0;
17552
				}
17553
 
17554
				slots.forEach(function (pos, no) {
17555
					var slotX = series.getX(pos, i) + chart.plotLeft - (i ? 100 : 0),
17556
						slotY = pos + chart.plotTop;
17557
 
17558
					if (!isNaN(slotX)) {
17559
						series.slotElements.push(chart.renderer.rect(slotX, slotY - 7, 100, labelHeight, 1)
17560
							.attr({
17561
								'stroke-width': 1,
17562
								stroke: 'silver',
17563
								fill: 'rgba(0,0,255,0.1)'
17564
							})
17565
							.add());
17566
						series.slotElements.push(chart.renderer.text('Slot '+ no, slotX, slotY + 4)
17567
							.attr({
17568
								fill: 'silver'
17569
							}).add());
17570
					}
17571
				});
17572
				// */
17573
 
17574
				// if there are more values than available slots, remove lowest values
17575
				if (length > slotsLength) {
17576
					// create an array for sorting and ranking the points within each quarter
17577
					rankArr = [].concat(points);
17578
					rankArr.sort(sort);
17579
					j = length;
17580
					while (j--) {
17581
						rankArr[j].rank = j;
17582
					}
17583
					j = length;
17584
					while (j--) {
17585
						if (points[j].rank >= slotsLength) {
17586
							points.splice(j, 1);
17587
						}
17588
					}
17589
					length = points.length;
17590
				}
17591
 
17592
				// The label goes to the nearest open slot, but not closer to the edge than
17593
				// the label's index.
17594
				for (j = 0; j < length; j++) {
17595
 
17596
					point = points[j];
17597
					labelPos = point.labelPos;
17598
 
17599
					var closest = 9999,
17600
						distance,
17601
						slotI;
17602
 
17603
					// find the closest slot index
17604
					for (slotI = 0; slotI < slotsLength; slotI++) {
17605
						distance = mathAbs(slots[slotI] - labelPos[1]);
17606
						if (distance < closest) {
17607
							closest = distance;
17608
							slotIndex = slotI;
17609
						}
17610
					}
17611
 
17612
					// if that slot index is closer to the edges of the slots, move it
17613
					// to the closest appropriate slot
17614
					if (slotIndex < j && slots[j] !== null) { // cluster at the top
17615
						slotIndex = j;
17616
					} else if (slotsLength  < length - j + slotIndex && slots[j] !== null) { // cluster at the bottom
17617
						slotIndex = slotsLength - length + j;
17618
						while (slots[slotIndex] === null) { // make sure it is not taken
17619
							slotIndex++;
17620
						}
17621
					} else {
17622
						// Slot is taken, find next free slot below. In the next run, the next slice will find the
17623
						// slot above these, because it is the closest one
17624
						while (slots[slotIndex] === null) { // make sure it is not taken
17625
							slotIndex++;
17626
						}
17627
					}
17628
 
17629
					usedSlots.push({ i: slotIndex, y: slots[slotIndex] });
17630
					slots[slotIndex] = null; // mark as taken
17631
				}
17632
				// sort them in order to fill in from the top
17633
				usedSlots.sort(sort);
17634
			}
17635
 
17636
			// now the used slots are sorted, fill them up sequentially
17637
			for (j = 0; j < length; j++) {
17638
 
17639
				var slot, naturalY;
17640
 
17641
				point = points[j];
17642
				labelPos = point.labelPos;
17643
				dataLabel = point.dataLabel;
17644
				visibility = point.visible === false ? HIDDEN : 'inherit';
17645
				naturalY = labelPos[1];
17646
 
17647
				if (distanceOption > 0) {
17648
					slot = usedSlots.pop();
17649
					slotIndex = slot.i;
17650
 
17651
					// if the slot next to currrent slot is free, the y value is allowed
17652
					// to fall back to the natural position
17653
					y = slot.y;
17654
					if ((naturalY > y && slots[slotIndex + 1] !== null) ||
17655
							(naturalY < y &&  slots[slotIndex - 1] !== null)) {
17656
						y = mathMin(mathMax(0, naturalY), chart.plotHeight);
17657
					}
17658
 
17659
				} else {
17660
					y = naturalY;
17661
				}
17662
 
17663
				// get the x - use the natural x position for first and last slot, to prevent the top
17664
				// and botton slice connectors from touching each other on either side
17665
				x = options.justify ?
17666
					seriesCenter[0] + (i ? -1 : 1) * (radius + distanceOption) :
17667
					series.getX(y === centerY - radius - distanceOption || y === centerY + radius + distanceOption ? naturalY : y, i);
17668
 
17669
 
17670
				// Record the placement and visibility
17671
				dataLabel._attr = {
17672
					visibility: visibility,
17673
					align: labelPos[6]
17674
				};
17675
				dataLabel._pos = {
17676
					x: x + options.x +
17677
						({ left: connectorPadding, right: -connectorPadding }[labelPos[6]] || 0),
17678
					y: y + options.y - 10 // 10 is for the baseline (label vs text)
17679
				};
17680
				dataLabel.connX = x;
17681
				dataLabel.connY = y;
17682
 
17683
 
17684
				// Detect overflowing data labels
17685
				if (this.options.size === null) {
17686
					dataLabelWidth = dataLabel.width;
17687
					// Overflow left
17688
					if (x - dataLabelWidth < connectorPadding) {
17689
						overflow[3] = mathMax(mathRound(dataLabelWidth - x + connectorPadding), overflow[3]);
17690
 
17691
					// Overflow right
17692
					} else if (x + dataLabelWidth > plotWidth - connectorPadding) {
17693
						overflow[1] = mathMax(mathRound(x + dataLabelWidth - plotWidth + connectorPadding), overflow[1]);
17694
					}
17695
 
17696
					// Overflow top
17697
					if (y - labelHeight / 2 < 0) {
17698
						overflow[0] = mathMax(mathRound(-y + labelHeight / 2), overflow[0]);
17699
 
17700
					// Overflow left
17701
					} else if (y + labelHeight / 2 > plotHeight) {
17702
						overflow[2] = mathMax(mathRound(y + labelHeight / 2 - plotHeight), overflow[2]);
17703
					}
17704
				}
17705
			} // for each point
17706
		} // for each half
17707
 
17708
		// Do not apply the final placement and draw the connectors until we have verified
17709
		// that labels are not spilling over.
17710
		if (arrayMax(overflow) === 0 || this.verifyDataLabelOverflow(overflow)) {
17711
 
17712
			// Place the labels in the final position
17713
			this.placeDataLabels();
17714
 
17715
			// Draw the connectors
17716
			if (outside && connectorWidth) {
17717
				each(this.points, function (point) {
17718
					connector = point.connector;
17719
					labelPos = point.labelPos;
17720
					dataLabel = point.dataLabel;
17721
 
17722
					if (dataLabel && dataLabel._pos && point.visible) {
17723
						visibility = dataLabel._attr.visibility;
17724
						x = dataLabel.connX;
17725
						y = dataLabel.connY;
17726
						connectorPath = softConnector ? [
17727
							M,
17728
							x + (labelPos[6] === 'left' ? 5 : -5), y, // end of the string at the label
17729
							'C',
17730
							x, y, // first break, next to the label
17731
							2 * labelPos[2] - labelPos[4], 2 * labelPos[3] - labelPos[5],
17732
							labelPos[2], labelPos[3], // second break
17733
							L,
17734
							labelPos[4], labelPos[5] // base
17735
						] : [
17736
							M,
17737
							x + (labelPos[6] === 'left' ? 5 : -5), y, // end of the string at the label
17738
							L,
17739
							labelPos[2], labelPos[3], // second break
17740
							L,
17741
							labelPos[4], labelPos[5] // base
17742
						];
17743
 
17744
						if (connector) {
17745
							connector.animate({ d: connectorPath });
17746
							connector.attr('visibility', visibility);
17747
 
17748
						} else {
17749
							point.connector = connector = series.chart.renderer.path(connectorPath).attr({
17750
								'stroke-width': connectorWidth,
17751
								stroke: options.connectorColor || point.color || '#606060',
17752
								visibility: visibility
17753
								//zIndex: 0 // #2722 (reversed)
17754
							})
17755
							.add(series.dataLabelsGroup);
17756
						}
17757
					} else if (connector) {
17758
						point.connector = connector.destroy();
17759
					}
17760
				});
17761
			}
17762
		}
17763
	};
17764
	/**
17765
	 * Perform the final placement of the data labels after we have verified that they
17766
	 * fall within the plot area.
17767
	 */
17768
	seriesTypes.pie.prototype.placeDataLabels = function () {
17769
		each(this.points, function (point) {
17770
			var dataLabel = point.dataLabel,
17771
				_pos;
17772
 
17773
			if (dataLabel && point.visible) {
17774
				_pos = dataLabel._pos;
17775
				if (_pos) {
17776
					dataLabel.attr(dataLabel._attr);
17777
					dataLabel[dataLabel.moved ? 'animate' : 'attr'](_pos);
17778
					dataLabel.moved = true;
17779
				} else if (dataLabel) {
17780
					dataLabel.attr({ y: -999 });
17781
				}
17782
			}
17783
		});
17784
	};
17785
 
17786
	seriesTypes.pie.prototype.alignDataLabel =  noop;
17787
 
17788
	/**
17789
	 * Verify whether the data labels are allowed to draw, or we should run more translation and data
17790
	 * label positioning to keep them inside the plot area. Returns true when data labels are ready
17791
	 * to draw.
17792
	 */
17793
	seriesTypes.pie.prototype.verifyDataLabelOverflow = function (overflow) {
17794
 
17795
		var center = this.center,
17796
			options = this.options,
17797
			centerOption = options.center,
17798
			minSize = options.minSize || 80,
17799
			newSize = minSize,
17800
			ret;
17801
 
17802
		// Handle horizontal size and center
17803
		if (centerOption[0] !== null) { // Fixed center
17804
			newSize = mathMax(center[2] - mathMax(overflow[1], overflow[3]), minSize);
17805
 
17806
		} else { // Auto center
17807
			newSize = mathMax(
17808
				center[2] - overflow[1] - overflow[3], // horizontal overflow
17809
				minSize
17810
			);
17811
			center[0] += (overflow[3] - overflow[1]) / 2; // horizontal center
17812
		}
17813
 
17814
		// Handle vertical size and center
17815
		if (centerOption[1] !== null) { // Fixed center
17816
			newSize = mathMax(mathMin(newSize, center[2] - mathMax(overflow[0], overflow[2])), minSize);
17817
 
17818
		} else { // Auto center
17819
			newSize = mathMax(
17820
				mathMin(
17821
					newSize,
17822
					center[2] - overflow[0] - overflow[2] // vertical overflow
17823
				),
17824
				minSize
17825
			);
17826
			center[1] += (overflow[0] - overflow[2]) / 2; // vertical center
17827
		}
17828
 
17829
		// If the size must be decreased, we need to run translate and drawDataLabels again
17830
		if (newSize < center[2]) {
17831
			center[2] = newSize;
17832
			center[3] = Math.min(relativeLength(options.innerSize || 0, newSize), newSize); // #3632
17833
			this.translate(center);
17834
			each(this.points, function (point) {
17835
				if (point.dataLabel) {
17836
					point.dataLabel._pos = null; // reset
17837
				}
17838
			});
17839
 
17840
			if (this.drawDataLabels) {
17841
				this.drawDataLabels();
17842
			}
17843
		// Else, return true to indicate that the pie and its labels is within the plot area
17844
		} else {
17845
			ret = true;
17846
		}
17847
		return ret;
17848
	};
17849
}
17850
 
17851
if (seriesTypes.column) {
17852
 
17853
	/**
17854
	 * Override the basic data label alignment by adjusting for the position of the column
17855
	 */
17856
	seriesTypes.column.prototype.alignDataLabel = function (point, dataLabel, options,  alignTo, isNew) {
17857
		var inverted = this.chart.inverted,
17858
			series = point.series,
17859
			dlBox = point.dlBox || point.shapeArgs, // data label box for alignment
17860
			below = pick(point.below, point.plotY > pick(this.translatedThreshold, series.yAxis.len)), // point.below is used in range series
17861
			inside = pick(options.inside, !!this.options.stacking); // draw it inside the box?
17862
 
17863
		// Align to the column itself, or the top of it
17864
		if (dlBox) { // Area range uses this method but not alignTo
17865
			alignTo = merge(dlBox);
17866
 
17867
			if (inverted) {
17868
				alignTo = {
17869
					x: series.yAxis.len - alignTo.y - alignTo.height,
17870
					y: series.xAxis.len - alignTo.x - alignTo.width,
17871
					width: alignTo.height,
17872
					height: alignTo.width
17873
				};
17874
			}
17875
 
17876
			// Compute the alignment box
17877
			if (!inside) {
17878
				if (inverted) {
17879
					alignTo.x += below ? 0 : alignTo.width;
17880
					alignTo.width = 0;
17881
				} else {
17882
					alignTo.y += below ? alignTo.height : 0;
17883
					alignTo.height = 0;
17884
				}
17885
			}
17886
		}
17887
 
17888
 
17889
		// When alignment is undefined (typically columns and bars), display the individual
17890
		// point below or above the point depending on the threshold
17891
		options.align = pick(
17892
			options.align,
17893
			!inverted || inside ? 'center' : below ? 'right' : 'left'
17894
		);
17895
		options.verticalAlign = pick(
17896
			options.verticalAlign,
17897
			inverted || inside ? 'middle' : below ? 'top' : 'bottom'
17898
		);
17899
 
17900
		// Call the parent method
17901
		Series.prototype.alignDataLabel.call(this, point, dataLabel, options, alignTo, isNew);
17902
	};
17903
}
17904
 
17905
 
17906
 
17907
/**
17908
 * Highstock JS v2.1.9 (2015-10-07)
17909
 * Highcharts module to hide overlapping data labels. This module is included by default in Highmaps.
17910
 *
17911
 * (c) 2010-2014 Torstein Honsi
17912
 *
17913
 * License: www.highcharts.com/license
17914
 */
17915
 
17916
/*global Highcharts, HighchartsAdapter */
17917
(function (H) {
17918
	var Chart = H.Chart,
17919
		each = H.each,
17920
		pick = H.pick,
17921
		addEvent = HighchartsAdapter.addEvent;
17922
 
17923
	// Collect potensial overlapping data labels. Stack labels probably don't need to be
17924
	// considered because they are usually accompanied by data labels that lie inside the columns.
17925
	Chart.prototype.callbacks.push(function (chart) {
17926
		function collectAndHide() {
17927
			var labels = [];
17928
 
17929
			each(chart.series, function (series) {
17930
				var dlOptions = series.options.dataLabels,
17931
					collections = series.dataLabelCollections || ['dataLabel']; // Range series have two collections
17932
				if ((dlOptions.enabled || series._hasPointLabels) && !dlOptions.allowOverlap && series.visible) { // #3866
17933
					each(collections, function (coll) {
17934
						each(series.points, function (point) {
17935
							if (point[coll]) {
17936
								point[coll].labelrank = pick(point.labelrank, point.shapeArgs && point.shapeArgs.height); // #4118
17937
								labels.push(point[coll]);
17938
							}
17939
						});
17940
					});
17941
				}
17942
			});
17943
			chart.hideOverlappingLabels(labels);
17944
		}
17945
 
17946
		// Do it now ...
17947
		collectAndHide();
17948
 
17949
		// ... and after each chart redraw
17950
		addEvent(chart, 'redraw', collectAndHide);
17951
 
17952
	});
17953
 
17954
	/**
17955
	 * Hide overlapping labels. Labels are moved and faded in and out on zoom to provide a smooth
17956
	 * visual imression.
17957
	 */
17958
	Chart.prototype.hideOverlappingLabels = function (labels) {
17959
 
17960
		var len = labels.length,
17961
			label,
17962
			i,
17963
			j,
17964
			label1,
17965
			label2,
17966
			isIntersecting,
17967
			pos1,
17968
			pos2,
17969
			padding,
17970
			intersectRect = function (x1, y1, w1, h1, x2, y2, w2, h2) {
17971
				return !(
17972
					x2 > x1 + w1 ||
17973
					x2 + w2 < x1 ||
17974
					y2 > y1 + h1 ||
17975
					y2 + h2 < y1
17976
				);
17977
			};
17978
 
17979
		// Mark with initial opacity
17980
		for (i = 0; i < len; i++) {
17981
			label = labels[i];
17982
			if (label) {
17983
				label.oldOpacity = label.opacity;
17984
				label.newOpacity = 1;
17985
			}
17986
		}
17987
 
17988
		// Prevent a situation in a gradually rising slope, that each label
17989
		// will hide the previous one because the previous one always has
17990
		// lower rank.
17991
		labels.sort(function (a, b) {
17992
			return (b.labelrank || 0) - (a.labelrank || 0);
17993
		});
17994
 
17995
		// Detect overlapping labels
17996
		for (i = 0; i < len; i++) {
17997
			label1 = labels[i];
17998
 
17999
			for (j = i + 1; j < len; ++j) {
18000
				label2 = labels[j];
18001
				if (label1 && label2 && label1.placed && label2.placed && label1.newOpacity !== 0 && label2.newOpacity !== 0) {
18002
					pos1 = label1.alignAttr;
18003
					pos2 = label2.alignAttr;
18004
					padding = 2 * (label1.box ? 0 : label1.padding); // Substract the padding if no background or border (#4333)
18005
					isIntersecting = intersectRect(
18006
						pos1.x,
18007
						pos1.y,
18008
						label1.width - padding,
18009
						label1.height - padding,
18010
						pos2.x,
18011
						pos2.y,
18012
						label2.width - padding,
18013
						label2.height - padding
18014
					);
18015
 
18016
					if (isIntersecting) {
18017
						(label1.labelrank < label2.labelrank ? label1 : label2).newOpacity = 0;
18018
					}
18019
				}
18020
			}
18021
		}
18022
 
18023
		// Hide or show
18024
		each(labels, function (label) {
18025
			var complete,
18026
				newOpacity;
18027
 
18028
			if (label) {
18029
				newOpacity = label.newOpacity;
18030
 
18031
				if (label.oldOpacity !== newOpacity && label.placed) {
18032
 
18033
					// Make sure the label is completely hidden to avoid catching clicks (#4362)
18034
					if (newOpacity) {
18035
						label.show(true);
18036
					} else {
18037
						complete = function () {
18038
							label.hide();
18039
						};
18040
					}
18041
 
18042
					// Animate or set the opacity
18043
					label.alignAttr.opacity = newOpacity;
18044
					label[label.isOld ? 'animate' : 'attr'](label.alignAttr, null, complete);
18045
 
18046
				}
18047
				label.isOld = true;
18048
			}
18049
		});
18050
	};
18051
 
18052
}(Highcharts));/**
18053
 * TrackerMixin for points and graphs
18054
 */
18055
 
18056
var TrackerMixin = Highcharts.TrackerMixin = {
18057
 
18058
	drawTrackerPoint: function () {
18059
		var series = this,
18060
			chart = series.chart,
18061
			pointer = chart.pointer,
18062
			cursor = series.options.cursor,
18063
			css = cursor && { cursor: cursor },
18064
			onMouseOver = function (e) {
18065
				var target = e.target,
18066
				point;
18067
 
18068
				while (target && !point) {
18069
					point = target.point;
18070
					target = target.parentNode;
18071
				}
18072
 
18073
				if (point !== UNDEFINED && point !== chart.hoverPoint) { // undefined on graph in scatterchart
18074
					point.onMouseOver(e);
18075
				}
18076
			};
18077
 
18078
		// Add reference to the point
18079
		each(series.points, function (point) {
18080
			if (point.graphic) {
18081
				point.graphic.element.point = point;
18082
			}
18083
			if (point.dataLabel) {
18084
				point.dataLabel.element.point = point;
18085
			}
18086
		});
18087
 
18088
		// Add the event listeners, we need to do this only once
18089
		if (!series._hasTracking) {
18090
			each(series.trackerGroups, function (key) {
18091
				if (series[key]) { // we don't always have dataLabelsGroup
18092
					series[key]
18093
						.addClass(PREFIX + 'tracker')
18094
						.on('mouseover', onMouseOver)
18095
						.on('mouseout', function (e) { pointer.onTrackerMouseOut(e); })
18096
						.css(css);
18097
					if (hasTouch) {
18098
						series[key].on('touchstart', onMouseOver);
18099
					}
18100
				}
18101
			});
18102
			series._hasTracking = true;
18103
		}
18104
	},
18105
 
18106
	/**
18107
	 * Draw the tracker object that sits above all data labels and markers to
18108
	 * track mouse events on the graph or points. For the line type charts
18109
	 * the tracker uses the same graphPath, but with a greater stroke width
18110
	 * for better control.
18111
	 */
18112
	drawTrackerGraph: function () {
18113
		var series = this,
18114
			options = series.options,
18115
			trackByArea = options.trackByArea,
18116
			trackerPath = [].concat(trackByArea ? series.areaPath : series.graphPath),
18117
			trackerPathLength = trackerPath.length,
18118
			chart = series.chart,
18119
			pointer = chart.pointer,
18120
			renderer = chart.renderer,
18121
			snap = chart.options.tooltip.snap,
18122
			tracker = series.tracker,
18123
			cursor = options.cursor,
18124
			css = cursor && { cursor: cursor },
18125
			singlePoints = series.singlePoints,
18126
			singlePoint,
18127
			i,
18128
			onMouseOver = function () {
18129
				if (chart.hoverSeries !== series) {
18130
					series.onMouseOver();
18131
				}
18132
			},
18133
			/*
18134
			 * Empirical lowest possible opacities for TRACKER_FILL for an element to stay invisible but clickable
18135
			 * IE6: 0.002
18136
			 * IE7: 0.002
18137
			 * IE8: 0.002
18138
			 * IE9: 0.00000000001 (unlimited)
18139
			 * IE10: 0.0001 (exporting only)
18140
			 * FF: 0.00000000001 (unlimited)
18141
			 * Chrome: 0.000001
18142
			 * Safari: 0.000001
18143
			 * Opera: 0.00000000001 (unlimited)
18144
			 */
18145
			TRACKER_FILL = 'rgba(192,192,192,' + (hasSVG ? 0.0001 : 0.002) + ')';
18146
 
18147
		// Extend end points. A better way would be to use round linecaps,
18148
		// but those are not clickable in VML.
18149
		if (trackerPathLength && !trackByArea) {
18150
			i = trackerPathLength + 1;
18151
			while (i--) {
18152
				if (trackerPath[i] === M) { // extend left side
18153
					trackerPath.splice(i + 1, 0, trackerPath[i + 1] - snap, trackerPath[i + 2], L);
18154
				}
18155
				if ((i && trackerPath[i] === M) || i === trackerPathLength) { // extend right side
18156
					trackerPath.splice(i, 0, L, trackerPath[i - 2] + snap, trackerPath[i - 1]);
18157
				}
18158
			}
18159
		}
18160
 
18161
		// handle single points
18162
		for (i = 0; i < singlePoints.length; i++) {
18163
			singlePoint = singlePoints[i];
18164
			trackerPath.push(M, singlePoint.plotX - snap, singlePoint.plotY,
18165
			L, singlePoint.plotX + snap, singlePoint.plotY);
18166
		}
18167
 
18168
		// draw the tracker
18169
		if (tracker) {
18170
			tracker.attr({ d: trackerPath });
18171
		} else { // create
18172
 
18173
			series.tracker = renderer.path(trackerPath)
18174
			.attr({
18175
				'stroke-linejoin': 'round', // #1225
18176
				visibility: series.visible ? VISIBLE : HIDDEN,
18177
				stroke: TRACKER_FILL,
18178
				fill: trackByArea ? TRACKER_FILL : NONE,
18179
				'stroke-width' : options.lineWidth + (trackByArea ? 0 : 2 * snap),
18180
				zIndex: 2
18181
			})
18182
			.add(series.group);
18183
 
18184
			// The tracker is added to the series group, which is clipped, but is covered
18185
			// by the marker group. So the marker group also needs to capture events.
18186
			each([series.tracker, series.markerGroup], function (tracker) {
18187
				tracker.addClass(PREFIX + 'tracker')
18188
					.on('mouseover', onMouseOver)
18189
					.on('mouseout', function (e) { pointer.onTrackerMouseOut(e); })
18190
					.css(css);
18191
 
18192
				if (hasTouch) {
18193
					tracker.on('touchstart', onMouseOver);
18194
				}
18195
			});
18196
		}
18197
	}
18198
};
18199
/* End TrackerMixin */
18200
 
18201
 
18202
/**
18203
 * Add tracking event listener to the series group, so the point graphics
18204
 * themselves act as trackers
18205
 */
18206
 
18207
if (seriesTypes.column) {
18208
	ColumnSeries.prototype.drawTracker = TrackerMixin.drawTrackerPoint;
18209
}
18210
 
18211
if (seriesTypes.pie) {
18212
	seriesTypes.pie.prototype.drawTracker = TrackerMixin.drawTrackerPoint;
18213
}
18214
 
18215
if (seriesTypes.scatter) {
18216
	ScatterSeries.prototype.drawTracker = TrackerMixin.drawTrackerPoint;
18217
}
18218
 
18219
/*
18220
 * Extend Legend for item events
18221
 */
18222
extend(Legend.prototype, {
18223
 
18224
	setItemEvents: function (item, legendItem, useHTML, itemStyle, itemHiddenStyle) {
18225
	var legend = this;
18226
	// Set the events on the item group, or in case of useHTML, the item itself (#1249)
18227
	(useHTML ? legendItem : item.legendGroup).on('mouseover', function () {
18228
			item.setState(HOVER_STATE);
18229
			legendItem.css(legend.options.itemHoverStyle);
18230
		})
18231
		.on('mouseout', function () {
18232
			legendItem.css(item.visible ? itemStyle : itemHiddenStyle);
18233
			item.setState();
18234
		})
18235
		.on('click', function (event) {
18236
			var strLegendItemClick = 'legendItemClick',
18237
				fnLegendItemClick = function () {
18238
					if (item.setVisible) {
18239
						item.setVisible();
18240
					}
18241
				};
18242
 
18243
			// Pass over the click/touch event. #4.
18244
			event = {
18245
				browserEvent: event
18246
			};
18247
 
18248
			// click the name or symbol
18249
			if (item.firePointEvent) { // point
18250
				item.firePointEvent(strLegendItemClick, event, fnLegendItemClick);
18251
			} else {
18252
				fireEvent(item, strLegendItemClick, event, fnLegendItemClick);
18253
			}
18254
		});
18255
	},
18256
 
18257
	createCheckboxForItem: function (item) {
18258
		var legend = this;
18259
 
18260
		item.checkbox = createElement('input', {
18261
			type: 'checkbox',
18262
			checked: item.selected,
18263
			defaultChecked: item.selected // required by IE7
18264
		}, legend.options.itemCheckboxStyle, legend.chart.container);
18265
 
18266
		addEvent(item.checkbox, 'click', function (event) {
18267
			var target = event.target;
18268
			fireEvent(item.series || item, 'checkboxClick', { // #3712
18269
					checked: target.checked,
18270
					item: item
18271
				},
18272
				function () {
18273
					item.select();
18274
				}
18275
			);
18276
		});
18277
	}
18278
});
18279
 
18280
/*
18281
 * Add pointer cursor to legend itemstyle in defaultOptions
18282
 */
18283
defaultOptions.legend.itemStyle.cursor = 'pointer';
18284
 
18285
 
18286
/*
18287
 * Extend the Chart object with interaction
18288
 */
18289
 
18290
extend(Chart.prototype, {
18291
	/**
18292
	 * Display the zoom button
18293
	 */
18294
	showResetZoom: function () {
18295
		var chart = this,
18296
			lang = defaultOptions.lang,
18297
			btnOptions = chart.options.chart.resetZoomButton,
18298
			theme = btnOptions.theme,
18299
			states = theme.states,
18300
			alignTo = btnOptions.relativeTo === 'chart' ? null : 'plotBox';
18301
 
18302
		this.resetZoomButton = chart.renderer.button(lang.resetZoom, null, null, function () { chart.zoomOut(); }, theme, states && states.hover)
18303
			.attr({
18304
				align: btnOptions.position.align,
18305
				title: lang.resetZoomTitle
18306
			})
18307
			.add()
18308
			.align(btnOptions.position, false, alignTo);
18309
 
18310
	},
18311
 
18312
	/**
18313
	 * Zoom out to 1:1
18314
	 */
18315
	zoomOut: function () {
18316
		var chart = this;
18317
		fireEvent(chart, 'selection', { resetSelection: true }, function () {
18318
			chart.zoom();
18319
		});
18320
	},
18321
 
18322
	/**
18323
	 * Zoom into a given portion of the chart given by axis coordinates
18324
	 * @param {Object} event
18325
	 */
18326
	zoom: function (event) {
18327
		var chart = this,
18328
			hasZoomed,
18329
			pointer = chart.pointer,
18330
			displayButton = false,
18331
			resetZoomButton;
18332
 
18333
		// If zoom is called with no arguments, reset the axes
18334
		if (!event || event.resetSelection) {
18335
			each(chart.axes, function (axis) {
18336
				hasZoomed = axis.zoom();
18337
			});
18338
		} else { // else, zoom in on all axes
18339
			each(event.xAxis.concat(event.yAxis), function (axisData) {
18340
				var axis = axisData.axis,
18341
					isXAxis = axis.isXAxis;
18342
 
18343
				// don't zoom more than minRange
18344
				if (pointer[isXAxis ? 'zoomX' : 'zoomY'] || pointer[isXAxis ? 'pinchX' : 'pinchY']) {
18345
					hasZoomed = axis.zoom(axisData.min, axisData.max);
18346
					if (axis.displayBtn) {
18347
						displayButton = true;
18348
					}
18349
				}
18350
			});
18351
		}
18352
 
18353
		// Show or hide the Reset zoom button
18354
		resetZoomButton = chart.resetZoomButton;
18355
		if (displayButton && !resetZoomButton) {
18356
			chart.showResetZoom();
18357
		} else if (!displayButton && isObject(resetZoomButton)) {
18358
			chart.resetZoomButton = resetZoomButton.destroy();
18359
		}
18360
 
18361
 
18362
		// Redraw
18363
		if (hasZoomed) {
18364
			chart.redraw(
18365
				pick(chart.options.chart.animation, event && event.animation, chart.pointCount < 100) // animation
18366
			);
18367
		}
18368
	},
18369
 
18370
	/**
18371
	 * Pan the chart by dragging the mouse across the pane. This function is called
18372
	 * on mouse move, and the distance to pan is computed from chartX compared to
18373
	 * the first chartX position in the dragging operation.
18374
	 */
18375
	pan: function (e, panning) {
18376
 
18377
		var chart = this,
18378
			hoverPoints = chart.hoverPoints,
18379
			doRedraw;
18380
 
18381
		// remove active points for shared tooltip
18382
		if (hoverPoints) {
18383
			each(hoverPoints, function (point) {
18384
				point.setState();
18385
			});
18386
		}
18387
 
18388
		each(panning === 'xy' ? [1, 0] : [1], function (isX) { // xy is used in maps
18389
			var mousePos = e[isX ? 'chartX' : 'chartY'],
18390
				axis = chart[isX ? 'xAxis' : 'yAxis'][0],
18391
				startPos = chart[isX ? 'mouseDownX' : 'mouseDownY'],
18392
				halfPointRange = (axis.pointRange || 0) / 2,
18393
				extremes = axis.getExtremes(),
18394
				newMin = axis.toValue(startPos - mousePos, true) + halfPointRange,
18395
				newMax = axis.toValue(startPos + chart[isX ? 'plotWidth' : 'plotHeight'] - mousePos, true) - halfPointRange,
18396
				goingLeft = startPos > mousePos; // #3613
18397
 
18398
			if (axis.series.length &&
18399
					(goingLeft || newMin > mathMin(extremes.dataMin, extremes.min)) &&
18400
					(!goingLeft || newMax < mathMax(extremes.dataMax, extremes.max))) {
18401
				axis.setExtremes(newMin, newMax, false, false, { trigger: 'pan' });
18402
				doRedraw = true;
18403
			}
18404
 
18405
			chart[isX ? 'mouseDownX' : 'mouseDownY'] = mousePos; // set new reference for next run
18406
		});
18407
 
18408
		if (doRedraw) {
18409
			chart.redraw(false);
18410
		}
18411
		css(chart.container, { cursor: 'move' });
18412
	}
18413
});
18414
 
18415
/*
18416
 * Extend the Point object with interaction
18417
 */
18418
extend(Point.prototype, {
18419
	/**
18420
	 * Toggle the selection status of a point
18421
	 * @param {Boolean} selected Whether to select or unselect the point.
18422
	 * @param {Boolean} accumulate Whether to add to the previous selection. By default,
18423
	 *		 this happens if the control key (Cmd on Mac) was pressed during clicking.
18424
	 */
18425
	select: function (selected, accumulate) {
18426
		var point = this,
18427
			series = point.series,
18428
			chart = series.chart;
18429
 
18430
		selected = pick(selected, !point.selected);
18431
 
18432
		// fire the event with the defalut handler
18433
		point.firePointEvent(selected ? 'select' : 'unselect', { accumulate: accumulate }, function () {
18434
			point.selected = point.options.selected = selected;
18435
			series.options.data[inArray(point, series.data)] = point.options;
18436
 
18437
			point.setState(selected && SELECT_STATE);
18438
 
18439
			// unselect all other points unless Ctrl or Cmd + click
18440
			if (!accumulate) {
18441
				each(chart.getSelectedPoints(), function (loopPoint) {
18442
					if (loopPoint.selected && loopPoint !== point) {
18443
						loopPoint.selected = loopPoint.options.selected = false;
18444
						series.options.data[inArray(loopPoint, series.data)] = loopPoint.options;
18445
						loopPoint.setState(NORMAL_STATE);
18446
							loopPoint.firePointEvent('unselect');
18447
					}
18448
				});
18449
			}
18450
		});
18451
	},
18452
 
18453
	/**
18454
	 * Runs on mouse over the point
18455
	 *
18456
	 * @param {Object} e The event arguments
18457
	 * @param {Boolean} byProximity Falsy for kd points that are closest to the mouse, or to
18458
	 *        actually hovered points. True for other points in shared tooltip.
18459
	 */
18460
	onMouseOver: function (e, byProximity) {
18461
		var point = this,
18462
			series = point.series,
18463
			chart = series.chart,
18464
			tooltip = chart.tooltip,
18465
			hoverPoint = chart.hoverPoint;
18466
 
18467
		if (chart.hoverSeries !== series) {
18468
			series.onMouseOver();
18469
		}
18470
 
18471
		// set normal state to previous series
18472
		if (hoverPoint && hoverPoint !== point) {
18473
			hoverPoint.onMouseOut();
18474
		}
18475
 
18476
		if (point.series) { // It may have been destroyed, #4130
18477
 
18478
			// trigger the event
18479
			point.firePointEvent('mouseOver');
18480
 
18481
			// update the tooltip
18482
			if (tooltip && (!tooltip.shared || series.noSharedTooltip)) {
18483
				tooltip.refresh(point, e);
18484
			}
18485
 
18486
			// hover this
18487
			point.setState(HOVER_STATE);
18488
			if (!byProximity) {
18489
				chart.hoverPoint = point;
18490
			}
18491
		}
18492
	},
18493
 
18494
	/**
18495
	 * Runs on mouse out from the point
18496
	 */
18497
	onMouseOut: function () {
18498
		var chart = this.series.chart,
18499
			hoverPoints = chart.hoverPoints;
18500
 
18501
		this.firePointEvent('mouseOut');
18502
 
18503
		if (!hoverPoints || inArray(this, hoverPoints) === -1) { // #887, #2240
18504
			this.setState();
18505
			chart.hoverPoint = null;
18506
		}
18507
	},
18508
 
18509
	/**
18510
	 * Import events from the series' and point's options. Only do it on
18511
	 * demand, to save processing time on hovering.
18512
	 */
18513
	importEvents: function () {
18514
		if (!this.hasImportedEvents) {
18515
			var point = this,
18516
				options = merge(point.series.options.point, point.options),
18517
				events = options.events,
18518
				eventType;
18519
 
18520
			point.events = events;
18521
 
18522
			for (eventType in events) {
18523
				addEvent(point, eventType, events[eventType]);
18524
			}
18525
			this.hasImportedEvents = true;
18526
 
18527
		}
18528
	},
18529
 
18530
	/**
18531
	 * Set the point's state
18532
	 * @param {String} state
18533
	 */
18534
	setState: function (state, move) {
18535
		var point = this,
18536
			plotX = mathFloor(point.plotX), // #4586
18537
			plotY = point.plotY,
18538
			series = point.series,
18539
			stateOptions = series.options.states,
18540
			markerOptions = defaultPlotOptions[series.type].marker && series.options.marker,
18541
			normalDisabled = markerOptions && !markerOptions.enabled,
18542
			markerStateOptions = markerOptions && markerOptions.states[state],
18543
			stateDisabled = markerStateOptions && markerStateOptions.enabled === false,
18544
			stateMarkerGraphic = series.stateMarkerGraphic,
18545
			pointMarker = point.marker || {},
18546
			chart = series.chart,
18547
			radius,
18548
			halo = series.halo,
18549
			haloOptions,
18550
			newSymbol,
18551
			pointAttr;
18552
 
18553
		state = state || NORMAL_STATE; // empty string
18554
		pointAttr = point.pointAttr[state] || series.pointAttr[state];
18555
 
18556
		if (
18557
				// already has this state
18558
				(state === point.state && !move) ||
18559
				// selected points don't respond to hover
18560
				(point.selected && state !== SELECT_STATE) ||
18561
				// series' state options is disabled
18562
				(stateOptions[state] && stateOptions[state].enabled === false) ||
18563
				// general point marker's state options is disabled
18564
				(state && (stateDisabled || (normalDisabled && markerStateOptions.enabled === false))) ||
18565
				// individual point marker's state options is disabled
18566
				(state && pointMarker.states && pointMarker.states[state] && pointMarker.states[state].enabled === false) // #1610
18567
 
18568
			) {
18569
			return;
18570
		}
18571
 
18572
		// apply hover styles to the existing point
18573
		if (point.graphic) {
18574
			radius = markerOptions && point.graphic.symbolName && pointAttr.r;
18575
			point.graphic.attr(merge(
18576
				pointAttr,
18577
				radius ? { // new symbol attributes (#507, #612)
18578
					x: plotX - radius,
18579
					y: plotY - radius,
18580
					width: 2 * radius,
18581
					height: 2 * radius
18582
				} : {}
18583
			));
18584
 
18585
			// Zooming in from a range with no markers to a range with markers
18586
			if (stateMarkerGraphic) {
18587
				stateMarkerGraphic.hide();
18588
			}
18589
		} else {
18590
			// if a graphic is not applied to each point in the normal state, create a shared
18591
			// graphic for the hover state
18592
			if (state && markerStateOptions) {
18593
				radius = markerStateOptions.radius;
18594
				newSymbol = pointMarker.symbol || series.symbol;
18595
 
18596
				// If the point has another symbol than the previous one, throw away the
18597
				// state marker graphic and force a new one (#1459)
18598
				if (stateMarkerGraphic && stateMarkerGraphic.currentSymbol !== newSymbol) {
18599
					stateMarkerGraphic = stateMarkerGraphic.destroy();
18600
				}
18601
 
18602
				// Add a new state marker graphic
18603
				if (!stateMarkerGraphic) {
18604
					if (newSymbol) {
18605
						series.stateMarkerGraphic = stateMarkerGraphic = chart.renderer.symbol(
18606
							newSymbol,
18607
							plotX - radius,
18608
							plotY - radius,
18609
							2 * radius,
18610
							2 * radius
18611
						)
18612
						.attr(pointAttr)
18613
						.add(series.markerGroup);
18614
						stateMarkerGraphic.currentSymbol = newSymbol;
18615
					}
18616
 
18617
				// Move the existing graphic
18618
				} else {
18619
					stateMarkerGraphic[move ? 'animate' : 'attr']({ // #1054
18620
						x: plotX - radius,
18621
						y: plotY - radius
18622
					});
18623
				}
18624
			}
18625
 
18626
			if (stateMarkerGraphic) {
18627
				stateMarkerGraphic[state && chart.isInsidePlot(plotX, plotY, chart.inverted) ? 'show' : 'hide'](); // #2450
18628
				stateMarkerGraphic.element.point = point; // #4310
18629
			}
18630
		}
18631
 
18632
		// Show me your halo
18633
		haloOptions = stateOptions[state] && stateOptions[state].halo;
18634
		if (haloOptions && haloOptions.size) {
18635
			if (!halo) {
18636
				series.halo = halo = chart.renderer.path()
18637
					.add(chart.seriesGroup);
18638
			}
18639
			halo.attr(extend({
18640
				fill: Color(point.color || series.color).setOpacity(haloOptions.opacity).get()
18641
			}, haloOptions.attributes))[move ? 'animate' : 'attr']({
18642
				d: point.haloPath(haloOptions.size)
18643
			});
18644
		} else if (halo) {
18645
			halo.attr({ d: [] });
18646
		}
18647
 
18648
		point.state = state;
18649
	},
18650
 
18651
	haloPath: function (size) {
18652
		var series = this.series,
18653
			chart = series.chart,
18654
			plotBox = series.getPlotBox(),
18655
			inverted = chart.inverted;
18656
 
18657
		return chart.renderer.symbols.circle(
18658
			plotBox.translateX + (inverted ? series.yAxis.len - this.plotY : this.plotX) - size,
18659
			plotBox.translateY + (inverted ? series.xAxis.len - this.plotX : this.plotY) - size,
18660
			size * 2,
18661
			size * 2
18662
		);
18663
	}
18664
});
18665
 
18666
/*
18667
 * Extend the Series object with interaction
18668
 */
18669
 
18670
extend(Series.prototype, {
18671
	/**
18672
	 * Series mouse over handler
18673
	 */
18674
	onMouseOver: function () {
18675
		var series = this,
18676
			chart = series.chart,
18677
			hoverSeries = chart.hoverSeries;
18678
 
18679
		// set normal state to previous series
18680
		if (hoverSeries && hoverSeries !== series) {
18681
			hoverSeries.onMouseOut();
18682
		}
18683
 
18684
		// trigger the event, but to save processing time,
18685
		// only if defined
18686
		if (series.options.events.mouseOver) {
18687
			fireEvent(series, 'mouseOver');
18688
		}
18689
 
18690
		// hover this
18691
		series.setState(HOVER_STATE);
18692
		chart.hoverSeries = series;
18693
	},
18694
 
18695
	/**
18696
	 * Series mouse out handler
18697
	 */
18698
	onMouseOut: function () {
18699
		// trigger the event only if listeners exist
18700
		var series = this,
18701
			options = series.options,
18702
			chart = series.chart,
18703
			tooltip = chart.tooltip,
18704
			hoverPoint = chart.hoverPoint;
18705
 
18706
		chart.hoverSeries = null; // #182, set to null before the mouseOut event fires
18707
 
18708
		// trigger mouse out on the point, which must be in this series
18709
		if (hoverPoint) {
18710
			hoverPoint.onMouseOut();
18711
		}
18712
 
18713
		// fire the mouse out event
18714
		if (series && options.events.mouseOut) {
18715
			fireEvent(series, 'mouseOut');
18716
		}
18717
 
18718
 
18719
		// hide the tooltip
18720
		if (tooltip && !options.stickyTracking && (!tooltip.shared || series.noSharedTooltip)) {
18721
			tooltip.hide();
18722
		}
18723
 
18724
		// set normal state
18725
		series.setState();
18726
	},
18727
 
18728
	/**
18729
	 * Set the state of the graph
18730
	 */
18731
	setState: function (state) {
18732
		var series = this,
18733
			options = series.options,
18734
			graph = series.graph,
18735
			stateOptions = options.states,
18736
			lineWidth = options.lineWidth,
18737
			attribs,
18738
			i = 0;
18739
 
18740
		state = state || NORMAL_STATE;
18741
 
18742
		if (series.state !== state) {
18743
			series.state = state;
18744
 
18745
			if (stateOptions[state] && stateOptions[state].enabled === false) {
18746
				return;
18747
			}
18748
 
18749
			if (state) {
18750
				lineWidth = stateOptions[state].lineWidth || lineWidth + (stateOptions[state].lineWidthPlus || 0); // #4035
18751
			}
18752
 
18753
			if (graph && !graph.dashstyle) { // hover is turned off for dashed lines in VML
18754
				attribs = {
18755
					'stroke-width': lineWidth
18756
				};
18757
				// use attr because animate will cause any other animation on the graph to stop
18758
				graph.attr(attribs);
18759
				while (series['zoneGraph' + i]) {
18760
					series['zoneGraph' + i].attr(attribs);
18761
					i = i + 1;
18762
				}
18763
			}
18764
		}
18765
	},
18766
 
18767
	/**
18768
	 * Set the visibility of the graph
18769
	 *
18770
	 * @param vis {Boolean} True to show the series, false to hide. If UNDEFINED,
18771
	 *				the visibility is toggled.
18772
	 */
18773
	setVisible: function (vis, redraw) {
18774
		var series = this,
18775
			chart = series.chart,
18776
			legendItem = series.legendItem,
18777
			showOrHide,
18778
			ignoreHiddenSeries = chart.options.chart.ignoreHiddenSeries,
18779
			oldVisibility = series.visible;
18780
 
18781
		// if called without an argument, toggle visibility
18782
		series.visible = vis = series.userOptions.visible = vis === UNDEFINED ? !oldVisibility : vis;
18783
		showOrHide = vis ? 'show' : 'hide';
18784
 
18785
		// show or hide elements
18786
		each(['group', 'dataLabelsGroup', 'markerGroup', 'tracker'], function (key) {
18787
			if (series[key]) {
18788
				series[key][showOrHide]();
18789
			}
18790
		});
18791
 
18792
 
18793
		// hide tooltip (#1361)
18794
		if (chart.hoverSeries === series || (chart.hoverPoint && chart.hoverPoint.series) === series) {
18795
			series.onMouseOut();
18796
		}
18797
 
18798
 
18799
		if (legendItem) {
18800
			chart.legend.colorizeItem(series, vis);
18801
		}
18802
 
18803
 
18804
		// rescale or adapt to resized chart
18805
		series.isDirty = true;
18806
		// in a stack, all other series are affected
18807
		if (series.options.stacking) {
18808
			each(chart.series, function (otherSeries) {
18809
				if (otherSeries.options.stacking && otherSeries.visible) {
18810
					otherSeries.isDirty = true;
18811
				}
18812
			});
18813
		}
18814
 
18815
		// show or hide linked series
18816
		each(series.linkedSeries, function (otherSeries) {
18817
			otherSeries.setVisible(vis, false);
18818
		});
18819
 
18820
		if (ignoreHiddenSeries) {
18821
			chart.isDirtyBox = true;
18822
		}
18823
		if (redraw !== false) {
18824
			chart.redraw();
18825
		}
18826
 
18827
		fireEvent(series, showOrHide);
18828
	},
18829
 
18830
	/**
18831
	 * Show the graph
18832
	 */
18833
	show: function () {
18834
		this.setVisible(true);
18835
	},
18836
 
18837
	/**
18838
	 * Hide the graph
18839
	 */
18840
	hide: function () {
18841
		this.setVisible(false);
18842
	},
18843
 
18844
 
18845
	/**
18846
	 * Set the selected state of the graph
18847
	 *
18848
	 * @param selected {Boolean} True to select the series, false to unselect. If
18849
	 *				UNDEFINED, the selection state is toggled.
18850
	 */
18851
	select: function (selected) {
18852
		var series = this;
18853
		// if called without an argument, toggle
18854
		series.selected = selected = (selected === UNDEFINED) ? !series.selected : selected;
18855
 
18856
		if (series.checkbox) {
18857
			series.checkbox.checked = selected;
18858
		}
18859
 
18860
		fireEvent(series, selected ? 'select' : 'unselect');
18861
	},
18862
 
18863
	drawTracker: TrackerMixin.drawTrackerGraph
18864
});/* ****************************************************************************
18865
 * Start ordinal axis logic                                                   *
18866
 *****************************************************************************/
18867
 
18868
 
18869
wrap(Series.prototype, 'init', function (proceed) {
18870
	var series = this,
18871
		xAxis;
18872
 
18873
	// call the original function
18874
	proceed.apply(this, Array.prototype.slice.call(arguments, 1));
18875
 
18876
	xAxis = series.xAxis;
18877
 
18878
	// Destroy the extended ordinal index on updated data
18879
	if (xAxis && xAxis.options.ordinal) {
18880
		addEvent(series, 'updatedData', function () {
18881
			delete xAxis.ordinalIndex;
18882
		});
18883
	}
18884
});
18885
 
18886
/**
18887
 * In an ordinal axis, there might be areas with dense consentrations of points, then large
18888
 * gaps between some. Creating equally distributed ticks over this entire range
18889
 * may lead to a huge number of ticks that will later be removed. So instead, break the
18890
 * positions up in segments, find the tick positions for each segment then concatenize them.
18891
 * This method is used from both data grouping logic and X axis tick position logic.
18892
 */
18893
wrap(Axis.prototype, 'getTimeTicks', function (proceed, normalizedInterval, min, max, startOfWeek, positions, closestDistance, findHigherRanks) {
18894
 
18895
	var start = 0,
18896
		end = 0,
18897
		segmentPositions,
18898
		higherRanks = {},
18899
		hasCrossedHigherRank,
18900
		info,
18901
		posLength,
18902
		outsideMax,
18903
		groupPositions = [],
18904
		lastGroupPosition = -Number.MAX_VALUE,
18905
		tickPixelIntervalOption = this.options.tickPixelInterval;
18906
 
18907
	// The positions are not always defined, for example for ordinal positions when data
18908
	// has regular interval (#1557, #2090)
18909
	if ((!this.options.ordinal && !this.options.breaks) || !positions || positions.length < 3 || min === UNDEFINED) {
18910
		return proceed.call(this, normalizedInterval, min, max, startOfWeek);
18911
	}
18912
 
18913
	// Analyze the positions array to split it into segments on gaps larger than 5 times
18914
	// the closest distance. The closest distance is already found at this point, so
18915
	// we reuse that instead of computing it again.
18916
	posLength = positions.length;
18917
 
18918
	for (; end < posLength; end++) {
18919
 
18920
		outsideMax = end && positions[end - 1] > max;
18921
 
18922
		if (positions[end] < min) { // Set the last position before min
18923
			start = end;
18924
		}
18925
 
18926
		if (end === posLength - 1 || positions[end + 1] - positions[end] > closestDistance * 5 || outsideMax) {
18927
 
18928
			// For each segment, calculate the tick positions from the getTimeTicks utility
18929
			// function. The interval will be the same regardless of how long the segment is.
18930
			if (positions[end] > lastGroupPosition) { // #1475
18931
 
18932
				segmentPositions = proceed.call(this, normalizedInterval, positions[start], positions[end], startOfWeek);
18933
 
18934
				// Prevent duplicate groups, for example for multiple segments within one larger time frame (#1475)
18935
				while (segmentPositions.length && segmentPositions[0] <= lastGroupPosition) {
18936
					segmentPositions.shift();
18937
				}
18938
				if (segmentPositions.length) {
18939
					lastGroupPosition = segmentPositions[segmentPositions.length - 1];
18940
				}
18941
 
18942
				groupPositions = groupPositions.concat(segmentPositions);
18943
			}
18944
			// Set start of next segment
18945
			start = end + 1;
18946
		}
18947
 
18948
		if (outsideMax) {
18949
			break;
18950
		}
18951
	}
18952
 
18953
	// Get the grouping info from the last of the segments. The info is the same for
18954
	// all segments.
18955
	info = segmentPositions.info;
18956
 
18957
	// Optionally identify ticks with higher rank, for example when the ticks
18958
	// have crossed midnight.
18959
	if (findHigherRanks && info.unitRange <= timeUnits.hour) {
18960
		end = groupPositions.length - 1;
18961
 
18962
		// Compare points two by two
18963
		for (start = 1; start < end; start++) {
18964
			if (dateFormat('%d', groupPositions[start]) !== dateFormat('%d', groupPositions[start - 1])) {
18965
				higherRanks[groupPositions[start]] = 'day';
18966
				hasCrossedHigherRank = true;
18967
			}
18968
		}
18969
 
18970
		// If the complete array has crossed midnight, we want to mark the first
18971
		// positions also as higher rank
18972
		if (hasCrossedHigherRank) {
18973
			higherRanks[groupPositions[0]] = 'day';
18974
		}
18975
		info.higherRanks = higherRanks;
18976
	}
18977
 
18978
	// Save the info
18979
	groupPositions.info = info;
18980
 
18981
 
18982
 
18983
	// Don't show ticks within a gap in the ordinal axis, where the space between
18984
	// two points is greater than a portion of the tick pixel interval
18985
	if (findHigherRanks && defined(tickPixelIntervalOption)) { // check for squashed ticks
18986
 
18987
		var length = groupPositions.length,
18988
			i = length,
18989
			itemToRemove,
18990
			translated,
18991
			translatedArr = [],
18992
			lastTranslated,
18993
			medianDistance,
18994
			distance,
18995
			distances = [];
18996
 
18997
		// Find median pixel distance in order to keep a reasonably even distance between
18998
		// ticks (#748)
18999
		while (i--) {
19000
			translated = this.translate(groupPositions[i]);
19001
			if (lastTranslated) {
19002
				distances[i] = lastTranslated - translated;
19003
			}
19004
			translatedArr[i] = lastTranslated = translated;
19005
		}
19006
		distances.sort();
19007
		medianDistance = distances[mathFloor(distances.length / 2)];
19008
		if (medianDistance < tickPixelIntervalOption * 0.6) {
19009
			medianDistance = null;
19010
		}
19011
 
19012
		// Now loop over again and remove ticks where needed
19013
		i = groupPositions[length - 1] > max ? length - 1 : length; // #817
19014
		lastTranslated = undefined;
19015
		while (i--) {
19016
			translated = translatedArr[i];
19017
			distance = lastTranslated - translated;
19018
 
19019
			// Remove ticks that are closer than 0.6 times the pixel interval from the one to the right,
19020
			// but not if it is close to the median distance (#748).
19021
			if (lastTranslated && distance < tickPixelIntervalOption * 0.8 &&
19022
					(medianDistance === null || distance < medianDistance * 0.8)) {
19023
 
19024
				// Is this a higher ranked position with a normal position to the right?
19025
				if (higherRanks[groupPositions[i]] && !higherRanks[groupPositions[i + 1]]) {
19026
 
19027
					// Yes: remove the lower ranked neighbour to the right
19028
					itemToRemove = i + 1;
19029
					lastTranslated = translated; // #709
19030
 
19031
				} else {
19032
 
19033
					// No: remove this one
19034
					itemToRemove = i;
19035
				}
19036
 
19037
				groupPositions.splice(itemToRemove, 1);
19038
 
19039
			} else {
19040
				lastTranslated = translated;
19041
			}
19042
		}
19043
	}
19044
	return groupPositions;
19045
});
19046
 
19047
// Extend the Axis prototype
19048
extend(Axis.prototype, {
19049
 
19050
	/**
19051
	 * Calculate the ordinal positions before tick positions are calculated.
19052
	 */
19053
	beforeSetTickPositions: function () {
19054
		var axis = this,
19055
			len,
19056
			ordinalPositions = [],
19057
			useOrdinal = false,
19058
			dist,
19059
			extremes = axis.getExtremes(),
19060
			min = extremes.min,
19061
			max = extremes.max,
19062
			minIndex,
19063
			maxIndex,
19064
			slope,
19065
			hasBreaks = axis.isXAxis && !!axis.options.breaks,
19066
			isOrdinal = axis.options.ordinal,
19067
			i;
19068
 
19069
		// apply the ordinal logic
19070
		if (isOrdinal || hasBreaks) { // #4167 YAxis is never ordinal ?
19071
 
19072
			each(axis.series, function (series, i) {
19073
 
19074
				if (series.visible !== false && (series.takeOrdinalPosition !== false || hasBreaks)) {
19075
 
19076
					// concatenate the processed X data into the existing positions, or the empty array
19077
					ordinalPositions = ordinalPositions.concat(series.processedXData);
19078
					len = ordinalPositions.length;
19079
 
19080
					// remove duplicates (#1588)
19081
					ordinalPositions.sort(function (a, b) {
19082
						return a - b; // without a custom function it is sorted as strings
19083
					});
19084
 
19085
					if (len) {
19086
						i = len - 1;
19087
						while (i--) {
19088
							if (ordinalPositions[i] === ordinalPositions[i + 1]) {
19089
								ordinalPositions.splice(i, 1);
19090
							}
19091
						}
19092
					}
19093
				}
19094
 
19095
			});
19096
 
19097
			// cache the length
19098
			len = ordinalPositions.length;
19099
 
19100
			// Check if we really need the overhead of mapping axis data against the ordinal positions.
19101
			// If the series consist of evenly spaced data any way, we don't need any ordinal logic.
19102
			if (len > 2) { // two points have equal distance by default
19103
				dist = ordinalPositions[1] - ordinalPositions[0];
19104
				i = len - 1;
19105
				while (i-- && !useOrdinal) {
19106
					if (ordinalPositions[i + 1] - ordinalPositions[i] !== dist) {
19107
						useOrdinal = true;
19108
					}
19109
				}
19110
 
19111
				// When zooming in on a week, prevent axis padding for weekends even though the data within
19112
				// the week is evenly spaced.
19113
				if (!axis.options.keepOrdinalPadding && (ordinalPositions[0] - min > dist || max - ordinalPositions[ordinalPositions.length - 1] > dist)) {
19114
					useOrdinal = true;
19115
				}
19116
			}
19117
 
19118
			// Record the slope and offset to compute the linear values from the array index.
19119
			// Since the ordinal positions may exceed the current range, get the start and
19120
			// end positions within it (#719, #665b)
19121
			if (useOrdinal) {
19122
 
19123
				// Register
19124
				axis.ordinalPositions = ordinalPositions;
19125
 
19126
				// This relies on the ordinalPositions being set. Use mathMax and mathMin to prevent
19127
				// padding on either sides of the data.
19128
				minIndex = axis.val2lin(mathMax(min, ordinalPositions[0]), true);
19129
				maxIndex = mathMax(axis.val2lin(mathMin(max, ordinalPositions[ordinalPositions.length - 1]), true), 1); // #3339
19130
 
19131
				// Set the slope and offset of the values compared to the indices in the ordinal positions
19132
				axis.ordinalSlope = slope = (max - min) / (maxIndex - minIndex);
19133
				axis.ordinalOffset = min - (minIndex * slope);
19134
 
19135
			} else {
19136
				axis.ordinalPositions = axis.ordinalSlope = axis.ordinalOffset = UNDEFINED;
19137
			}
19138
		}
19139
		axis.doPostTranslate = (isOrdinal && useOrdinal) || hasBreaks; // #3818, #4196
19140
		axis.groupIntervalFactor = null; // reset for next run
19141
	},
19142
	/**
19143
	 * Translate from a linear axis value to the corresponding ordinal axis position. If there
19144
	 * are no gaps in the ordinal axis this will be the same. The translated value is the value
19145
	 * that the point would have if the axis were linear, using the same min and max.
19146
	 *
19147
	 * @param Number val The axis value
19148
	 * @param Boolean toIndex Whether to return the index in the ordinalPositions or the new value
19149
	 */
19150
	val2lin: function (val, toIndex) {
19151
		var axis = this,
19152
			ordinalPositions = axis.ordinalPositions;
19153
 
19154
		if (!ordinalPositions) {
19155
			return val;
19156
 
19157
		} else {
19158
 
19159
			var ordinalLength = ordinalPositions.length,
19160
				i,
19161
				distance,
19162
				ordinalIndex;
19163
 
19164
			// first look for an exact match in the ordinalpositions array
19165
			i = ordinalLength;
19166
			while (i--) {
19167
				if (ordinalPositions[i] === val) {
19168
					ordinalIndex = i;
19169
					break;
19170
				}
19171
			}
19172
 
19173
			// if that failed, find the intermediate position between the two nearest values
19174
			i = ordinalLength - 1;
19175
			while (i--) {
19176
				if (val > ordinalPositions[i] || i === 0) { // interpolate
19177
					distance = (val - ordinalPositions[i]) / (ordinalPositions[i + 1] - ordinalPositions[i]); // something between 0 and 1
19178
					ordinalIndex = i + distance;
19179
					break;
19180
				}
19181
			}
19182
			return toIndex ?
19183
				ordinalIndex :
19184
				axis.ordinalSlope * (ordinalIndex || 0) + axis.ordinalOffset;
19185
		}
19186
	},
19187
	/**
19188
	 * Translate from linear (internal) to axis value
19189
	 *
19190
	 * @param Number val The linear abstracted value
19191
	 * @param Boolean fromIndex Translate from an index in the ordinal positions rather than a value
19192
	 */
19193
	lin2val: function (val, fromIndex) {
19194
		var axis = this,
19195
			ordinalPositions = axis.ordinalPositions;
19196
 
19197
		if (!ordinalPositions) { // the visible range contains only equally spaced values
19198
			return val;
19199
 
19200
		} else {
19201
 
19202
			var ordinalSlope = axis.ordinalSlope,
19203
				ordinalOffset = axis.ordinalOffset,
19204
				i = ordinalPositions.length - 1,
19205
				linearEquivalentLeft,
19206
				linearEquivalentRight,
19207
				distance;
19208
 
19209
 
19210
			// Handle the case where we translate from the index directly, used only
19211
			// when panning an ordinal axis
19212
			if (fromIndex) {
19213
 
19214
				if (val < 0) { // out of range, in effect panning to the left
19215
					val = ordinalPositions[0];
19216
				} else if (val > i) { // out of range, panning to the right
19217
					val = ordinalPositions[i];
19218
				} else { // split it up
19219
					i = mathFloor(val);
19220
					distance = val - i; // the decimal
19221
				}
19222
 
19223
			// Loop down along the ordinal positions. When the linear equivalent of i matches
19224
			// an ordinal position, interpolate between the left and right values.
19225
			} else {
19226
				while (i--) {
19227
					linearEquivalentLeft = (ordinalSlope * i) + ordinalOffset;
19228
					if (val >= linearEquivalentLeft) {
19229
						linearEquivalentRight = (ordinalSlope * (i + 1)) + ordinalOffset;
19230
						distance = (val - linearEquivalentLeft) / (linearEquivalentRight - linearEquivalentLeft); // something between 0 and 1
19231
						break;
19232
					}
19233
				}
19234
			}
19235
 
19236
			// If the index is within the range of the ordinal positions, return the associated
19237
			// or interpolated value. If not, just return the value
19238
			return distance !== UNDEFINED && ordinalPositions[i] !== UNDEFINED ?
19239
				ordinalPositions[i] + (distance ? distance * (ordinalPositions[i + 1] - ordinalPositions[i]) : 0) :
19240
				val;
19241
		}
19242
	},
19243
	/**
19244
	 * Get the ordinal positions for the entire data set. This is necessary in chart panning
19245
	 * because we need to find out what points or data groups are available outside the
19246
	 * visible range. When a panning operation starts, if an index for the given grouping
19247
	 * does not exists, it is created and cached. This index is deleted on updated data, so
19248
	 * it will be regenerated the next time a panning operation starts.
19249
	 */
19250
	getExtendedPositions: function () {
19251
		var axis = this,
19252
			chart = axis.chart,
19253
			grouping = axis.series[0].currentDataGrouping,
19254
			ordinalIndex = axis.ordinalIndex,
19255
			key = grouping ? grouping.count + grouping.unitName : 'raw',
19256
			extremes = axis.getExtremes(),
19257
			fakeAxis,
19258
			fakeSeries;
19259
 
19260
		// If this is the first time, or the ordinal index is deleted by updatedData,
19261
		// create it.
19262
		if (!ordinalIndex) {
19263
			ordinalIndex = axis.ordinalIndex = {};
19264
		}
19265
 
19266
 
19267
		if (!ordinalIndex[key]) {
19268
 
19269
			// Create a fake axis object where the extended ordinal positions are emulated
19270
			fakeAxis = {
19271
				series: [],
19272
				getExtremes: function () {
19273
					return {
19274
						min: extremes.dataMin,
19275
						max: extremes.dataMax
19276
					};
19277
				},
19278
				options: {
19279
					ordinal: true
19280
				},
19281
				val2lin: Axis.prototype.val2lin // #2590
19282
			};
19283
 
19284
			// Add the fake series to hold the full data, then apply processData to it
19285
			each(axis.series, function (series) {
19286
				fakeSeries = {
19287
					xAxis: fakeAxis,
19288
					xData: series.xData,
19289
					chart: chart,
19290
					destroyGroupedData: noop
19291
				};
19292
				fakeSeries.options = {
19293
					dataGrouping : grouping ? {
19294
						enabled: true,
19295
						forced: true,
19296
						approximation: 'open', // doesn't matter which, use the fastest
19297
						units: [[grouping.unitName, [grouping.count]]]
19298
					} : {
19299
						enabled: false
19300
					}
19301
				};
19302
				series.processData.apply(fakeSeries);
19303
 
19304
				fakeAxis.series.push(fakeSeries);
19305
			});
19306
 
19307
			// Run beforeSetTickPositions to compute the ordinalPositions
19308
			axis.beforeSetTickPositions.apply(fakeAxis);
19309
 
19310
			// Cache it
19311
			ordinalIndex[key] = fakeAxis.ordinalPositions;
19312
		}
19313
		return ordinalIndex[key];
19314
	},
19315
 
19316
	/**
19317
	 * Find the factor to estimate how wide the plot area would have been if ordinal
19318
	 * gaps were included. This value is used to compute an imagined plot width in order
19319
	 * to establish the data grouping interval.
19320
	 *
19321
	 * A real world case is the intraday-candlestick
19322
	 * example. Without this logic, it would show the correct data grouping when viewing
19323
	 * a range within each day, but once moving the range to include the gap between two
19324
	 * days, the interval would include the cut-away night hours and the data grouping
19325
	 * would be wrong. So the below method tries to compensate by identifying the most
19326
	 * common point interval, in this case days.
19327
	 *
19328
	 * An opposite case is presented in issue #718. We have a long array of daily data,
19329
	 * then one point is appended one hour after the last point. We expect the data grouping
19330
	 * not to change.
19331
	 *
19332
	 * In the future, if we find cases where this estimation doesn't work optimally, we
19333
	 * might need to add a second pass to the data grouping logic, where we do another run
19334
	 * with a greater interval if the number of data groups is more than a certain fraction
19335
	 * of the desired group count.
19336
	 */
19337
	getGroupIntervalFactor: function (xMin, xMax, series) {
19338
		var i = 0,
19339
			processedXData = series.processedXData,
19340
			len = processedXData.length,
19341
			distances = [],
19342
			median,
19343
			groupIntervalFactor = this.groupIntervalFactor;
19344
 
19345
		// Only do this computation for the first series, let the other inherit it (#2416)
19346
		if (!groupIntervalFactor) {
19347
 
19348
			// Register all the distances in an array
19349
			for (; i < len - 1; i++) {
19350
				distances[i] = processedXData[i + 1] - processedXData[i];
19351
			}
19352
 
19353
			// Sort them and find the median
19354
			distances.sort(function (a, b) {
19355
					return a - b;
19356
			});
19357
			median = distances[mathFloor(len / 2)];
19358
 
19359
			// Compensate for series that don't extend through the entire axis extent. #1675.
19360
			xMin = mathMax(xMin, processedXData[0]);
19361
			xMax = mathMin(xMax, processedXData[len - 1]);
19362
 
19363
			this.groupIntervalFactor = groupIntervalFactor = (len * median) / (xMax - xMin);
19364
		}
19365
 
19366
		// Return the factor needed for data grouping
19367
		return groupIntervalFactor;
19368
	},
19369
 
19370
	/**
19371
	 * Make the tick intervals closer because the ordinal gaps make the ticks spread out or cluster
19372
	 */
19373
	postProcessTickInterval: function (tickInterval) {
19374
		// TODO: http://jsfiddle.net/highcharts/FQm4E/1/
19375
		// This is a case where this algorithm doesn't work optimally. In this case, the
19376
		// tick labels are spread out per week, but all the gaps reside within weeks. So
19377
		// we have a situation where the labels are courser than the ordinal gaps, and
19378
		// thus the tick interval should not be altered
19379
		var ordinalSlope = this.ordinalSlope;
19380
 
19381
 
19382
		if (ordinalSlope) {
19383
			if (!this.options.breaks) {
19384
				return tickInterval / (ordinalSlope / this.closestPointRange);
19385
			} else {
19386
				return this.closestPointRange;
19387
			}
19388
		} else {
19389
			return tickInterval;
19390
		}
19391
	}
19392
});
19393
 
19394
// Extending the Chart.pan method for ordinal axes
19395
wrap(Chart.prototype, 'pan', function (proceed, e) {
19396
	var chart = this,
19397
		xAxis = chart.xAxis[0],
19398
		chartX = e.chartX,
19399
		runBase = false;
19400
 
19401
	if (xAxis.options.ordinal && xAxis.series.length) {
19402
 
19403
		var mouseDownX = chart.mouseDownX,
19404
			extremes = xAxis.getExtremes(),
19405
			dataMax = extremes.dataMax,
19406
			min = extremes.min,
19407
			max = extremes.max,
19408
			trimmedRange,
19409
			hoverPoints = chart.hoverPoints,
19410
			closestPointRange = xAxis.closestPointRange,
19411
			pointPixelWidth = xAxis.translationSlope * (xAxis.ordinalSlope || closestPointRange),
19412
			movedUnits = (mouseDownX - chartX) / pointPixelWidth, // how many ordinal units did we move?
19413
			extendedAxis = { ordinalPositions: xAxis.getExtendedPositions() }, // get index of all the chart's points
19414
			ordinalPositions,
19415
			searchAxisLeft,
19416
			lin2val = xAxis.lin2val,
19417
			val2lin = xAxis.val2lin,
19418
			searchAxisRight;
19419
 
19420
		if (!extendedAxis.ordinalPositions) { // we have an ordinal axis, but the data is equally spaced
19421
			runBase = true;
19422
 
19423
		} else if (mathAbs(movedUnits) > 1) {
19424
 
19425
			// Remove active points for shared tooltip
19426
			if (hoverPoints) {
19427
				each(hoverPoints, function (point) {
19428
					point.setState();
19429
				});
19430
			}
19431
 
19432
			if (movedUnits < 0) {
19433
				searchAxisLeft = extendedAxis;
19434
				searchAxisRight = xAxis.ordinalPositions ? xAxis : extendedAxis;
19435
			} else {
19436
				searchAxisLeft = xAxis.ordinalPositions ? xAxis : extendedAxis;
19437
				searchAxisRight = extendedAxis;
19438
			}
19439
 
19440
			// In grouped data series, the last ordinal position represents the grouped data, which is
19441
			// to the left of the real data max. If we don't compensate for this, we will be allowed
19442
			// to pan grouped data series passed the right of the plot area.
19443
			ordinalPositions = searchAxisRight.ordinalPositions;
19444
			if (dataMax > ordinalPositions[ordinalPositions.length - 1]) {
19445
				ordinalPositions.push(dataMax);
19446
			}
19447
 
19448
			// Get the new min and max values by getting the ordinal index for the current extreme,
19449
			// then add the moved units and translate back to values. This happens on the
19450
			// extended ordinal positions if the new position is out of range, else it happens
19451
			// on the current x axis which is smaller and faster.
19452
			chart.fixedRange = max - min;
19453
			trimmedRange = xAxis.toFixedRange(null, null,
19454
				lin2val.apply(searchAxisLeft, [
19455
					val2lin.apply(searchAxisLeft, [min, true]) + movedUnits, // the new index
19456
					true // translate from index
19457
				]),
19458
				lin2val.apply(searchAxisRight, [
19459
					val2lin.apply(searchAxisRight, [max, true]) + movedUnits, // the new index
19460
					true // translate from index
19461
				])
19462
			);
19463
 
19464
			// Apply it if it is within the available data range
19465
			if (trimmedRange.min >= mathMin(extremes.dataMin, min) && trimmedRange.max <= mathMax(dataMax, max)) {
19466
				xAxis.setExtremes(trimmedRange.min, trimmedRange.max, true, false, { trigger: 'pan' });
19467
			}
19468
 
19469
			chart.mouseDownX = chartX; // set new reference for next run
19470
			css(chart.container, { cursor: 'move' });
19471
		}
19472
 
19473
	} else {
19474
		runBase = true;
19475
	}
19476
 
19477
	// revert to the linear chart.pan version
19478
	if (runBase) {
19479
		// call the original function
19480
		proceed.apply(this, Array.prototype.slice.call(arguments, 1));
19481
	}
19482
});
19483
 
19484
 
19485
 
19486
/**
19487
 * Extend getSegments by identifying gaps in the ordinal data so that we can draw a gap in the
19488
 * line or area
19489
 */
19490
wrap(Series.prototype, 'getSegments', function (proceed) {
19491
 
19492
	var series = this,
19493
		segments,
19494
		gapSize = series.options.gapSize,
19495
		xAxis = series.xAxis;
19496
 
19497
	// call base method
19498
	proceed.apply(this, Array.prototype.slice.call(arguments, 1));
19499
 
19500
	if (gapSize) {
19501
 
19502
		// properties
19503
		segments = series.segments;
19504
 
19505
		// extension for ordinal breaks
19506
		each(segments, function (segment, no) {
19507
			var i = segment.length - 1;
19508
			while (i--) {
19509
				if (segment[i].x < xAxis.min && segment[i + 1].x > xAxis.max) {
19510
					segments.length = 0;
19511
					break;
19512
				} else if (segment[i + 1].x - segment[i].x > xAxis.closestPointRange * gapSize) {
19513
					segments.splice( // insert after this one
19514
						no + 1,
19515
						0,
19516
						segment.splice(i + 1, segment.length - i)
19517
					);
19518
				}
19519
			}
19520
		});
19521
	}
19522
});
19523
 
19524
/* ****************************************************************************
19525
 * End ordinal axis logic                                                   *
19526
 *****************************************************************************/
19527
/**
19528
 * Highstock JS v2.1.9 (2015-10-07)
19529
 * Highcharts Broken Axis module
19530
 *
19531
 * Author: Stephane Vanraes, Torstein Honsi
19532
 * License: www.highcharts.com/license
19533
 */
19534
 
19535
/*global HighchartsAdapter*/
19536
(function (H) {
19537
 
19538
	"use strict";
19539
 
19540
	var pick = H.pick,
19541
		wrap = H.wrap,
19542
		extend = H.extend,
19543
		fireEvent = HighchartsAdapter.fireEvent,
19544
		Axis = H.Axis,
19545
		Series = H.Series;
19546
 
19547
	function stripArguments() {
19548
		return Array.prototype.slice.call(arguments, 1);
19549
	}
19550
 
19551
	extend(Axis.prototype, {
19552
		isInBreak: function (brk, val) {
19553
			var ret,
19554
				repeat = brk.repeat || Infinity,
19555
				from = brk.from,
19556
				length = brk.to - brk.from,
19557
				test = (val >= from ? (val - from) % repeat :  repeat - ((from - val) % repeat));
19558
 
19559
			if (!brk.inclusive) {
19560
				ret = test < length && test !== 0;
19561
			} else {
19562
				ret = test <= length;
19563
			}
19564
			return ret;
19565
		},
19566
 
19567
		isInAnyBreak: function (val, testKeep) {
19568
 
19569
			var breaks = this.options.breaks,
19570
				i = breaks && breaks.length,
19571
				inbrk,
19572
				keep,
19573
				ret;
19574
 
19575
 
19576
			if (i) {
19577
 
19578
				while (i--) {
19579
					if (this.isInBreak(breaks[i], val)) {
19580
						inbrk = true;
19581
						if (!keep) {
19582
							keep = pick(breaks[i].showPoints, this.isXAxis ? false : true);
19583
						}
19584
					}
19585
				}
19586
 
19587
				if (inbrk && testKeep) {
19588
					ret = inbrk && !keep;
19589
				} else {
19590
					ret = inbrk;
19591
				}
19592
			}
19593
			return ret;
19594
		}
19595
	});
19596
 
19597
	wrap(Axis.prototype, 'setTickPositions', function (proceed) {
19598
		proceed.apply(this, Array.prototype.slice.call(arguments, 1));
19599
 
19600
		if (this.options.breaks) {
19601
			var axis = this,
19602
				tickPositions = this.tickPositions,
19603
				info = this.tickPositions.info,
19604
				newPositions = [],
19605
				i;
19606
 
19607
			for (i = 0; i < tickPositions.length; i++) {
19608
				if (!axis.isInAnyBreak(tickPositions[i])) {
19609
					newPositions.push(tickPositions[i]);
19610
				}
19611
			}
19612
 
19613
			this.tickPositions = newPositions;
19614
			this.tickPositions.info = info;
19615
		}
19616
	});
19617
 
19618
	wrap(Axis.prototype, 'init', function (proceed, chart, userOptions) {
19619
		// Force Axis to be not-ordinal when breaks are defined
19620
		if (userOptions.breaks && userOptions.breaks.length) {
19621
			userOptions.ordinal = false;
19622
		}
19623
 
19624
		proceed.call(this, chart, userOptions);
19625
 
19626
		if (this.options.breaks) {
19627
 
19628
			var axis = this;
19629
 
19630
			axis.doPostTranslate = true;
19631
 
19632
			this.val2lin = function (val) {
19633
				var nval = val,
19634
					brk,
19635
					i;
19636
 
19637
				for (i = 0; i < axis.breakArray.length; i++) {
19638
					brk = axis.breakArray[i];
19639
					if (brk.to <= val) {
19640
						nval -= brk.len;
19641
					} else if (brk.from >= val) {
19642
						break;
19643
					} else if (axis.isInBreak(brk, val)) {
19644
						nval -= (val - brk.from);
19645
						break;
19646
					}
19647
				}
19648
 
19649
				return nval;
19650
			};
19651
 
19652
			this.lin2val = function (val) {
19653
				var nval = val,
19654
					brk,
19655
					i;
19656
 
19657
				for (i = 0; i < axis.breakArray.length; i++) {
19658
					brk = axis.breakArray[i];
19659
					if (brk.from >= nval) {
19660
						break;
19661
					} else if (brk.to < nval) {
19662
						nval += brk.len;
19663
					} else if (axis.isInBreak(brk, nval)) {
19664
						nval += brk.len;
19665
					}
19666
				}
19667
				return nval;
19668
			};
19669
 
19670
			this.setExtremes = function (newMin, newMax, redraw, animation, eventArguments) {
19671
				// If trying to set extremes inside a break, extend it to before and after the break ( #3857 )
19672
				while (this.isInAnyBreak(newMin)) {
19673
					newMin -= this.closestPointRange;
19674
				}
19675
				while (this.isInAnyBreak(newMax)) {
19676
					newMax -= this.closestPointRange;
19677
				}
19678
				Axis.prototype.setExtremes.call(this, newMin, newMax, redraw, animation, eventArguments);
19679
			};
19680
 
19681
			this.setAxisTranslation = function (saveOld) {
19682
				Axis.prototype.setAxisTranslation.call(this, saveOld);
19683
 
19684
				var breaks = axis.options.breaks,
19685
					breakArrayT = [],	// Temporary one
19686
					breakArray = [],
19687
					length = 0,
19688
					inBrk,
19689
					repeat,
19690
					brk,
19691
					min = axis.userMin || axis.min,
19692
					max = axis.userMax || axis.max,
19693
					start,
19694
					i,
19695
					j;
19696
 
19697
				// Min & max check (#4247)
19698
				for (i in breaks) {
19699
					brk = breaks[i];
19700
					repeat = brk.repeat || Infinity;
19701
					if (axis.isInBreak(brk, min)) {
19702
						min += (brk.to % repeat) - (min % repeat);
19703
					}
19704
					if (axis.isInBreak(brk, max)) {
19705
						max -= (max % repeat) - (brk.from % repeat);
19706
					}
19707
				}
19708
 
19709
				// Construct an array holding all breaks in the axis
19710
				for (i in breaks) {
19711
					brk = breaks[i];
19712
					start = brk.from;
19713
					repeat = brk.repeat || Infinity;
19714
 
19715
					while (start - repeat > min) {
19716
						start -= repeat;
19717
					}
19718
					while (start < min) {
19719
						start += repeat;
19720
					}
19721
 
19722
					for (j = start; j < max; j += repeat) {
19723
						breakArrayT.push({
19724
							value: j,
19725
							move: 'in'
19726
						});
19727
						breakArrayT.push({
19728
							value: j + (brk.to - brk.from),
19729
							move: 'out',
19730
							size: brk.breakSize
19731
						});
19732
					}
19733
				}
19734
 
19735
				breakArrayT.sort(function (a, b) {
19736
					if (a.value === b.value) {
19737
						return (a.move === 'in' ? 0 : 1) - (b.move === 'in' ? 0 : 1);
19738
					} else {
19739
						return a.value - b.value;
19740
					}
19741
				});
19742
 
19743
				// Simplify the breaks
19744
				inBrk = 0;
19745
				start = min;
19746
 
19747
				for (i in breakArrayT) {
19748
					brk = breakArrayT[i];
19749
					inBrk += (brk.move === 'in' ? 1 : -1);
19750
 
19751
					if (inBrk === 1 && brk.move === 'in') {
19752
						start = brk.value;
19753
					}
19754
					if (inBrk === 0) {
19755
						breakArray.push({
19756
							from: start,
19757
							to: brk.value,
19758
							len: brk.value - start - (brk.size || 0)
19759
						});
19760
						length += brk.value - start - (brk.size || 0);
19761
					}
19762
				}
19763
 
19764
				axis.breakArray = breakArray;
19765
 
19766
				fireEvent(axis, 'afterBreaks');
19767
 
19768
				axis.transA *= ((max - axis.min) / (max - min - length));
19769
 
19770
				axis.min = min;
19771
				axis.max = max;
19772
			};
19773
		}
19774
	});
19775
 
19776
	wrap(Series.prototype, 'generatePoints', function (proceed) {
19777
 
19778
		proceed.apply(this, stripArguments(arguments));
19779
 
19780
		var series = this,
19781
			xAxis = series.xAxis,
19782
			yAxis = series.yAxis,
19783
			points = series.points,
19784
			point,
19785
			i = points.length,
19786
			connectNulls = series.options.connectNulls,
19787
			nullGap;
19788
 
19789
 
19790
		if (xAxis && yAxis && (xAxis.options.breaks || yAxis.options.breaks)) {
19791
			while (i--) {
19792
				point = points[i];
19793
 
19794
				nullGap = point.y === null && connectNulls === false; // respect nulls inside the break (#4275)
19795
				if (!nullGap && (xAxis.isInAnyBreak(point.x, true) || yAxis.isInAnyBreak(point.y, true))) {
19796
					points.splice(i, 1);
19797
					if (this.data[i]) {
19798
						this.data[i].destroyElements(); // removes the graphics for this point if they exist
19799
					}
19800
				}
19801
			}
19802
		}
19803
 
19804
	});
19805
 
19806
	wrap(H.seriesTypes.column.prototype, 'drawPoints', function (proceed) {
19807
		proceed.apply(this);
19808
 
19809
		var series = this,
19810
			points = series.points,
19811
			yAxis = series.yAxis,
19812
			breaks = yAxis.breakArray || [],
19813
			threshold = pick(this.options.threshold, yAxis.min),
19814
			eventName,
19815
			point,
19816
			brk,
19817
			i,
19818
			j,
19819
			y;
19820
 
19821
		for (i = 0; i < points.length; i++) {
19822
			point = points[i];
19823
			y = point.stackY || point.y;
19824
			for (j = 0; j < breaks.length; j++) {
19825
				brk = breaks[j];
19826
				eventName = false;
19827
 
19828
				if ((threshold < brk.from && y > brk.to) || (threshold > brk.from && y < brk.from)) {
19829
						eventName = 'pointBreak';
19830
				} 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
19831
						eventName = 'pointInBreak'; // docs
19832
				}
19833
				if (eventName) {
19834
					fireEvent(yAxis, eventName, {point: point, brk: brk});
19835
				}
19836
			}
19837
		}
19838
 
19839
	});
19840
}(Highcharts));
19841
/* ****************************************************************************
19842
 * Start data grouping module												 *
19843
 ******************************************************************************/
19844
/*jslint white:true */
19845
var DATA_GROUPING = 'dataGrouping',
19846
	seriesProto = Series.prototype,
19847
	tooltipProto = Tooltip.prototype,
19848
	baseProcessData = seriesProto.processData,
19849
	baseGeneratePoints = seriesProto.generatePoints,
19850
	baseDestroy = seriesProto.destroy,
19851
	baseTooltipFooterHeaderFormatter = tooltipProto.tooltipFooterHeaderFormatter,
19852
	NUMBER = 'number',
19853
 
19854
	commonOptions = {
19855
		approximation: 'average', // average, open, high, low, close, sum
19856
		//enabled: null, // (true for stock charts, false for basic),
19857
		//forced: undefined,
19858
		groupPixelWidth: 2,
19859
		// the first one is the point or start value, the second is the start value if we're dealing with range,
19860
		// the third one is the end value if dealing with a range
19861
		dateTimeLabelFormats: {
19862
			millisecond: ['%A, %b %e, %H:%M:%S.%L', '%A, %b %e, %H:%M:%S.%L', '-%H:%M:%S.%L'],
19863
			second: ['%A, %b %e, %H:%M:%S', '%A, %b %e, %H:%M:%S', '-%H:%M:%S'],
19864
			minute: ['%A, %b %e, %H:%M', '%A, %b %e, %H:%M', '-%H:%M'],
19865
			hour: ['%A, %b %e, %H:%M', '%A, %b %e, %H:%M', '-%H:%M'],
19866
			day: ['%A, %b %e, %Y', '%A, %b %e', '-%A, %b %e, %Y'],
19867
			week: ['Week from %A, %b %e, %Y', '%A, %b %e', '-%A, %b %e, %Y'],
19868
			month: ['%B %Y', '%B', '-%B %Y'],
19869
			year: ['%Y', '%Y', '-%Y']
19870
		}
19871
		// smoothed = false, // enable this for navigator series only
19872
	},
19873
 
19874
	specificOptions = { // extends common options
19875
		line: {},
19876
		spline: {},
19877
		area: {},
19878
		areaspline: {},
19879
		column: {
19880
			approximation: 'sum',
19881
			groupPixelWidth: 10
19882
		},
19883
		arearange: {
19884
			approximation: 'range'
19885
		},
19886
		areasplinerange: {
19887
			approximation: 'range'
19888
		},
19889
		columnrange: {
19890
			approximation: 'range',
19891
			groupPixelWidth: 10
19892
		},
19893
		candlestick: {
19894
			approximation: 'ohlc',
19895
			groupPixelWidth: 10
19896
		},
19897
		ohlc: {
19898
			approximation: 'ohlc',
19899
			groupPixelWidth: 5
19900
		}
19901
	},
19902
 
19903
	// units are defined in a separate array to allow complete overriding in case of a user option
19904
	defaultDataGroupingUnits = [[
19905
			'millisecond', // unit name
19906
			[1, 2, 5, 10, 20, 25, 50, 100, 200, 500] // allowed multiples
19907
		], [
19908
			'second',
19909
			[1, 2, 5, 10, 15, 30]
19910
		], [
19911
			'minute',
19912
			[1, 2, 5, 10, 15, 30]
19913
		], [
19914
			'hour',
19915
			[1, 2, 3, 4, 6, 8, 12]
19916
		], [
19917
			'day',
19918
			[1]
19919
		], [
19920
			'week',
19921
			[1]
19922
		], [
19923
			'month',
19924
			[1, 3, 6]
19925
		], [
19926
			'year',
19927
			null
19928
		]
19929
	],
19930
 
19931
 
19932
	/**
19933
	 * Define the available approximation types. The data grouping approximations takes an array
19934
	 * or numbers as the first parameter. In case of ohlc, four arrays are sent in as four parameters.
19935
	 * Each array consists only of numbers. In case null values belong to the group, the property
19936
	 * .hasNulls will be set to true on the array.
19937
	 */
19938
	approximations = {
19939
		sum: function (arr) {
19940
			var len = arr.length,
19941
				ret;
19942
 
19943
			// 1. it consists of nulls exclusively
19944
			if (!len && arr.hasNulls) {
19945
				ret = null;
19946
			// 2. it has a length and real values
19947
			} else if (len) {
19948
				ret = 0;
19949
				while (len--) {
19950
					ret += arr[len];
19951
				}
19952
			}
19953
			// 3. it has zero length, so just return undefined
19954
			// => doNothing()
19955
 
19956
			return ret;
19957
		},
19958
		average: function (arr) {
19959
			var len = arr.length,
19960
				ret = approximations.sum(arr);
19961
 
19962
			// If we have a number, return it divided by the length. If not, return
19963
			// null or undefined based on what the sum method finds.
19964
			if (typeof ret === NUMBER && len) {
19965
				ret = ret / len;
19966
			}
19967
 
19968
			return ret;
19969
		},
19970
		open: function (arr) {
19971
			return arr.length ? arr[0] : (arr.hasNulls ? null : UNDEFINED);
19972
		},
19973
		high: function (arr) {
19974
			return arr.length ? arrayMax(arr) : (arr.hasNulls ? null : UNDEFINED);
19975
		},
19976
		low: function (arr) {
19977
			return arr.length ? arrayMin(arr) : (arr.hasNulls ? null : UNDEFINED);
19978
		},
19979
		close: function (arr) {
19980
			return arr.length ? arr[arr.length - 1] : (arr.hasNulls ? null : UNDEFINED);
19981
		},
19982
		// ohlc and range are special cases where a multidimensional array is input and an array is output
19983
		ohlc: function (open, high, low, close) {
19984
			open = approximations.open(open);
19985
			high = approximations.high(high);
19986
			low = approximations.low(low);
19987
			close = approximations.close(close);
19988
 
19989
			if (typeof open === NUMBER || typeof high === NUMBER || typeof low === NUMBER || typeof close === NUMBER) {
19990
				return [open, high, low, close];
19991
			}
19992
			// else, return is undefined
19993
		},
19994
		range: function (low, high) {
19995
			low = approximations.low(low);
19996
			high = approximations.high(high);
19997
 
19998
			if (typeof low === NUMBER || typeof high === NUMBER) {
19999
				return [low, high];
20000
			}
20001
			// else, return is undefined
20002
		}
20003
	};
20004
 
20005
/*jslint white:false */
20006
 
20007
/**
20008
 * Takes parallel arrays of x and y data and groups the data into intervals defined by groupPositions, a collection
20009
 * of starting x values for each group.
20010
 */
20011
seriesProto.groupData = function (xData, yData, groupPositions, approximation) {
20012
	var series = this,
20013
		data = series.data,
20014
		dataOptions = series.options.data,
20015
		groupedXData = [],
20016
		groupedYData = [],
20017
		dataLength = xData.length,
20018
		pointX,
20019
		pointY,
20020
		groupedY,
20021
		handleYData = !!yData, // when grouping the fake extended axis for panning, we don't need to consider y
20022
		values = [[], [], [], []],
20023
		approximationFn = typeof approximation === 'function' ? approximation : approximations[approximation],
20024
		pointArrayMap = series.pointArrayMap,
20025
		pointArrayMapLength = pointArrayMap && pointArrayMap.length,
20026
		i;
20027
 
20028
	// Start with the first point within the X axis range (#2696)
20029
	for (i = 0; i <= dataLength; i++) {
20030
		if (xData[i] >= groupPositions[0]) {
20031
			break;
20032
		}
20033
	}
20034
 
20035
	for (; i <= dataLength; i++) {
20036
 
20037
		// when a new group is entered, summarize and initiate the previous group
20038
		while ((groupPositions[1] !== UNDEFINED && xData[i] >= groupPositions[1]) ||
20039
				i === dataLength) { // get the last group
20040
 
20041
			// get group x and y
20042
			pointX = groupPositions.shift();
20043
			groupedY = approximationFn.apply(0, values);
20044
 
20045
			// push the grouped data
20046
			if (groupedY !== UNDEFINED) {
20047
				groupedXData.push(pointX);
20048
				groupedYData.push(groupedY);
20049
			}
20050
 
20051
			// reset the aggregate arrays
20052
			values[0] = [];
20053
			values[1] = [];
20054
			values[2] = [];
20055
			values[3] = [];
20056
 
20057
			// don't loop beyond the last group
20058
			if (i === dataLength) {
20059
				break;
20060
			}
20061
		}
20062
 
20063
		// break out
20064
		if (i === dataLength) {
20065
			break;
20066
		}
20067
 
20068
		// for each raw data point, push it to an array that contains all values for this specific group
20069
		if (pointArrayMap) {
20070
 
20071
			var index = series.cropStart + i,
20072
				point = (data && data[index]) || series.pointClass.prototype.applyOptions.apply({ series: series }, [dataOptions[index]]),
20073
				j,
20074
				val;
20075
 
20076
			for (j = 0; j < pointArrayMapLength; j++) {
20077
				val = point[pointArrayMap[j]];
20078
				if (typeof val === NUMBER) {
20079
					values[j].push(val);
20080
				} else if (val === null) {
20081
					values[j].hasNulls = true;
20082
				}
20083
			}
20084
 
20085
		} else {
20086
			pointY = handleYData ? yData[i] : null;
20087
 
20088
			if (typeof pointY === NUMBER) {
20089
				values[0].push(pointY);
20090
			} else if (pointY === null) {
20091
				values[0].hasNulls = true;
20092
			}
20093
		}
20094
	}
20095
 
20096
	return [groupedXData, groupedYData];
20097
};
20098
 
20099
/**
20100
 * Extend the basic processData method, that crops the data to the current zoom
20101
 * range, with data grouping logic.
20102
 */
20103
seriesProto.processData = function () {
20104
	var series = this,
20105
		chart = series.chart,
20106
		options = series.options,
20107
		dataGroupingOptions = options[DATA_GROUPING],
20108
		groupingEnabled = series.allowDG !== false && dataGroupingOptions && pick(dataGroupingOptions.enabled, chart.options._stock),
20109
		hasGroupedData;
20110
 
20111
	// run base method
20112
	series.forceCrop = groupingEnabled; // #334
20113
	series.groupPixelWidth = null; // #2110
20114
	series.hasProcessed = true; // #2692
20115
 
20116
	// skip if processData returns false or if grouping is disabled (in that order)
20117
	if (baseProcessData.apply(series, arguments) === false || !groupingEnabled) {
20118
		return;
20119
 
20120
	} else {
20121
		series.destroyGroupedData();
20122
 
20123
	}
20124
	var i,
20125
		processedXData = series.processedXData,
20126
		processedYData = series.processedYData,
20127
		plotSizeX = chart.plotSizeX,
20128
		xAxis = series.xAxis,
20129
		ordinal = xAxis.options.ordinal,
20130
		groupPixelWidth = series.groupPixelWidth = xAxis.getGroupPixelWidth && xAxis.getGroupPixelWidth(),
20131
		nonGroupedPointRange = series.pointRange;
20132
 
20133
	// Execute grouping if the amount of points is greater than the limit defined in groupPixelWidth
20134
	if (groupPixelWidth) {
20135
		hasGroupedData = true;
20136
 
20137
		series.points = null; // force recreation of point instances in series.translate
20138
 
20139
		var extremes = xAxis.getExtremes(),
20140
			xMin = extremes.min,
20141
			xMax = extremes.max,
20142
			groupIntervalFactor = (ordinal && xAxis.getGroupIntervalFactor(xMin, xMax, series)) || 1,
20143
			interval = (groupPixelWidth * (xMax - xMin) / plotSizeX) * groupIntervalFactor,
20144
			groupPositions = xAxis.getTimeTicks(
20145
				xAxis.normalizeTimeTickInterval(interval, dataGroupingOptions.units || defaultDataGroupingUnits),
20146
				xMin,
20147
				xMax,
20148
				xAxis.options.startOfWeek,
20149
				processedXData,
20150
				series.closestPointRange
20151
			),
20152
			groupedXandY = seriesProto.groupData.apply(series, [processedXData, processedYData, groupPositions, dataGroupingOptions.approximation]),
20153
			groupedXData = groupedXandY[0],
20154
			groupedYData = groupedXandY[1];
20155
 
20156
		// prevent the smoothed data to spill out left and right, and make
20157
		// sure data is not shifted to the left
20158
		if (dataGroupingOptions.smoothed) {
20159
			i = groupedXData.length - 1;
20160
			groupedXData[i] = Math.min(groupedXData[i], xMax);
20161
			while (i-- && i > 0) {
20162
				groupedXData[i] += interval / 2;
20163
			}
20164
			groupedXData[0] = Math.max(groupedXData[0], xMin);
20165
		}
20166
 
20167
		// record what data grouping values were used
20168
		series.currentDataGrouping = groupPositions.info;
20169
		if (options.pointRange === null) { // null means auto, as for columns, candlesticks and OHLC
20170
			series.pointRange = groupPositions.info.totalRange;
20171
		}
20172
		series.closestPointRange = groupPositions.info.totalRange;
20173
 
20174
		// Make sure the X axis extends to show the first group (#2533)
20175
		if (defined(groupedXData[0]) && groupedXData[0] < xAxis.dataMin) {
20176
			if (xAxis.min === xAxis.dataMin) {
20177
				xAxis.min = groupedXData[0];
20178
			}
20179
			xAxis.dataMin = groupedXData[0];
20180
		}
20181
 
20182
		// set series props
20183
		series.processedXData = groupedXData;
20184
		series.processedYData = groupedYData;
20185
	} else {
20186
		series.currentDataGrouping = null;
20187
		series.pointRange = nonGroupedPointRange;
20188
	}
20189
	series.hasGroupedData = hasGroupedData;
20190
};
20191
 
20192
/**
20193
 * Destroy the grouped data points. #622, #740
20194
 */
20195
seriesProto.destroyGroupedData = function () {
20196
 
20197
	var groupedData = this.groupedData;
20198
 
20199
	// clear previous groups
20200
	each(groupedData || [], function (point, i) {
20201
		if (point) {
20202
			groupedData[i] = point.destroy ? point.destroy() : null;
20203
		}
20204
	});
20205
	this.groupedData = null;
20206
};
20207
 
20208
/**
20209
 * Override the generatePoints method by adding a reference to grouped data
20210
 */
20211
seriesProto.generatePoints = function () {
20212
 
20213
	baseGeneratePoints.apply(this);
20214
 
20215
	// record grouped data in order to let it be destroyed the next time processData runs
20216
	this.destroyGroupedData(); // #622
20217
	this.groupedData = this.hasGroupedData ? this.points : null;
20218
};
20219
 
20220
/**
20221
 * Extend the original method, make the tooltip's header reflect the grouped range
20222
 */
20223
tooltipProto.tooltipFooterHeaderFormatter = function (point, isFooter) {
20224
	var tooltip = this,
20225
		series = point.series,
20226
		options = series.options,
20227
		tooltipOptions = series.tooltipOptions,
20228
		dataGroupingOptions = options.dataGrouping,
20229
		xDateFormat = tooltipOptions.xDateFormat,
20230
		xDateFormatEnd,
20231
		xAxis = series.xAxis,
20232
		currentDataGrouping,
20233
		dateTimeLabelFormats,
20234
		labelFormats,
20235
		formattedKey,
20236
		ret;
20237
 
20238
	// apply only to grouped series
20239
	if (xAxis && xAxis.options.type === 'datetime' && dataGroupingOptions && isNumber(point.key)) {
20240
 
20241
		// set variables
20242
		currentDataGrouping = series.currentDataGrouping;
20243
		dateTimeLabelFormats = dataGroupingOptions.dateTimeLabelFormats;
20244
 
20245
		// if we have grouped data, use the grouping information to get the right format
20246
		if (currentDataGrouping) {
20247
			labelFormats = dateTimeLabelFormats[currentDataGrouping.unitName];
20248
			if (currentDataGrouping.count === 1) {
20249
				xDateFormat = labelFormats[0];
20250
			} else {
20251
				xDateFormat = labelFormats[1];
20252
				xDateFormatEnd = labelFormats[2];
20253
			}
20254
		// if not grouped, and we don't have set the xDateFormat option, get the best fit,
20255
		// so if the least distance between points is one minute, show it, but if the
20256
		// least distance is one day, skip hours and minutes etc.
20257
		} else if (!xDateFormat && dateTimeLabelFormats) {
20258
			xDateFormat = tooltip.getXDateFormat(point, tooltipOptions, xAxis);
20259
		}
20260
 
20261
		// now format the key
20262
		formattedKey = dateFormat(xDateFormat, point.key);
20263
		if (xDateFormatEnd) {
20264
			formattedKey += dateFormat(xDateFormatEnd, point.key + currentDataGrouping.totalRange - 1);
20265
		}
20266
 
20267
		// return the replaced format
20268
		ret = tooltipOptions[(isFooter ? 'footer' : 'header') + 'Format'].replace('{point.key}', formattedKey);
20269
 
20270
	// else, fall back to the regular formatter
20271
	} else {
20272
		ret = baseTooltipFooterHeaderFormatter.call(tooltip, point, isFooter);
20273
	}
20274
 
20275
	return ret;
20276
};
20277
 
20278
/**
20279
 * Extend the series destroyer
20280
 */
20281
seriesProto.destroy = function () {
20282
	var series = this,
20283
		groupedData = series.groupedData || [],
20284
		i = groupedData.length;
20285
 
20286
	while (i--) {
20287
		if (groupedData[i]) {
20288
			groupedData[i].destroy();
20289
		}
20290
	}
20291
	baseDestroy.apply(series);
20292
};
20293
 
20294
 
20295
// Handle default options for data grouping. This must be set at runtime because some series types are
20296
// defined after this.
20297
wrap(seriesProto, 'setOptions', function (proceed, itemOptions) {
20298
 
20299
	var options = proceed.call(this, itemOptions),
20300
		type = this.type,
20301
		plotOptions = this.chart.options.plotOptions,
20302
		defaultOptions = defaultPlotOptions[type].dataGrouping;
20303
 
20304
	if (specificOptions[type]) { // #1284
20305
		if (!defaultOptions) {
20306
			defaultOptions = merge(commonOptions, specificOptions[type]);
20307
		}
20308
 
20309
		options.dataGrouping = merge(
20310
			defaultOptions,
20311
			plotOptions.series && plotOptions.series.dataGrouping, // #1228
20312
			plotOptions[type].dataGrouping, // Set by the StockChart constructor
20313
			itemOptions.dataGrouping
20314
		);
20315
	}
20316
 
20317
	if (this.chart.options._stock) {
20318
		this.requireSorting = true;
20319
	}
20320
 
20321
	return options;
20322
});
20323
 
20324
 
20325
/**
20326
 * When resetting the scale reset the hasProccessed flag to avoid taking previous data grouping
20327
 * of neighbour series into accound when determining group pixel width (#2692).
20328
 */
20329
wrap(Axis.prototype, 'setScale', function (proceed) {
20330
	proceed.call(this);
20331
	each(this.series, function (series) {
20332
		series.hasProcessed = false;
20333
	});
20334
});
20335
 
20336
/**
20337
 * Get the data grouping pixel width based on the greatest defined individual width
20338
 * of the axis' series, and if whether one of the axes need grouping.
20339
 */
20340
Axis.prototype.getGroupPixelWidth = function () {
20341
 
20342
	var series = this.series,
20343
		len = series.length,
20344
		i,
20345
		groupPixelWidth = 0,
20346
		doGrouping = false,
20347
		dataLength,
20348
		dgOptions;
20349
 
20350
	// If multiple series are compared on the same x axis, give them the same
20351
	// group pixel width (#334)
20352
	i = len;
20353
	while (i--) {
20354
		dgOptions = series[i].options.dataGrouping;
20355
		if (dgOptions) {
20356
			groupPixelWidth = mathMax(groupPixelWidth, dgOptions.groupPixelWidth);
20357
 
20358
		}
20359
	}
20360
 
20361
	// If one of the series needs grouping, apply it to all (#1634)
20362
	i = len;
20363
	while (i--) {
20364
		dgOptions = series[i].options.dataGrouping;
20365
 
20366
		if (dgOptions && series[i].hasProcessed) { // #2692
20367
 
20368
			dataLength = (series[i].processedXData || series[i].data).length;
20369
 
20370
			// Execute grouping if the amount of points is greater than the limit defined in groupPixelWidth
20371
			if (series[i].groupPixelWidth || dataLength > (this.chart.plotSizeX / groupPixelWidth) || (dataLength && dgOptions.forced)) {
20372
				doGrouping = true;
20373
			}
20374
		}
20375
	}
20376
 
20377
	return doGrouping ? groupPixelWidth : 0;
20378
};
20379
 
20380
/**
20381
 * Force data grouping on all the axis' series.
20382
 */
20383
Axis.prototype.setDataGrouping = function (dataGrouping, redraw) {
20384
	var i;
20385
 
20386
	redraw = pick(redraw, true);
20387
 
20388
	if (!dataGrouping) {
20389
		dataGrouping = {
20390
			forced: false,
20391
			units: null
20392
		};
20393
	}
20394
 
20395
	// Axis is instantiated, update all series
20396
	if (this instanceof Axis) {
20397
		i = this.series.length;
20398
		while (i--) {
20399
			this.series[i].update({
20400
				dataGrouping: dataGrouping
20401
			}, false);
20402
		}
20403
 
20404
	// Axis not yet instanciated, alter series options
20405
	} else {
20406
		each(this.chart.options.series, function (seriesOptions) {
20407
			seriesOptions.dataGrouping = dataGrouping;
20408
		}, false);
20409
	}
20410
 
20411
	if (redraw) {
20412
		this.chart.redraw();
20413
	}
20414
};
20415
 
20416
 
20417
 
20418
/* ****************************************************************************
20419
 * End data grouping module												   *
20420
 ******************************************************************************//* ****************************************************************************
20421
 * Start OHLC series code													 *
20422
 *****************************************************************************/
20423
 
20424
// 1 - Set default options
20425
defaultPlotOptions.ohlc = merge(defaultPlotOptions.column, {
20426
	lineWidth: 1,
20427
	tooltip: {
20428
		pointFormat: '<span style="color:{point.color}">\u25CF</span> <b> {series.name}</b><br/>' + // docs
20429
			'Open: {point.open}<br/>' +
20430
			'High: {point.high}<br/>' +
20431
			'Low: {point.low}<br/>' +
20432
			'Close: {point.close}<br/>'
20433
	},
20434
	states: {
20435
		hover: {
20436
			lineWidth: 3
20437
		}
20438
	},
20439
	threshold: null
20440
	//upColor: undefined
20441
});
20442
 
20443
// 2 - Create the OHLCSeries object
20444
var OHLCSeries = extendClass(seriesTypes.column, {
20445
	type: 'ohlc',
20446
	pointArrayMap: ['open', 'high', 'low', 'close'], // array point configs are mapped to this
20447
	toYData: function (point) { // return a plain array for speedy calculation
20448
		return [point.open, point.high, point.low, point.close];
20449
	},
20450
	pointValKey: 'high',
20451
 
20452
	pointAttrToOptions: { // mapping between SVG attributes and the corresponding options
20453
		stroke: 'color',
20454
		'stroke-width': 'lineWidth'
20455
	},
20456
	upColorProp: 'stroke',
20457
 
20458
	/**
20459
	 * Postprocess mapping between options and SVG attributes
20460
	 */
20461
	getAttribs: function () {
20462
		seriesTypes.column.prototype.getAttribs.apply(this, arguments);
20463
		var series = this,
20464
			options = series.options,
20465
			stateOptions = options.states,
20466
			upColor = options.upColor || series.color,
20467
			seriesDownPointAttr = merge(series.pointAttr),
20468
			upColorProp = series.upColorProp;
20469
 
20470
		seriesDownPointAttr[''][upColorProp] = upColor;
20471
		seriesDownPointAttr.hover[upColorProp] = stateOptions.hover.upColor || upColor;
20472
		seriesDownPointAttr.select[upColorProp] = stateOptions.select.upColor || upColor;
20473
 
20474
		each(series.points, function (point) {
20475
			if (point.open < point.close && !point.options.color) {
20476
				point.pointAttr = seriesDownPointAttr;
20477
			}
20478
		});
20479
	},
20480
 
20481
	/**
20482
	 * Translate data points from raw values x and y to plotX and plotY
20483
	 */
20484
	translate: function () {
20485
		var series = this,
20486
			yAxis = series.yAxis;
20487
 
20488
		seriesTypes.column.prototype.translate.apply(series);
20489
 
20490
		// do the translation
20491
		each(series.points, function (point) {
20492
			// the graphics
20493
			if (point.open !== null) {
20494
				point.plotOpen = yAxis.translate(point.open, 0, 1, 0, 1);
20495
			}
20496
			if (point.close !== null) {
20497
				point.plotClose = yAxis.translate(point.close, 0, 1, 0, 1);
20498
			}
20499
 
20500
		});
20501
	},
20502
 
20503
	/**
20504
	 * Draw the data points
20505
	 */
20506
	drawPoints: function () {
20507
		var series = this,
20508
			points = series.points,
20509
			chart = series.chart,
20510
			pointAttr,
20511
			plotOpen,
20512
			plotClose,
20513
			crispCorr,
20514
			halfWidth,
20515
			path,
20516
			graphic,
20517
			crispX;
20518
 
20519
 
20520
		each(points, function (point) {
20521
			if (point.plotY !== UNDEFINED) {
20522
 
20523
				graphic = point.graphic;
20524
				pointAttr = point.pointAttr[point.selected ? 'selected' : ''] || series.pointAttr[NORMAL_STATE];
20525
 
20526
				// crisp vector coordinates
20527
				crispCorr = (pointAttr['stroke-width'] % 2) / 2;
20528
				crispX = mathRound(point.plotX) - crispCorr;  // #2596
20529
				halfWidth = mathRound(point.shapeArgs.width / 2);
20530
 
20531
				// the vertical stem
20532
				path = [
20533
					'M',
20534
					crispX, mathRound(point.yBottom),
20535
					'L',
20536
					crispX, mathRound(point.plotY)
20537
				];
20538
 
20539
				// open
20540
				if (point.open !== null) {
20541
					plotOpen = mathRound(point.plotOpen) + crispCorr;
20542
					path.push(
20543
						'M',
20544
						crispX,
20545
						plotOpen,
20546
						'L',
20547
						crispX - halfWidth,
20548
						plotOpen
20549
					);
20550
				}
20551
 
20552
				// close
20553
				if (point.close !== null) {
20554
					plotClose = mathRound(point.plotClose) + crispCorr;
20555
					path.push(
20556
						'M',
20557
						crispX,
20558
						plotClose,
20559
						'L',
20560
						crispX + halfWidth,
20561
						plotClose
20562
					);
20563
				}
20564
 
20565
				// create and/or update the graphic
20566
				if (graphic) {
20567
					graphic
20568
						.attr(pointAttr) // #3897
20569
						.animate({ d: path });
20570
				} else {
20571
					point.graphic = chart.renderer.path(path)
20572
						.attr(pointAttr)
20573
						.add(series.group);
20574
				}
20575
 
20576
			}
20577
 
20578
 
20579
		});
20580
 
20581
	},
20582
 
20583
	/**
20584
	 * Disable animation
20585
	 */
20586
	animate: null
20587
 
20588
 
20589
});
20590
seriesTypes.ohlc = OHLCSeries;
20591
/* ****************************************************************************
20592
 * End OHLC series code													   *
20593
 *****************************************************************************/
20594
/* ****************************************************************************
20595
 * Start Candlestick series code											  *
20596
 *****************************************************************************/
20597
 
20598
// 1 - set default options
20599
defaultPlotOptions.candlestick = merge(defaultPlotOptions.column, {
20600
	lineColor: 'black',
20601
	lineWidth: 1,
20602
	states: {
20603
		hover: {
20604
			lineWidth: 2
20605
		}
20606
	},
20607
	tooltip: defaultPlotOptions.ohlc.tooltip,
20608
	threshold: null,
20609
	upColor: 'white'
20610
	// upLineColor: null
20611
});
20612
 
20613
// 2 - Create the CandlestickSeries object
20614
var CandlestickSeries = extendClass(OHLCSeries, {
20615
	type: 'candlestick',
20616
 
20617
	/**
20618
	 * One-to-one mapping from options to SVG attributes
20619
	 */
20620
	pointAttrToOptions: { // mapping between SVG attributes and the corresponding options
20621
		fill: 'color',
20622
		stroke: 'lineColor',
20623
		'stroke-width': 'lineWidth'
20624
	},
20625
	upColorProp: 'fill',
20626
 
20627
	/**
20628
	 * Postprocess mapping between options and SVG attributes
20629
	 */
20630
	getAttribs: function () {
20631
		seriesTypes.ohlc.prototype.getAttribs.apply(this, arguments);
20632
		var series = this,
20633
			options = series.options,
20634
			stateOptions = options.states,
20635
			upLineColor = options.upLineColor || options.lineColor,
20636
			hoverStroke = stateOptions.hover.upLineColor || upLineColor,
20637
			selectStroke = stateOptions.select.upLineColor || upLineColor;
20638
 
20639
		// Add custom line color for points going up (close > open).
20640
		// Fill is handled by OHLCSeries' getAttribs.
20641
		each(series.points, function (point) {
20642
			if (point.open < point.close) {
20643
 
20644
				// If an individual line color is set, we need to merge the
20645
				// point attributes, because they are shared between all up
20646
				// points by inheritance from OHCLSeries.
20647
				if (point.lineColor) {
20648
					point.pointAttr = merge(point.pointAttr);
20649
					upLineColor = point.lineColor;
20650
				}
20651
 
20652
				point.pointAttr[''].stroke = upLineColor;
20653
				point.pointAttr.hover.stroke = hoverStroke;
20654
				point.pointAttr.select.stroke = selectStroke;
20655
			}
20656
		});
20657
	},
20658
 
20659
	/**
20660
	 * Draw the data points
20661
	 */
20662
	drawPoints: function () {
20663
		var series = this,  //state = series.state,
20664
			points = series.points,
20665
			chart = series.chart,
20666
			pointAttr,
20667
			seriesPointAttr = series.pointAttr[''],
20668
			plotOpen,
20669
			plotClose,
20670
			topBox,
20671
			bottomBox,
20672
			hasTopWhisker,
20673
			hasBottomWhisker,
20674
			crispCorr,
20675
			crispX,
20676
			graphic,
20677
			path,
20678
			halfWidth;
20679
 
20680
 
20681
		each(points, function (point) {
20682
 
20683
			graphic = point.graphic;
20684
			if (point.plotY !== UNDEFINED) {
20685
 
20686
				pointAttr = point.pointAttr[point.selected ? 'selected' : ''] || seriesPointAttr;
20687
 
20688
				// crisp vector coordinates
20689
				crispCorr = (pointAttr['stroke-width'] % 2) / 2;
20690
				crispX = mathRound(point.plotX) - crispCorr; // #2596
20691
				plotOpen = point.plotOpen;
20692
				plotClose = point.plotClose;
20693
				topBox = math.min(plotOpen, plotClose);
20694
				bottomBox = math.max(plotOpen, plotClose);
20695
				halfWidth = mathRound(point.shapeArgs.width / 2);
20696
				hasTopWhisker = mathRound(topBox) !== mathRound(point.plotY);
20697
				hasBottomWhisker = bottomBox !== point.yBottom;
20698
				topBox = mathRound(topBox) + crispCorr;
20699
				bottomBox = mathRound(bottomBox) + crispCorr;
20700
 
20701
				// create the path
20702
				path = [
20703
					'M',
20704
					crispX - halfWidth, bottomBox,
20705
					'L',
20706
					crispX - halfWidth, topBox,
20707
					'L',
20708
					crispX + halfWidth, topBox,
20709
					'L',
20710
					crispX + halfWidth, bottomBox,
20711
					'Z', // Use a close statement to ensure a nice rectangle #2602
20712
					'M',
20713
					crispX, topBox,
20714
					'L',
20715
					crispX, hasTopWhisker ? mathRound(point.plotY) : topBox, // #460, #2094
20716
					'M',
20717
					crispX, bottomBox,
20718
					'L',
20719
					crispX, hasBottomWhisker ? mathRound(point.yBottom) : bottomBox // #460, #2094
20720
				];
20721
 
20722
				if (graphic) {
20723
					graphic
20724
						.attr(pointAttr) // #3897
20725
						.animate({ d: path });
20726
				} else {
20727
					point.graphic = chart.renderer.path(path)
20728
						.attr(pointAttr)
20729
						.add(series.group)
20730
						.shadow(series.options.shadow);
20731
				}
20732
 
20733
			}
20734
		});
20735
 
20736
	}
20737
 
20738
 
20739
});
20740
 
20741
seriesTypes.candlestick = CandlestickSeries;
20742
 
20743
/* ****************************************************************************
20744
 * End Candlestick series code												*
20745
 *****************************************************************************/
20746
/* ****************************************************************************
20747
 * Start Flags series code													*
20748
 *****************************************************************************/
20749
 
20750
var symbols = SVGRenderer.prototype.symbols;
20751
 
20752
// 1 - set default options
20753
defaultPlotOptions.flags = merge(defaultPlotOptions.column, {
20754
	fillColor: 'white',
20755
	lineWidth: 1,
20756
	pointRange: 0, // #673
20757
	//radius: 2,
20758
	shape: 'flag',
20759
	stackDistance: 12,
20760
	states: {
20761
		hover: {
20762
			lineColor: 'black',
20763
			fillColor: '#FCFFC5'
20764
		}
20765
	},
20766
	style: {
20767
		fontSize: '11px',
20768
		fontWeight: 'bold',
20769
		textAlign: 'center'
20770
	},
20771
	tooltip: {
20772
		pointFormat: '{point.text}<br/>'
20773
	},
20774
	threshold: null,
20775
	y: -30
20776
});
20777
 
20778
// 2 - Create the CandlestickSeries object
20779
seriesTypes.flags = extendClass(seriesTypes.column, {
20780
	type: 'flags',
20781
	sorted: false,
20782
	noSharedTooltip: true,
20783
	allowDG: false,
20784
	takeOrdinalPosition: false, // #1074
20785
	trackerGroups: ['markerGroup'],
20786
	forceCrop: true,
20787
	/**
20788
	 * Inherit the initialization from base Series
20789
	 */
20790
	init: Series.prototype.init,
20791
 
20792
	/**
20793
	 * One-to-one mapping from options to SVG attributes
20794
	 */
20795
	pointAttrToOptions: { // mapping between SVG attributes and the corresponding options
20796
		fill: 'fillColor',
20797
		stroke: 'color',
20798
		'stroke-width': 'lineWidth',
20799
		r: 'radius'
20800
	},
20801
 
20802
	/**
20803
	 * Extend the translate method by placing the point on the related series
20804
	 */
20805
	translate: function () {
20806
 
20807
		seriesTypes.column.prototype.translate.apply(this);
20808
 
20809
		var series = this,
20810
			options = series.options,
20811
			chart = series.chart,
20812
			points = series.points,
20813
			cursor = points.length - 1,
20814
			point,
20815
			lastPoint,
20816
			optionsOnSeries = options.onSeries,
20817
			onSeries = optionsOnSeries && chart.get(optionsOnSeries),
20818
			step = onSeries && onSeries.options.step,
20819
			onData = onSeries && onSeries.points,
20820
			i = onData && onData.length,
20821
			xAxis = series.xAxis,
20822
			xAxisExt = xAxis.getExtremes(),
20823
			leftPoint,
20824
			lastX,
20825
			rightPoint,
20826
			currentDataGrouping;
20827
 
20828
		// relate to a master series
20829
		if (onSeries && onSeries.visible && i) {
20830
			currentDataGrouping = onSeries.currentDataGrouping;
20831
			lastX = onData[i - 1].x + (currentDataGrouping ? currentDataGrouping.totalRange : 0); // #2374
20832
 
20833
			// sort the data points
20834
			points.sort(function (a, b) {
20835
				return (a.x - b.x);
20836
			});
20837
 
20838
			while (i-- && points[cursor]) {
20839
				point = points[cursor];
20840
				leftPoint = onData[i];
20841
 
20842
				if (leftPoint.x <= point.x && leftPoint.plotY !== UNDEFINED) {
20843
					if (point.x <= lastX) { // #803
20844
 
20845
						point.plotY = leftPoint.plotY;
20846
 
20847
						// interpolate between points, #666
20848
						if (leftPoint.x < point.x && !step) {
20849
							rightPoint = onData[i + 1];
20850
							if (rightPoint && rightPoint.plotY !== UNDEFINED) {
20851
								point.plotY +=
20852
									((point.x - leftPoint.x) / (rightPoint.x - leftPoint.x)) * // the distance ratio, between 0 and 1
20853
									(rightPoint.plotY - leftPoint.plotY); // the y distance
20854
							}
20855
						}
20856
					}
20857
					cursor--;
20858
					i++; // check again for points in the same x position
20859
					if (cursor < 0) {
20860
						break;
20861
					}
20862
				}
20863
			}
20864
		}
20865
 
20866
		// Add plotY position and handle stacking
20867
		each(points, function (point, i) {
20868
 
20869
			var stackIndex;
20870
 
20871
			// Undefined plotY means the point is either on axis, outside series range or hidden series.
20872
			// If the series is outside the range of the x axis it should fall through with
20873
			// an undefined plotY, but then we must remove the shapeArgs (#847).
20874
			if (point.plotY === UNDEFINED) {
20875
				if (point.x >= xAxisExt.min && point.x <= xAxisExt.max) { // we're inside xAxis range
20876
					point.plotY = chart.chartHeight - xAxis.bottom - (xAxis.opposite ? xAxis.height : 0) + xAxis.offset - chart.plotTop;
20877
				} else {
20878
					point.shapeArgs = {}; // 847
20879
				}
20880
			}
20881
			// if multiple flags appear at the same x, order them into a stack
20882
			lastPoint = points[i - 1];
20883
			if (lastPoint && lastPoint.plotX === point.plotX) {
20884
				if (lastPoint.stackIndex === UNDEFINED) {
20885
					lastPoint.stackIndex = 0;
20886
				}
20887
				stackIndex = lastPoint.stackIndex + 1;
20888
			}
20889
			point.stackIndex = stackIndex; // #3639
20890
		});
20891
 
20892
 
20893
	},
20894
 
20895
	/**
20896
	 * Draw the markers
20897
	 */
20898
	drawPoints: function () {
20899
		var series = this,
20900
			pointAttr,
20901
			seriesPointAttr = series.pointAttr[''],
20902
			points = series.points,
20903
			chart = series.chart,
20904
			renderer = chart.renderer,
20905
			plotX,
20906
			plotY,
20907
			options = series.options,
20908
			optionsY = options.y,
20909
			shape,
20910
			i,
20911
			point,
20912
			graphic,
20913
			stackIndex,
20914
			anchorX,
20915
			anchorY,
20916
			outsideRight;
20917
 
20918
		i = points.length;
20919
		while (i--) {
20920
			point = points[i];
20921
			outsideRight = point.plotX > series.xAxis.len;
20922
			plotX = point.plotX;
20923
			if (plotX > 0) { // #3119
20924
				plotX -= pick(point.lineWidth, options.lineWidth) % 2; // #4285
20925
			}
20926
			stackIndex = point.stackIndex;
20927
			shape = point.options.shape || options.shape;
20928
			plotY = point.plotY;
20929
			if (plotY !== UNDEFINED) {
20930
				plotY = point.plotY + optionsY - (stackIndex !== UNDEFINED && stackIndex * options.stackDistance);
20931
			}
20932
			anchorX = stackIndex ? UNDEFINED : point.plotX; // skip connectors for higher level stacked points
20933
			anchorY = stackIndex ? UNDEFINED : point.plotY;
20934
 
20935
			graphic = point.graphic;
20936
 
20937
			// only draw the point if y is defined and the flag is within the visible area
20938
			if (plotY !== UNDEFINED && plotX >= 0 && !outsideRight) {
20939
				// shortcuts
20940
				pointAttr = point.pointAttr[point.selected ? 'select' : ''] || seriesPointAttr;
20941
				if (graphic) { // update
20942
					graphic.attr({
20943
						x: plotX,
20944
						y: plotY,
20945
						r: pointAttr.r,
20946
						anchorX: anchorX,
20947
						anchorY: anchorY
20948
					});
20949
				} else {
20950
					graphic = point.graphic = renderer.label(
20951
						point.options.title || options.title || 'A',
20952
						plotX,
20953
						plotY,
20954
						shape,
20955
						anchorX,
20956
						anchorY,
20957
						options.useHTML
20958
					)
20959
					.css(merge(options.style, point.style))
20960
					.attr(pointAttr)
20961
					.attr({
20962
						align: shape === 'flag' ? 'left' : 'center',
20963
						width: options.width,
20964
						height: options.height
20965
					})
20966
					.add(series.markerGroup)
20967
					.shadow(options.shadow);
20968
 
20969
				}
20970
 
20971
				// Set the tooltip anchor position
20972
				point.tooltipPos = [plotX, plotY];
20973
 
20974
			} else if (graphic) {
20975
				point.graphic = graphic.destroy();
20976
			}
20977
 
20978
		}
20979
 
20980
	},
20981
 
20982
	/**
20983
	 * Extend the column trackers with listeners to expand and contract stacks
20984
	 */
20985
	drawTracker: function () {
20986
		var series = this,
20987
			points = series.points;
20988
 
20989
		TrackerMixin.drawTrackerPoint.apply(this);
20990
 
20991
		// Bring each stacked flag up on mouse over, this allows readability of vertically
20992
		// stacked elements as well as tight points on the x axis. #1924.
20993
		each(points, function (point) {
20994
			var graphic = point.graphic;
20995
			if (graphic) {
20996
				addEvent(graphic.element, 'mouseover', function () {
20997
 
20998
					// Raise this point
20999
					if (point.stackIndex > 0 && !point.raised) {
21000
						point._y = graphic.y;
21001
						graphic.attr({
21002
							y: point._y - 8
21003
						});
21004
						point.raised = true;
21005
					}
21006
 
21007
					// Revert other raised points
21008
					each(points, function (otherPoint) {
21009
						if (otherPoint !== point && otherPoint.raised && otherPoint.graphic) {
21010
							otherPoint.graphic.attr({
21011
								y: otherPoint._y
21012
							});
21013
							otherPoint.raised = false;
21014
						}
21015
					});
21016
				});
21017
			}
21018
		});
21019
	},
21020
 
21021
	/**
21022
	 * Disable animation
21023
	 */
21024
	animate: noop,
21025
	buildKDTree: noop,
21026
	setClip: noop
21027
 
21028
});
21029
 
21030
// create the flag icon with anchor
21031
symbols.flag = function (x, y, w, h, options) {
21032
	var anchorX = (options && options.anchorX) || x,
21033
		anchorY = (options &&  options.anchorY) || y;
21034
 
21035
	return [
21036
		'M', anchorX, anchorY,
21037
		'L', x, y + h,
21038
		x, y,
21039
		x + w, y,
21040
		x + w, y + h,
21041
		x, y + h,
21042
		'Z'
21043
	];
21044
};
21045
 
21046
// create the circlepin and squarepin icons with anchor
21047
each(['circle', 'square'], function (shape) {
21048
	symbols[shape + 'pin'] = function (x, y, w, h, options) {
21049
 
21050
		var anchorX = options && options.anchorX,
21051
			anchorY = options &&  options.anchorY,
21052
			path,
21053
			labelTopOrBottomY;
21054
 
21055
		// For single-letter flags, make sure circular flags are not taller than their width
21056
		if (shape === 'circle' && h > w) {
21057
			x -= mathRound((h - w) / 2);
21058
			w = h;
21059
		}
21060
 
21061
		path = symbols[shape](x, y, w, h);
21062
 
21063
		if (anchorX && anchorY) {
21064
			// if the label is below the anchor, draw the connecting line from the top edge of the label
21065
			// otherwise start drawing from the bottom edge
21066
			labelTopOrBottomY = (y > anchorY) ? y : y + h;
21067
			path.push('M', anchorX, labelTopOrBottomY, 'L', anchorX, anchorY);
21068
		}
21069
 
21070
		return path;
21071
	};
21072
});
21073
 
21074
// The symbol callbacks are generated on the SVGRenderer object in all browsers. Even
21075
// VML browsers need this in order to generate shapes in export. Now share
21076
// them with the VMLRenderer.
21077
if (Renderer === Highcharts.VMLRenderer) {
21078
	each(['flag', 'circlepin', 'squarepin'], function (shape) {
21079
		VMLRenderer.prototype.symbols[shape] = symbols[shape];
21080
	});
21081
}
21082
 
21083
/* ****************************************************************************
21084
 * End Flags series code													  *
21085
 *****************************************************************************/
21086
/* ****************************************************************************
21087
 * Start Scroller code														*
21088
 *****************************************************************************/
21089
var units = [].concat(defaultDataGroupingUnits), // copy
21090
	defaultSeriesType,
21091
 
21092
	// Finding the min or max of a set of variables where we don't know if they are defined,
21093
	// is a pattern that is repeated several places in Highcharts. Consider making this
21094
	// a global utility method.
21095
	numExt = function (extreme) {
21096
		var numbers = grep(arguments, function (n) { return typeof n === 'number'; });
21097
		if (numbers.length) {
21098
			return Math[extreme].apply(0, numbers);
21099
		}
21100
	};
21101
 
21102
// add more resolution to units
21103
units[4] = ['day', [1, 2, 3, 4]]; // allow more days
21104
units[5] = ['week', [1, 2, 3]]; // allow more weeks
21105
 
21106
defaultSeriesType = seriesTypes.areaspline === UNDEFINED ? 'line' : 'areaspline';
21107
 
21108
extend(defaultOptions, {
21109
	navigator: {
21110
		//enabled: true,
21111
		handles: {
21112
			backgroundColor: '#ebe7e8',
21113
			borderColor: '#b2b1b6'
21114
		},
21115
		height: 40,
21116
		margin: 25,
21117
		maskFill: 'rgba(128,179,236,0.3)',
21118
		maskInside: true,
21119
		outlineColor: '#b2b1b6',
21120
		outlineWidth: 1,
21121
		series: {
21122
			type: defaultSeriesType,
21123
			color: '#4572A7',
21124
			compare: null,
21125
			fillOpacity: 0.05,
21126
			dataGrouping: {
21127
				approximation: 'average',
21128
				enabled: true,
21129
				groupPixelWidth: 2,
21130
				smoothed: true,
21131
				units: units
21132
			},
21133
			dataLabels: {
21134
				enabled: false,
21135
				zIndex: 2 // #1839
21136
			},
21137
			id: PREFIX + 'navigator-series',
21138
			lineColor: null, // Allow color setting while disallowing default candlestick setting (#4602)
21139
			lineWidth: 1,
21140
			marker: {
21141
				enabled: false
21142
			},
21143
			pointRange: 0,
21144
			shadow: false,
21145
			threshold: null
21146
		},
21147
		//top: undefined,
21148
		xAxis: {
21149
			tickWidth: 0,
21150
			lineWidth: 0,
21151
			gridLineColor: '#EEE',
21152
			gridLineWidth: 1,
21153
			tickPixelInterval: 200,
21154
			labels: {
21155
				align: 'left',
21156
				style: {
21157
					color: '#888'
21158
				},
21159
				x: 3,
21160
				y: -4
21161
			},
21162
			crosshair: false
21163
		},
21164
		yAxis: {
21165
			gridLineWidth: 0,
21166
			startOnTick: false,
21167
			endOnTick: false,
21168
			minPadding: 0.1,
21169
			maxPadding: 0.1,
21170
			labels: {
21171
				enabled: false
21172
			},
21173
			crosshair: false,
21174
			title: {
21175
				text: null
21176
			},
21177
			tickWidth: 0
21178
		}
21179
	},
21180
	scrollbar: {
21181
		//enabled: true
21182
		height: isTouchDevice ? 20 : 14,
21183
		barBackgroundColor: '#bfc8d1',
21184
		barBorderRadius: 0,
21185
		barBorderWidth: 1,
21186
		barBorderColor: '#bfc8d1',
21187
		buttonArrowColor: '#666',
21188
		buttonBackgroundColor: '#ebe7e8',
21189
		buttonBorderColor: '#bbb',
21190
		buttonBorderRadius: 0,
21191
		buttonBorderWidth: 1,
21192
		minWidth: 6,
21193
		rifleColor: '#666',
21194
		trackBackgroundColor: '#eeeeee',
21195
		trackBorderColor: '#eeeeee',
21196
		trackBorderWidth: 1,
21197
		// trackBorderRadius: 0
21198
		liveRedraw: hasSVG && !isTouchDevice
21199
	}
21200
});
21201
 
21202
/**
21203
 * The Scroller class
21204
 * @param {Object} chart
21205
 */
21206
function Scroller(chart) {
21207
	var chartOptions = chart.options,
21208
		navigatorOptions = chartOptions.navigator,
21209
		navigatorEnabled = navigatorOptions.enabled,
21210
		scrollbarOptions = chartOptions.scrollbar,
21211
		scrollbarEnabled = scrollbarOptions.enabled,
21212
		height = navigatorEnabled ? navigatorOptions.height : 0,
21213
		scrollbarHeight = scrollbarEnabled ? scrollbarOptions.height : 0;
21214
 
21215
 
21216
	this.handles = [];
21217
	this.scrollbarButtons = [];
21218
	this.elementsToDestroy = []; // Array containing the elements to destroy when Scroller is destroyed
21219
 
21220
	this.chart = chart;
21221
	this.setBaseSeries();
21222
 
21223
	this.height = height;
21224
	this.scrollbarHeight = scrollbarHeight;
21225
	this.scrollbarEnabled = scrollbarEnabled;
21226
	this.navigatorEnabled = navigatorEnabled;
21227
	this.navigatorOptions = navigatorOptions;
21228
	this.scrollbarOptions = scrollbarOptions;
21229
	this.outlineHeight = height + scrollbarHeight;
21230
 
21231
	// Run scroller
21232
	this.init();
21233
}
21234
 
21235
Scroller.prototype = {
21236
	/**
21237
	 * Draw one of the handles on the side of the zoomed range in the navigator
21238
	 * @param {Number} x The x center for the handle
21239
	 * @param {Number} index 0 for left and 1 for right
21240
	 */
21241
	drawHandle: function (x, index) {
21242
		var scroller = this,
21243
			chart = scroller.chart,
21244
			renderer = chart.renderer,
21245
			elementsToDestroy = scroller.elementsToDestroy,
21246
			handles = scroller.handles,
21247
			handlesOptions = scroller.navigatorOptions.handles,
21248
			attr = {
21249
				fill: handlesOptions.backgroundColor,
21250
				stroke: handlesOptions.borderColor,
21251
				'stroke-width': 1
21252
			},
21253
			tempElem;
21254
 
21255
		// create the elements
21256
		if (!scroller.rendered) {
21257
			// the group
21258
			handles[index] = renderer.g('navigator-handle-' + ['left', 'right'][index])
21259
				.css({ cursor: 'ew-resize' })
21260
				.attr({ zIndex: 4 - index }) // zIndex = 3 for right handle, 4 for left
21261
				.add();
21262
 
21263
			// the rectangle
21264
			tempElem = renderer.rect(-4.5, 0, 9, 16, 0, 1)
21265
				.attr(attr)
21266
				.add(handles[index]);
21267
			elementsToDestroy.push(tempElem);
21268
 
21269
			// the rifles
21270
			tempElem = renderer.path([
21271
					'M',
21272
					-1.5, 4,
21273
					'L',
21274
					-1.5, 12,
21275
					'M',
21276
					0.5, 4,
21277
					'L',
21278
					0.5, 12
21279
				]).attr(attr)
21280
				.add(handles[index]);
21281
			elementsToDestroy.push(tempElem);
21282
		}
21283
 
21284
		// Place it
21285
		handles[index][chart.isResizing ? 'animate' : 'attr']({
21286
			translateX: scroller.scrollerLeft + scroller.scrollbarHeight + parseInt(x, 10),
21287
			translateY: scroller.top + scroller.height / 2 - 8
21288
		});
21289
	},
21290
 
21291
	/**
21292
	 * Draw the scrollbar buttons with arrows
21293
	 * @param {Number} index 0 is left, 1 is right
21294
	 */
21295
	drawScrollbarButton: function (index) {
21296
		var scroller = this,
21297
			chart = scroller.chart,
21298
			renderer = chart.renderer,
21299
			elementsToDestroy = scroller.elementsToDestroy,
21300
			scrollbarButtons = scroller.scrollbarButtons,
21301
			scrollbarHeight = scroller.scrollbarHeight,
21302
			scrollbarOptions = scroller.scrollbarOptions,
21303
			tempElem;
21304
 
21305
		if (!scroller.rendered) {
21306
			scrollbarButtons[index] = renderer.g().add(scroller.scrollbarGroup);
21307
 
21308
			tempElem = renderer.rect(
21309
					-0.5,
21310
					-0.5,
21311
					scrollbarHeight + 1, // +1 to compensate for crispifying in rect method
21312
					scrollbarHeight + 1,
21313
					scrollbarOptions.buttonBorderRadius,
21314
					scrollbarOptions.buttonBorderWidth
21315
				).attr({
21316
					stroke: scrollbarOptions.buttonBorderColor,
21317
					'stroke-width': scrollbarOptions.buttonBorderWidth,
21318
					fill: scrollbarOptions.buttonBackgroundColor
21319
				}).add(scrollbarButtons[index]);
21320
			elementsToDestroy.push(tempElem);
21321
 
21322
			tempElem = renderer.path([
21323
					'M',
21324
					scrollbarHeight / 2 + (index ? -1 : 1), scrollbarHeight / 2 - 3,
21325
					'L',
21326
					scrollbarHeight / 2 + (index ? -1 : 1), scrollbarHeight / 2 + 3,
21327
					scrollbarHeight / 2 + (index ? 2 : -2), scrollbarHeight / 2
21328
				]).attr({
21329
					fill: scrollbarOptions.buttonArrowColor
21330
				}).add(scrollbarButtons[index]);
21331
			elementsToDestroy.push(tempElem);
21332
		}
21333
 
21334
		// adjust the right side button to the varying length of the scroll track
21335
		if (index) {
21336
			scrollbarButtons[index].attr({
21337
				translateX: scroller.scrollerWidth - scrollbarHeight
21338
			});
21339
		}
21340
	},
21341
 
21342
	/**
21343
	 * Render the navigator and scroll bar
21344
	 * @param {Number} min X axis value minimum
21345
	 * @param {Number} max X axis value maximum
21346
	 * @param {Number} pxMin Pixel value minimum
21347
	 * @param {Number} pxMax Pixel value maximum
21348
	 */
21349
	render: function (min, max, pxMin, pxMax) {
21350
		var scroller = this,
21351
			chart = scroller.chart,
21352
			renderer = chart.renderer,
21353
			navigatorLeft,
21354
			navigatorWidth,
21355
			scrollerLeft,
21356
			scrollerWidth,
21357
			scrollbarGroup = scroller.scrollbarGroup,
21358
			navigatorGroup = scroller.navigatorGroup,
21359
			scrollbar = scroller.scrollbar,
21360
			xAxis = scroller.xAxis,
21361
			scrollbarTrack = scroller.scrollbarTrack,
21362
			scrollbarHeight = scroller.scrollbarHeight,
21363
			scrollbarEnabled = scroller.scrollbarEnabled,
21364
			navigatorOptions = scroller.navigatorOptions,
21365
			scrollbarOptions = scroller.scrollbarOptions,
21366
			scrollbarMinWidth = scrollbarOptions.minWidth,
21367
			height = scroller.height,
21368
			top = scroller.top,
21369
			navigatorEnabled = scroller.navigatorEnabled,
21370
			outlineWidth = navigatorOptions.outlineWidth,
21371
			halfOutline = outlineWidth / 2,
21372
			zoomedMin,
21373
			zoomedMax,
21374
			range,
21375
			scrX,
21376
			scrWidth,
21377
			scrollbarPad = 0,
21378
			outlineHeight = scroller.outlineHeight,
21379
			barBorderRadius = scrollbarOptions.barBorderRadius,
21380
			strokeWidth,
21381
			scrollbarStrokeWidth = scrollbarOptions.barBorderWidth,
21382
			centerBarX,
21383
			outlineTop = top + halfOutline,
21384
			verb,
21385
			unionExtremes;
21386
 
21387
		// Don't render the navigator until we have data (#486, #4202)
21388
		if (!defined(min) || isNaN(min)) {
21389
			return;
21390
		}
21391
 
21392
		scroller.navigatorLeft = navigatorLeft = pick(
21393
			xAxis.left,
21394
			chart.plotLeft + scrollbarHeight // in case of scrollbar only, without navigator
21395
		);
21396
		scroller.navigatorWidth = navigatorWidth = pick(xAxis.len, chart.plotWidth - 2 * scrollbarHeight);
21397
		scroller.scrollerLeft = scrollerLeft = navigatorLeft - scrollbarHeight;
21398
		scroller.scrollerWidth = scrollerWidth = scrollerWidth = navigatorWidth + 2 * scrollbarHeight;
21399
 
21400
		// Set the scroller x axis extremes to reflect the total. The navigator extremes
21401
		// should always be the extremes of the union of all series in the chart as
21402
		// well as the navigator series.
21403
		if (xAxis.getExtremes) {
21404
			unionExtremes = scroller.getUnionExtremes(true);
21405
 
21406
			if (unionExtremes && (unionExtremes.dataMin !== xAxis.min || unionExtremes.dataMax !== xAxis.max)) {
21407
				xAxis.setExtremes(unionExtremes.dataMin, unionExtremes.dataMax, true, false);
21408
			}
21409
		}
21410
 
21411
		// Get the pixel position of the handles
21412
		pxMin = pick(pxMin, xAxis.translate(min));
21413
		pxMax = pick(pxMax, xAxis.translate(max));
21414
		if (isNaN(pxMin) || mathAbs(pxMin) === Infinity) { // Verify (#1851, #2238)
21415
			pxMin = 0;
21416
			pxMax = scrollerWidth;
21417
		}
21418
 
21419
		// Are we below the minRange? (#2618)
21420
		if (xAxis.translate(pxMax, true) - xAxis.translate(pxMin, true) < chart.xAxis[0].minRange) {
21421
			return;
21422
		}
21423
 
21424
 
21425
		// handles are allowed to cross, but never exceed the plot area
21426
		scroller.zoomedMax = mathMin(mathMax(pxMin, pxMax, 0), navigatorWidth);
21427
		scroller.zoomedMin =
21428
			mathMax(scroller.fixedWidth ? scroller.zoomedMax - scroller.fixedWidth : mathMin(pxMin, pxMax), 0);
21429
		scroller.range = scroller.zoomedMax - scroller.zoomedMin;
21430
		zoomedMax = mathRound(scroller.zoomedMax);
21431
		zoomedMin = mathRound(scroller.zoomedMin);
21432
		range = zoomedMax - zoomedMin;
21433
 
21434
 
21435
 
21436
		// on first render, create all elements
21437
		if (!scroller.rendered) {
21438
 
21439
			if (navigatorEnabled) {
21440
 
21441
				// draw the navigator group
21442
				scroller.navigatorGroup = navigatorGroup = renderer.g('navigator')
21443
					.attr({
21444
						zIndex: 3
21445
					})
21446
					.add();
21447
 
21448
				scroller.leftShade = renderer.rect()
21449
					.attr({
21450
						fill: navigatorOptions.maskFill
21451
					}).add(navigatorGroup);
21452
 
21453
				if (navigatorOptions.maskInside) {
21454
					scroller.leftShade.css({ cursor: 'ew-resize '});
21455
				} else {
21456
					scroller.rightShade = renderer.rect()
21457
						.attr({
21458
							fill: navigatorOptions.maskFill
21459
						}).add(navigatorGroup);
21460
				}
21461
 
21462
 
21463
				scroller.outline = renderer.path()
21464
					.attr({
21465
						'stroke-width': outlineWidth,
21466
						stroke: navigatorOptions.outlineColor
21467
					})
21468
					.add(navigatorGroup);
21469
			}
21470
 
21471
			if (scrollbarEnabled) {
21472
 
21473
				// draw the scrollbar group
21474
				scroller.scrollbarGroup = scrollbarGroup = renderer.g('scrollbar').add();
21475
 
21476
				// the scrollbar track
21477
				strokeWidth = scrollbarOptions.trackBorderWidth;
21478
				scroller.scrollbarTrack = scrollbarTrack = renderer.rect().attr({
21479
					x: 0,
21480
					y: -strokeWidth % 2 / 2,
21481
					fill: scrollbarOptions.trackBackgroundColor,
21482
					stroke: scrollbarOptions.trackBorderColor,
21483
					'stroke-width': strokeWidth,
21484
					r: scrollbarOptions.trackBorderRadius || 0,
21485
					height: scrollbarHeight
21486
				}).add(scrollbarGroup);
21487
 
21488
				// the scrollbar itself
21489
				scroller.scrollbar = scrollbar = renderer.rect()
21490
					.attr({
21491
						y: -scrollbarStrokeWidth % 2 / 2,
21492
						height: scrollbarHeight,
21493
						fill: scrollbarOptions.barBackgroundColor,
21494
						stroke: scrollbarOptions.barBorderColor,
21495
						'stroke-width': scrollbarStrokeWidth,
21496
						r: barBorderRadius
21497
					})
21498
					.add(scrollbarGroup);
21499
 
21500
				scroller.scrollbarRifles = renderer.path()
21501
					.attr({
21502
						stroke: scrollbarOptions.rifleColor,
21503
						'stroke-width': 1
21504
					})
21505
					.add(scrollbarGroup);
21506
			}
21507
		}
21508
 
21509
		// place elements
21510
		verb = chart.isResizing ? 'animate' : 'attr';
21511
 
21512
		if (navigatorEnabled) {
21513
			scroller.leftShade[verb](navigatorOptions.maskInside ? {
21514
				x: navigatorLeft + zoomedMin,
21515
				y: top,
21516
				width: zoomedMax - zoomedMin,
21517
				height: height
21518
			} : {
21519
				x: navigatorLeft,
21520
				y: top,
21521
				width: zoomedMin,
21522
				height: height
21523
			});
21524
			if (scroller.rightShade) {
21525
				scroller.rightShade[verb]({
21526
					x: navigatorLeft + zoomedMax,
21527
					y: top,
21528
					width: navigatorWidth - zoomedMax,
21529
					height: height
21530
				});
21531
			}
21532
 
21533
			scroller.outline[verb]({ d: [
21534
				M,
21535
				scrollerLeft, outlineTop, // left
21536
				L,
21537
				navigatorLeft + zoomedMin - halfOutline, outlineTop, // upper left of zoomed range
21538
				navigatorLeft + zoomedMin - halfOutline, outlineTop + outlineHeight, // lower left of z.r.
21539
				L,
21540
				navigatorLeft + zoomedMax - halfOutline, outlineTop + outlineHeight, // lower right of z.r.
21541
				L,
21542
				navigatorLeft + zoomedMax - halfOutline, outlineTop, // upper right of z.r.
21543
				scrollerLeft + scrollerWidth, outlineTop // right
21544
			].concat(navigatorOptions.maskInside ? [
21545
				M,
21546
				navigatorLeft + zoomedMin + halfOutline, outlineTop, // upper left of zoomed range
21547
				L,
21548
				navigatorLeft + zoomedMax - halfOutline, outlineTop // upper right of z.r.
21549
			] : [])});
21550
			// draw handles
21551
			scroller.drawHandle(zoomedMin + halfOutline, 0);
21552
			scroller.drawHandle(zoomedMax + halfOutline, 1);
21553
		}
21554
 
21555
		// draw the scrollbar
21556
		if (scrollbarEnabled && scrollbarGroup) {
21557
 
21558
			// draw the buttons
21559
			scroller.drawScrollbarButton(0);
21560
			scroller.drawScrollbarButton(1);
21561
 
21562
			scrollbarGroup[verb]({
21563
				translateX: scrollerLeft,
21564
				translateY: mathRound(outlineTop + height)
21565
			});
21566
 
21567
			scrollbarTrack[verb]({
21568
				width: scrollerWidth
21569
			});
21570
 
21571
			// prevent the scrollbar from drawing to small (#1246)
21572
			scrX = scrollbarHeight + zoomedMin;
21573
			scrWidth = range - scrollbarStrokeWidth;
21574
			if (scrWidth < scrollbarMinWidth) {
21575
				scrollbarPad = (scrollbarMinWidth - scrWidth) / 2;
21576
				scrWidth = scrollbarMinWidth;
21577
				scrX -= scrollbarPad;
21578
			}
21579
			scroller.scrollbarPad = scrollbarPad;
21580
			scrollbar[verb]({
21581
				x: mathFloor(scrX) + (scrollbarStrokeWidth % 2 / 2),
21582
				width: scrWidth
21583
			});
21584
 
21585
			centerBarX = scrollbarHeight + zoomedMin + range / 2 - 0.5;
21586
 
21587
			scroller.scrollbarRifles
21588
				.attr({
21589
					visibility: range > 12 ? VISIBLE : HIDDEN
21590
				})[verb]({
21591
					d: [
21592
						M,
21593
						centerBarX - 3, scrollbarHeight / 4,
21594
						L,
21595
						centerBarX - 3, 2 * scrollbarHeight / 3,
21596
						M,
21597
						centerBarX, scrollbarHeight / 4,
21598
						L,
21599
						centerBarX, 2 * scrollbarHeight / 3,
21600
						M,
21601
						centerBarX + 3, scrollbarHeight / 4,
21602
						L,
21603
						centerBarX + 3, 2 * scrollbarHeight / 3
21604
					]
21605
				});
21606
		}
21607
 
21608
		scroller.scrollbarPad = scrollbarPad;
21609
		scroller.rendered = true;
21610
	},
21611
 
21612
	/**
21613
	 * Set up the mouse and touch events for the navigator and scrollbar
21614
	 */
21615
	addEvents: function () {
21616
		var container = this.chart.container,
21617
			mouseDownHandler = this.mouseDownHandler,
21618
			mouseMoveHandler = this.mouseMoveHandler,
21619
			mouseUpHandler = this.mouseUpHandler,
21620
			_events;
21621
 
21622
		// Mouse events
21623
		_events = [
21624
			[container, 'mousedown', mouseDownHandler],
21625
			[container, 'mousemove', mouseMoveHandler],
21626
			[document, 'mouseup', mouseUpHandler]
21627
		];
21628
 
21629
		// Touch events
21630
		if (hasTouch) {
21631
			_events.push(
21632
				[container, 'touchstart', mouseDownHandler],
21633
				[container, 'touchmove', mouseMoveHandler],
21634
				[document, 'touchend', mouseUpHandler]
21635
			);
21636
		}
21637
 
21638
		// Add them all
21639
		each(_events, function (args) {
21640
			addEvent.apply(null, args);
21641
		});
21642
		this._events = _events;
21643
	},
21644
 
21645
	/**
21646
	 * Removes the event handlers attached previously with addEvents.
21647
	 */
21648
	removeEvents: function () {
21649
 
21650
		each(this._events, function (args) {
21651
			removeEvent.apply(null, args);
21652
		});
21653
		this._events = UNDEFINED;
21654
		if (this.navigatorEnabled && this.baseSeries) {
21655
			removeEvent(this.baseSeries, 'updatedData', this.updatedDataHandler);
21656
		}
21657
	},
21658
 
21659
	/**
21660
	 * Initiate the Scroller object
21661
	 */
21662
	init: function () {
21663
		var scroller = this,
21664
			chart = scroller.chart,
21665
			xAxis,
21666
			yAxis,
21667
			scrollbarHeight = scroller.scrollbarHeight,
21668
			navigatorOptions = scroller.navigatorOptions,
21669
			height = scroller.height,
21670
			top = scroller.top,
21671
			dragOffset,
21672
			hasDragged,
21673
			baseSeries = scroller.baseSeries;
21674
 
21675
		/**
21676
		 * Event handler for the mouse down event.
21677
		 */
21678
		scroller.mouseDownHandler = function (e) {
21679
			e = chart.pointer.normalize(e);
21680
 
21681
			var zoomedMin = scroller.zoomedMin,
21682
				zoomedMax = scroller.zoomedMax,
21683
				top = scroller.top,
21684
				scrollbarHeight = scroller.scrollbarHeight,
21685
				scrollerLeft = scroller.scrollerLeft,
21686
				scrollerWidth = scroller.scrollerWidth,
21687
				navigatorLeft = scroller.navigatorLeft,
21688
				navigatorWidth = scroller.navigatorWidth,
21689
				scrollbarPad = scroller.scrollbarPad,
21690
				range = scroller.range,
21691
				chartX = e.chartX,
21692
				chartY = e.chartY,
21693
				baseXAxis = chart.xAxis[0],
21694
				fixedMax,
21695
				ext,
21696
				handleSensitivity = isTouchDevice ? 10 : 7,
21697
				left,
21698
				isOnNavigator;
21699
 
21700
			if (chartY > top && chartY < top + height + scrollbarHeight) { // we're vertically inside the navigator
21701
				isOnNavigator = !scroller.scrollbarEnabled || chartY < top + height;
21702
 
21703
				// grab the left handle
21704
				if (isOnNavigator && math.abs(chartX - zoomedMin - navigatorLeft) < handleSensitivity) {
21705
					scroller.grabbedLeft = true;
21706
					scroller.otherHandlePos = zoomedMax;
21707
					scroller.fixedExtreme = baseXAxis.max;
21708
					chart.fixedRange = null;
21709
 
21710
				// grab the right handle
21711
				} else if (isOnNavigator && math.abs(chartX - zoomedMax - navigatorLeft) < handleSensitivity) {
21712
					scroller.grabbedRight = true;
21713
					scroller.otherHandlePos = zoomedMin;
21714
					scroller.fixedExtreme = baseXAxis.min;
21715
					chart.fixedRange = null;
21716
 
21717
				// grab the zoomed range
21718
				} else if (chartX > navigatorLeft + zoomedMin - scrollbarPad && chartX < navigatorLeft + zoomedMax + scrollbarPad) {
21719
					scroller.grabbedCenter = chartX;
21720
					scroller.fixedWidth = range;
21721
 
21722
					dragOffset = chartX - zoomedMin;
21723
 
21724
 
21725
				// shift the range by clicking on shaded areas, scrollbar track or scrollbar buttons
21726
				} else if (chartX > scrollerLeft && chartX < scrollerLeft + scrollerWidth) {
21727
 
21728
					// Center around the clicked point
21729
					if (isOnNavigator) {
21730
						left = chartX - navigatorLeft - range / 2;
21731
 
21732
					// Click on scrollbar
21733
					} else {
21734
 
21735
						// Click left scrollbar button
21736
						if (chartX < navigatorLeft) {
21737
							left = zoomedMin - range * 0.2;
21738
 
21739
						// Click right scrollbar button
21740
						} else if (chartX > scrollerLeft + scrollerWidth - scrollbarHeight) {
21741
							left = zoomedMin + range * 0.2;
21742
 
21743
						// Click on scrollbar track, shift the scrollbar by one range
21744
						} else {
21745
							left = chartX < navigatorLeft + zoomedMin ? // on the left
21746
								zoomedMin - range :
21747
								zoomedMax;
21748
						}
21749
					}
21750
					if (left < 0) {
21751
						left = 0;
21752
					} else if (left + range >= navigatorWidth) {
21753
						left = navigatorWidth - range;
21754
						fixedMax = scroller.getUnionExtremes().dataMax; // #2293, #3543
21755
					}
21756
					if (left !== zoomedMin) { // it has actually moved
21757
						scroller.fixedWidth = range; // #1370
21758
 
21759
						ext = xAxis.toFixedRange(left, left + range, null, fixedMax);
21760
						baseXAxis.setExtremes(
21761
							ext.min,
21762
							ext.max,
21763
							true,
21764
							false,
21765
							{ trigger: 'navigator' }
21766
						);
21767
					}
21768
				}
21769
 
21770
			}
21771
		};
21772
 
21773
		/**
21774
		 * Event handler for the mouse move event.
21775
		 */
21776
		scroller.mouseMoveHandler = function (e) {
21777
			var scrollbarHeight = scroller.scrollbarHeight,
21778
				navigatorLeft = scroller.navigatorLeft,
21779
				navigatorWidth = scroller.navigatorWidth,
21780
				scrollerLeft = scroller.scrollerLeft,
21781
				scrollerWidth = scroller.scrollerWidth,
21782
				range = scroller.range,
21783
				chartX;
21784
 
21785
			// In iOS, a mousemove event with e.pageX === 0 is fired when holding the finger
21786
			// down in the center of the scrollbar. This should be ignored.
21787
			if (e.pageX !== 0) {
21788
 
21789
				e = chart.pointer.normalize(e);
21790
				chartX = e.chartX;
21791
 
21792
				// validation for handle dragging
21793
				if (chartX < navigatorLeft) {
21794
					chartX = navigatorLeft;
21795
				} else if (chartX > scrollerLeft + scrollerWidth - scrollbarHeight) {
21796
					chartX = scrollerLeft + scrollerWidth - scrollbarHeight;
21797
				}
21798
 
21799
				// drag left handle
21800
				if (scroller.grabbedLeft) {
21801
					hasDragged = true;
21802
					scroller.render(0, 0, chartX - navigatorLeft, scroller.otherHandlePos);
21803
 
21804
				// drag right handle
21805
				} else if (scroller.grabbedRight) {
21806
					hasDragged = true;
21807
					scroller.render(0, 0, scroller.otherHandlePos, chartX - navigatorLeft);
21808
 
21809
				// drag scrollbar or open area in navigator
21810
				} else if (scroller.grabbedCenter) {
21811
 
21812
					hasDragged = true;
21813
					if (chartX < dragOffset) { // outside left
21814
						chartX = dragOffset;
21815
					} else if (chartX > navigatorWidth + dragOffset - range) { // outside right
21816
						chartX = navigatorWidth + dragOffset - range;
21817
					}
21818
 
21819
					scroller.render(0, 0, chartX - dragOffset, chartX - dragOffset + range);
21820
 
21821
				}
21822
				if (hasDragged && scroller.scrollbarOptions.liveRedraw) {
21823
					setTimeout(function () {
21824
						scroller.mouseUpHandler(e);
21825
					}, 0);
21826
				}
21827
			}
21828
		};
21829
 
21830
		/**
21831
		 * Event handler for the mouse up event.
21832
		 */
21833
		scroller.mouseUpHandler = function (e) {
21834
			var ext,
21835
				fixedMin,
21836
				fixedMax;
21837
 
21838
			if (hasDragged) {
21839
				// When dragging one handle, make sure the other one doesn't change
21840
				if (scroller.zoomedMin === scroller.otherHandlePos) {
21841
					fixedMin = scroller.fixedExtreme;
21842
				} else if (scroller.zoomedMax === scroller.otherHandlePos) {
21843
					fixedMax = scroller.fixedExtreme;
21844
				}
21845
 
21846
				ext = xAxis.toFixedRange(scroller.zoomedMin, scroller.zoomedMax, fixedMin, fixedMax);
21847
				chart.xAxis[0].setExtremes(
21848
					ext.min,
21849
					ext.max,
21850
					true,
21851
					false,
21852
					{
21853
						trigger: 'navigator',
21854
						triggerOp: 'navigator-drag',
21855
						DOMEvent: e // #1838
21856
					}
21857
				);
21858
			}
21859
 
21860
			if (e.type !== 'mousemove') {
21861
				scroller.grabbedLeft = scroller.grabbedRight = scroller.grabbedCenter = scroller.fixedWidth =
21862
					scroller.fixedExtreme = scroller.otherHandlePos = hasDragged = dragOffset = null;
21863
			}
21864
 
21865
		};
21866
 
21867
 
21868
 
21869
		var xAxisIndex = chart.xAxis.length,
21870
			yAxisIndex = chart.yAxis.length;
21871
 
21872
		// make room below the chart
21873
		chart.extraBottomMargin = scroller.outlineHeight + navigatorOptions.margin;
21874
 
21875
		if (scroller.navigatorEnabled) {
21876
			// an x axis is required for scrollbar also
21877
			scroller.xAxis = xAxis = new Axis(chart, merge({
21878
				// inherit base xAxis' break and ordinal options
21879
				breaks: baseSeries && baseSeries.xAxis.options.breaks,
21880
				ordinal: baseSeries && baseSeries.xAxis.options.ordinal
21881
			}, navigatorOptions.xAxis, {
21882
				id: 'navigator-x-axis',
21883
				isX: true,
21884
				type: 'datetime',
21885
				index: xAxisIndex,
21886
				height: height,
21887
				offset: 0,
21888
				offsetLeft: scrollbarHeight,
21889
				offsetRight: -scrollbarHeight,
21890
				keepOrdinalPadding: true, // #2436
21891
				startOnTick: false,
21892
				endOnTick: false,
21893
				minPadding: 0,
21894
				maxPadding: 0,
21895
				zoomEnabled: false
21896
			}));
21897
 
21898
			scroller.yAxis = yAxis = new Axis(chart, merge(navigatorOptions.yAxis, {
21899
				id: 'navigator-y-axis',
21900
				alignTicks: false,
21901
				height: height,
21902
				offset: 0,
21903
				index: yAxisIndex,
21904
				zoomEnabled: false
21905
			}));
21906
 
21907
			// If we have a base series, initialize the navigator series
21908
			if (baseSeries || navigatorOptions.series.data) {
21909
				scroller.addBaseSeries();
21910
 
21911
			// If not, set up an event to listen for added series
21912
			} else if (chart.series.length === 0) {
21913
 
21914
				wrap(chart, 'redraw', function (proceed, animation) {
21915
					// We've got one, now add it as base and reset chart.redraw
21916
					if (chart.series.length > 0 && !scroller.series) {
21917
						scroller.setBaseSeries();
21918
						chart.redraw = proceed; // reset
21919
					}
21920
					proceed.call(chart, animation);
21921
				});
21922
			}
21923
 
21924
 
21925
		// in case of scrollbar only, fake an x axis to get translation
21926
		} else {
21927
			scroller.xAxis = xAxis = {
21928
				translate: function (value, reverse) {
21929
					var axis = chart.xAxis[0],
21930
						ext = axis.getExtremes(),
21931
						scrollTrackWidth = chart.plotWidth - 2 * scrollbarHeight,
21932
						min = numExt('min', axis.options.min, ext.dataMin),
21933
						valueRange = numExt('max', axis.options.max, ext.dataMax) - min;
21934
 
21935
					return reverse ?
21936
						// from pixel to value
21937
						(value * valueRange / scrollTrackWidth) + min :
21938
						// from value to pixel
21939
						scrollTrackWidth * (value - min) / valueRange;
21940
				},
21941
				toFixedRange: Axis.prototype.toFixedRange
21942
			};
21943
		}
21944
 
21945
 
21946
		/**
21947
		 * For stock charts, extend the Chart.getMargins method so that we can set the final top position
21948
		 * of the navigator once the height of the chart, including the legend, is determined. #367.
21949
		 */
21950
		wrap(chart, 'getMargins', function (proceed) {
21951
 
21952
			var legend = this.legend,
21953
				legendOptions = legend.options;
21954
 
21955
			proceed.apply(this, [].slice.call(arguments, 1));
21956
 
21957
			// Compute the top position
21958
			scroller.top = top = scroller.navigatorOptions.top ||
21959
				this.chartHeight - scroller.height - scroller.scrollbarHeight - this.spacing[2] -
21960
						(legendOptions.verticalAlign === 'bottom' && legendOptions.enabled && !legendOptions.floating ?
21961
							legend.legendHeight + pick(legendOptions.margin, 10) : 0);
21962
 
21963
			if (xAxis && yAxis) { // false if navigator is disabled (#904)
21964
 
21965
				xAxis.options.top = yAxis.options.top = top;
21966
 
21967
				xAxis.setAxisSize();
21968
				yAxis.setAxisSize();
21969
			}
21970
		});
21971
 
21972
 
21973
		scroller.addEvents();
21974
	},
21975
 
21976
	/**
21977
	 * Get the union data extremes of the chart - the outer data extremes of the base
21978
	 * X axis and the navigator axis.
21979
	 */
21980
	getUnionExtremes: function (returnFalseOnNoBaseSeries) {
21981
		var baseAxis = this.chart.xAxis[0],
21982
			navAxis = this.xAxis,
21983
			navAxisOptions = navAxis.options,
21984
			baseAxisOptions = baseAxis.options,
21985
			ret;
21986
 
21987
		if (!returnFalseOnNoBaseSeries || baseAxis.dataMin !== null) {
21988
			ret = {
21989
				dataMin: pick( // #4053
21990
					navAxisOptions && navAxisOptions.min,
21991
					numExt(
21992
						'min',
21993
						baseAxisOptions.min,
21994
						baseAxis.dataMin,
21995
						navAxis.dataMin
21996
					)
21997
				),
21998
				dataMax: pick(
21999
					navAxisOptions && navAxisOptions.max,
22000
					numExt(
22001
						'max',
22002
						baseAxisOptions.max,
22003
						baseAxis.dataMax,
22004
						navAxis.dataMax
22005
					)
22006
				)
22007
			};
22008
		}
22009
		return ret;
22010
	},
22011
 
22012
	/**
22013
	 * Set the base series. With a bit of modification we should be able to make
22014
	 * this an API method to be called from the outside
22015
	 */
22016
	setBaseSeries: function (baseSeriesOption) {
22017
		var chart = this.chart;
22018
 
22019
		baseSeriesOption = baseSeriesOption || chart.options.navigator.baseSeries;
22020
 
22021
		// If we're resetting, remove the existing series
22022
		if (this.series) {
22023
			this.series.remove();
22024
		}
22025
 
22026
		// Set the new base series
22027
		this.baseSeries = chart.series[baseSeriesOption] ||
22028
			(typeof baseSeriesOption === 'string' && chart.get(baseSeriesOption)) ||
22029
			chart.series[0];
22030
 
22031
		// When run after render, this.xAxis already exists
22032
		if (this.xAxis) {
22033
			this.addBaseSeries();
22034
		}
22035
	},
22036
 
22037
	addBaseSeries: function () {
22038
		var baseSeries = this.baseSeries,
22039
			baseOptions = baseSeries ? baseSeries.options : {},
22040
			baseData = baseOptions.data,
22041
			mergedNavSeriesOptions,
22042
			navigatorSeriesOptions = this.navigatorOptions.series,
22043
			navigatorData;
22044
 
22045
		// remove it to prevent merging one by one
22046
		navigatorData = navigatorSeriesOptions.data;
22047
		this.hasNavigatorData = !!navigatorData;
22048
 
22049
		// Merge the series options
22050
		mergedNavSeriesOptions = merge(baseOptions, navigatorSeriesOptions, {
22051
			enableMouseTracking: false,
22052
			group: 'nav', // for columns
22053
			padXAxis: false,
22054
			xAxis: 'navigator-x-axis',
22055
			yAxis: 'navigator-y-axis',
22056
			name: 'Navigator',
22057
			showInLegend: false,
22058
			isInternal: true,
22059
			visible: true
22060
		});
22061
 
22062
		// set the data back
22063
		mergedNavSeriesOptions.data = navigatorData || baseData;
22064
 
22065
		// add the series
22066
		this.series = this.chart.initSeries(mergedNavSeriesOptions);
22067
 
22068
		// Respond to updated data in the base series.
22069
		// Abort if lazy-loading data from the server.
22070
		if (baseSeries && this.navigatorOptions.adaptToUpdatedData !== false) {
22071
			addEvent(baseSeries, 'updatedData', this.updatedDataHandler);
22072
			// Survive Series.update()
22073
			baseSeries.userOptions.events = extend(baseSeries.userOptions.event, { updatedData: this.updatedDataHandler });
22074
 
22075
		}
22076
	},
22077
 
22078
	updatedDataHandler: function () {
22079
		var scroller = this.chart.scroller,
22080
			baseSeries = scroller.baseSeries,
22081
			baseXAxis = baseSeries.xAxis,
22082
			baseExtremes = baseXAxis.getExtremes(),
22083
			baseMin = baseExtremes.min,
22084
			baseMax = baseExtremes.max,
22085
			baseDataMin = baseExtremes.dataMin,
22086
			baseDataMax = baseExtremes.dataMax,
22087
			range = baseMax - baseMin,
22088
			stickToMin,
22089
			stickToMax,
22090
			newMax,
22091
			newMin,
22092
			doRedraw,
22093
			navigatorSeries = scroller.series,
22094
			navXData = navigatorSeries.xData,
22095
			hasSetExtremes = !!baseXAxis.setExtremes;
22096
 
22097
		// detect whether to move the range
22098
		stickToMax = baseMax >= navXData[navXData.length - 1] - (this.closestPointRange || 0); // #570
22099
		stickToMin = baseMin <= baseDataMin;
22100
 
22101
		// set the navigator series data to the new data of the base series
22102
		if (!scroller.hasNavigatorData) {
22103
			navigatorSeries.options.pointStart = baseSeries.xData[0];
22104
			navigatorSeries.setData(baseSeries.options.data, false);
22105
			doRedraw = true;
22106
		}
22107
 
22108
		// if the zoomed range is already at the min, move it to the right as new data
22109
		// comes in
22110
		if (stickToMin) {
22111
			newMin = baseDataMin;
22112
			newMax = newMin + range;
22113
		}
22114
 
22115
		// if the zoomed range is already at the max, move it to the right as new data
22116
		// comes in
22117
		if (stickToMax) {
22118
			newMax = baseDataMax;
22119
			if (!stickToMin) { // if stickToMin is true, the new min value is set above
22120
				newMin = mathMax(newMax - range, navigatorSeries.xData[0]);
22121
			}
22122
		}
22123
 
22124
		// update the extremes
22125
		if (hasSetExtremes && (stickToMin || stickToMax)) {
22126
			if (!isNaN(newMin)) {
22127
				baseXAxis.setExtremes(newMin, newMax, true, false, { trigger: 'updatedData' });
22128
			}
22129
 
22130
		// if it is not at any edge, just move the scroller window to reflect the new series data
22131
		} else {
22132
			if (doRedraw) {
22133
				this.chart.redraw(false);
22134
			}
22135
 
22136
			scroller.render(
22137
				mathMax(baseMin, baseDataMin),
22138
				mathMin(baseMax, baseDataMax)
22139
			);
22140
		}
22141
	},
22142
 
22143
	/**
22144
	 * Destroys allocated elements.
22145
	 */
22146
	destroy: function () {
22147
		var scroller = this;
22148
 
22149
		// Disconnect events added in addEvents
22150
		scroller.removeEvents();
22151
 
22152
		// Destroy properties
22153
		each([scroller.xAxis, scroller.yAxis, scroller.leftShade, scroller.rightShade, scroller.outline, scroller.scrollbarTrack, scroller.scrollbarRifles, scroller.scrollbarGroup, scroller.scrollbar], function (prop) {
22154
			if (prop && prop.destroy) {
22155
				prop.destroy();
22156
			}
22157
		});
22158
		scroller.xAxis = scroller.yAxis = scroller.leftShade = scroller.rightShade = scroller.outline = scroller.scrollbarTrack = scroller.scrollbarRifles = scroller.scrollbarGroup = scroller.scrollbar = null;
22159
 
22160
		// Destroy elements in collection
22161
		each([scroller.scrollbarButtons, scroller.handles, scroller.elementsToDestroy], function (coll) {
22162
			destroyObjectProperties(coll);
22163
		});
22164
	}
22165
};
22166
 
22167
Highcharts.Scroller = Scroller;
22168
 
22169
 
22170
/**
22171
 * For Stock charts, override selection zooming with some special features because
22172
 * X axis zooming is already allowed by the Navigator and Range selector.
22173
 */
22174
wrap(Axis.prototype, 'zoom', function (proceed, newMin, newMax) {
22175
	var chart = this.chart,
22176
		chartOptions = chart.options,
22177
		zoomType = chartOptions.chart.zoomType,
22178
		previousZoom,
22179
		navigator = chartOptions.navigator,
22180
		rangeSelector = chartOptions.rangeSelector,
22181
		ret;
22182
 
22183
	if (this.isXAxis && ((navigator && navigator.enabled) ||
22184
			(rangeSelector && rangeSelector.enabled))) {
22185
 
22186
		// For x only zooming, fool the chart.zoom method not to create the zoom button
22187
		// because the property already exists
22188
		if (zoomType === 'x') {
22189
			chart.resetZoomButton = 'blocked';
22190
 
22191
		// For y only zooming, ignore the X axis completely
22192
		} else if (zoomType === 'y') {
22193
			ret = false;
22194
 
22195
		// For xy zooming, record the state of the zoom before zoom selection, then when
22196
		// the reset button is pressed, revert to this state
22197
		} else if (zoomType === 'xy') {
22198
			previousZoom = this.previousZoom;
22199
			if (defined(newMin)) {
22200
				this.previousZoom = [this.min, this.max];
22201
			} else if (previousZoom) {
22202
				newMin = previousZoom[0];
22203
				newMax = previousZoom[1];
22204
				delete this.previousZoom;
22205
			}
22206
		}
22207
 
22208
	}
22209
	return ret !== UNDEFINED ? ret : proceed.call(this, newMin, newMax);
22210
});
22211
 
22212
// Initialize scroller for stock charts
22213
wrap(Chart.prototype, 'init', function (proceed, options, callback) {
22214
 
22215
	addEvent(this, 'beforeRender', function () {
22216
		var options = this.options;
22217
		if (options.navigator.enabled || options.scrollbar.enabled) {
22218
			this.scroller = new Scroller(this);
22219
		}
22220
	});
22221
 
22222
	proceed.call(this, options, callback);
22223
 
22224
});
22225
 
22226
// Pick up badly formatted point options to addPoint
22227
wrap(Series.prototype, 'addPoint', function (proceed, options, redraw, shift, animation) {
22228
	var turboThreshold = this.options.turboThreshold;
22229
	if (turboThreshold && this.xData.length > turboThreshold && isObject(options) && !isArray(options) && this.chart.scroller) {
22230
		error(20, true);
22231
	}
22232
	proceed.call(this, options, redraw, shift, animation);
22233
});
22234
 
22235
/* ****************************************************************************
22236
 * End Scroller code														  *
22237
 *****************************************************************************/
22238
/* ****************************************************************************
22239
 * Start Range Selector code												  *
22240
 *****************************************************************************/
22241
extend(defaultOptions, {
22242
	rangeSelector: {
22243
		// allButtonsEnabled: false,
22244
		// enabled: true,
22245
		// buttons: {Object}
22246
		// buttonSpacing: 0,
22247
		buttonTheme: {
22248
			width: 28,
22249
			height: 18,
22250
			fill: '#f7f7f7',
22251
			padding: 2,
22252
			r: 0,
22253
			'stroke-width': 0,
22254
			style: {
22255
				color: '#444',
22256
				cursor: 'pointer',
22257
				fontWeight: 'normal'
22258
			},
22259
			zIndex: 7, // #484, #852
22260
			states: {
22261
				hover: {
22262
					fill: '#e7e7e7'
22263
				},
22264
				select: {
22265
					fill: '#e7f0f9',
22266
					style: {
22267
						color: 'black',
22268
						fontWeight: 'bold'
22269
					}
22270
				}
22271
			}
22272
		},
22273
		height: 35, // reserved space for buttons and input
22274
		inputPosition: {
22275
			align: 'right'
22276
		},
22277
		// inputDateFormat: '%b %e, %Y',
22278
		// inputEditDateFormat: '%Y-%m-%d',
22279
		// inputEnabled: true,
22280
		// inputStyle: {},
22281
		labelStyle: {
22282
			color: '#666'
22283
		}
22284
		// selected: undefined
22285
	}
22286
});
22287
defaultOptions.lang = merge(defaultOptions.lang, {
22288
	rangeSelectorZoom: 'Zoom',
22289
	rangeSelectorFrom: 'From',
22290
	rangeSelectorTo: 'To'
22291
});
22292
 
22293
/**
22294
 * The object constructor for the range selector
22295
 * @param {Object} chart
22296
 */
22297
function RangeSelector(chart) {
22298
 
22299
	// Run RangeSelector
22300
	this.init(chart);
22301
}
22302
 
22303
RangeSelector.prototype = {
22304
	/**
22305
	 * The method to run when one of the buttons in the range selectors is clicked
22306
	 * @param {Number} i The index of the button
22307
	 * @param {Object} rangeOptions
22308
	 * @param {Boolean} redraw
22309
	 */
22310
	clickButton: function (i, redraw) {
22311
		var rangeSelector = this,
22312
			selected = rangeSelector.selected,
22313
			chart = rangeSelector.chart,
22314
			buttons = rangeSelector.buttons,
22315
			rangeOptions = rangeSelector.buttonOptions[i],
22316
			baseAxis = chart.xAxis[0],
22317
			unionExtremes = (chart.scroller && chart.scroller.getUnionExtremes()) || baseAxis || {},
22318
			dataMin = unionExtremes.dataMin,
22319
			dataMax = unionExtremes.dataMax,
22320
			newMin,
22321
			newMax = baseAxis && mathRound(mathMin(baseAxis.max, pick(dataMax, baseAxis.max))), // #1568
22322
			now,
22323
			type = rangeOptions.type,
22324
			baseXAxisOptions,
22325
			range = rangeOptions._range,
22326
			rangeMin,
22327
			year,
22328
			minSetting,
22329
			rangeSetting,
22330
			ctx,
22331
			dataGrouping = rangeOptions.dataGrouping;
22332
 
22333
		if (dataMin === null || dataMax === null || // chart has no data, base series is removed
22334
				i === rangeSelector.selected) { // same button is clicked twice
22335
			return;
22336
		}
22337
 
22338
		// Set the fixed range before range is altered
22339
		chart.fixedRange = range;
22340
 
22341
		// Apply dataGrouping associated to button
22342
		if (dataGrouping) {
22343
			this.forcedDataGrouping = true;
22344
			Axis.prototype.setDataGrouping.call(baseAxis || { chart: this.chart }, dataGrouping, false);
22345
		}
22346
 
22347
		// Apply range
22348
		if (type === 'month' || type === 'year') {
22349
			if (!baseAxis) {
22350
				// This is set to the user options and picked up later when the axis is instantiated
22351
				// so that we know the min and max.
22352
				range = rangeOptions;
22353
			} else {
22354
				ctx = {
22355
					range: rangeOptions,
22356
					max: newMax,
22357
					dataMin: dataMin,
22358
					dataMax: dataMax
22359
				};
22360
				newMin = baseAxis.minFromRange.call(ctx);
22361
				if (typeof ctx.newMax === 'number') {
22362
					newMax = ctx.newMax;
22363
				}
22364
			}
22365
 
22366
		// Fixed times like minutes, hours, days
22367
		} else if (range) {
22368
			newMin = mathMax(newMax - range, dataMin);
22369
			newMax = mathMin(newMin + range, dataMax);
22370
 
22371
		} else if (type === 'ytd') {
22372
 
22373
			// On user clicks on the buttons, or a delayed action running from the beforeRender
22374
			// event (below), the baseAxis is defined.
22375
			if (baseAxis) {
22376
 
22377
				// When "ytd" is the pre-selected button for the initial view, its calculation
22378
				// is delayed and rerun in the beforeRender event (below). When the series
22379
				// are initialized, but before the chart is rendered, we have access to the xData
22380
				// array (#942).
22381
				if (dataMax === UNDEFINED) {
22382
					dataMin = Number.MAX_VALUE;
22383
					dataMax = Number.MIN_VALUE;
22384
					each(chart.series, function (series) {
22385
						var xData = series.xData; // reassign it to the last item
22386
						dataMin = mathMin(xData[0], dataMin);
22387
						dataMax = mathMax(xData[xData.length - 1], dataMax);
22388
					});
22389
					redraw = false;
22390
				}
22391
				now = new Date(dataMax);
22392
				year = now.getFullYear();
22393
				newMin = rangeMin = mathMax(dataMin || 0, Date.UTC(year, 0, 1));
22394
				now = now.getTime();
22395
				newMax = mathMin(dataMax || now, now);
22396
 
22397
			// "ytd" is pre-selected. We don't yet have access to processed point and extremes data
22398
			// (things like pointStart and pointInterval are missing), so we delay the process (#942)
22399
			} else {
22400
				addEvent(chart, 'beforeRender', function () {
22401
					rangeSelector.clickButton(i);
22402
				});
22403
				return;
22404
			}
22405
		} else if (type === 'all' && baseAxis) {
22406
			newMin = dataMin;
22407
			newMax = dataMax;
22408
		}
22409
 
22410
		// Deselect previous button
22411
		if (buttons[selected]) {
22412
			buttons[selected].setState(0);
22413
		}
22414
		// Select this button
22415
		if (buttons[i]) {
22416
			buttons[i].setState(2);
22417
		}
22418
 
22419
		// Update the chart
22420
		if (!baseAxis) {
22421
			// Axis not yet instanciated. Temporarily set min and range
22422
			// options and remove them on chart load (#4317).
22423
			baseXAxisOptions = chart.options.xAxis[0];
22424
			rangeSetting = baseXAxisOptions.range;
22425
			baseXAxisOptions.range = range;
22426
			minSetting = baseXAxisOptions.min;
22427
			baseXAxisOptions.min = rangeMin;
22428
			rangeSelector.setSelected(i);
22429
			addEvent(chart, 'load', function resetMinAndRange() {
22430
				baseXAxisOptions.range = rangeSetting;
22431
				baseXAxisOptions.min = minSetting;
22432
			});
22433
		} else {
22434
			// Existing axis object. Set extremes after render time.
22435
			baseAxis.setExtremes(
22436
				newMin,
22437
				newMax,
22438
				pick(redraw, 1),
22439
				0,
22440
				{
22441
					trigger: 'rangeSelectorButton',
22442
					rangeSelectorButton: rangeOptions
22443
				}
22444
			);
22445
			rangeSelector.setSelected(i);
22446
		}
22447
	},
22448
 
22449
	/**
22450
	 * Set the selected option. This method only sets the internal flag, it doesn't
22451
	 * update the buttons or the actual zoomed range.
22452
	 */
22453
	setSelected: function (selected) {
22454
		this.selected = this.options.selected = selected;
22455
	},
22456
 
22457
	/**
22458
	 * The default buttons for pre-selecting time frames
22459
	 */
22460
	defaultButtons: [{
22461
		type: 'month',
22462
		count: 1,
22463
		text: '1m'
22464
	}, {
22465
		type: 'month',
22466
		count: 3,
22467
		text: '3m'
22468
	}, {
22469
		type: 'month',
22470
		count: 6,
22471
		text: '6m'
22472
	}, {
22473
		type: 'ytd',
22474
		text: 'YTD'
22475
	}, {
22476
		type: 'year',
22477
		count: 1,
22478
		text: '1y'
22479
	}, {
22480
		type: 'all',
22481
		text: 'All'
22482
	}],
22483
 
22484
	/**
22485
	 * Initialize the range selector
22486
	 */
22487
	init: function (chart) {
22488
 
22489
		var rangeSelector = this,
22490
			options = chart.options.rangeSelector,
22491
			buttonOptions = options.buttons || [].concat(rangeSelector.defaultButtons),
22492
			selectedOption = options.selected,
22493
			blurInputs = rangeSelector.blurInputs = function () {
22494
				var minInput = rangeSelector.minInput,
22495
					maxInput = rangeSelector.maxInput;
22496
				if (minInput && minInput.blur) { //#3274 in some case blur is not defined
22497
					fireEvent(minInput, 'blur'); //#3274
22498
				}
22499
				if (maxInput && maxInput.blur) { //#3274 in some case blur is not defined
22500
					fireEvent(maxInput, 'blur'); //#3274
22501
				}
22502
			};
22503
 
22504
		rangeSelector.chart = chart;
22505
		rangeSelector.options = options;
22506
		rangeSelector.buttons = [];
22507
 
22508
		chart.extraTopMargin = options.height;
22509
		rangeSelector.buttonOptions = buttonOptions;
22510
 
22511
		addEvent(chart.container, 'mousedown', blurInputs);
22512
		addEvent(chart, 'resize', blurInputs);
22513
 
22514
		// Extend the buttonOptions with actual range
22515
		each(buttonOptions, rangeSelector.computeButtonRange);
22516
 
22517
		// zoomed range based on a pre-selected button index
22518
		if (selectedOption !== UNDEFINED && buttonOptions[selectedOption]) {
22519
			this.clickButton(selectedOption, false);
22520
		}
22521
 
22522
 
22523
		addEvent(chart, 'load', function () {
22524
			// If a data grouping is applied to the current button, release it when extremes change
22525
			addEvent(chart.xAxis[0], 'setExtremes', function (e) {
22526
				if (this.max - this.min !== chart.fixedRange && e.trigger !== 'rangeSelectorButton' &&
22527
						e.trigger !== 'updatedData' && rangeSelector.forcedDataGrouping) {
22528
					this.setDataGrouping(false, false);
22529
				}
22530
			});
22531
			// Normalize the pressed button whenever a new range is selected
22532
			addEvent(chart.xAxis[0], 'afterSetExtremes', function () {
22533
				rangeSelector.updateButtonStates(true);
22534
			});
22535
		});
22536
	},
22537
 
22538
	/**
22539
	 * Dynamically update the range selector buttons after a new range has been set
22540
	 */
22541
	updateButtonStates: function (updating) {
22542
		var rangeSelector = this,
22543
			chart = this.chart,
22544
			baseAxis = chart.xAxis[0],
22545
			unionExtremes = (chart.scroller && chart.scroller.getUnionExtremes()) || baseAxis,
22546
			dataMin = unionExtremes.dataMin,
22547
			dataMax = unionExtremes.dataMax,
22548
			selected = rangeSelector.selected,
22549
			allButtonsEnabled = rangeSelector.options.allButtonsEnabled,
22550
			buttons = rangeSelector.buttons;
22551
 
22552
		if (updating && chart.fixedRange !== mathRound(baseAxis.max - baseAxis.min)) {
22553
			if (buttons[selected]) {
22554
				buttons[selected].setState(0);
22555
			}
22556
			rangeSelector.setSelected(null);
22557
		}
22558
 
22559
		each(rangeSelector.buttonOptions, function (rangeOptions, i) {
22560
			var actualRange = mathRound(baseAxis.max - baseAxis.min),
22561
				range = rangeOptions._range,
22562
				type = rangeOptions.type,
22563
				count = rangeOptions.count || 1,
22564
				// Disable buttons where the range exceeds what is allowed in the current view
22565
				isTooGreatRange = range > dataMax - dataMin,
22566
				// Disable buttons where the range is smaller than the minimum range
22567
				isTooSmallRange = range < baseAxis.minRange,
22568
				// Disable the All button if we're already showing all
22569
				isAllButAlreadyShowingAll = rangeOptions.type === 'all' && baseAxis.max - baseAxis.min >= dataMax - dataMin &&
22570
					buttons[i].state !== 2,
22571
				// Disable the YTD button if the complete range is within the same year
22572
				isYTDButNotAvailable = rangeOptions.type === 'ytd' && dateFormat('%Y', dataMin) === dateFormat('%Y', dataMax),
22573
				// Set a button on export
22574
				isSelectedForExport = chart.renderer.forExport && i === selected,
22575
 
22576
				isSameRange = range === actualRange;
22577
 
22578
			// Months and years have a variable range so we check the extremes
22579
			if ((type === 'month' || type === 'year') && (actualRange >= { month: 28, year: 365 }[type] * 24 * 36e5 * count) &&
22580
					(actualRange <= { month: 31, year: 366 }[type] * 24 * 36e5 * count)) {
22581
				isSameRange = true;
22582
			}
22583
			// The new zoom area happens to match the range for a button - mark it selected.
22584
			// This happens when scrolling across an ordinal gap. It can be seen in the intraday
22585
			// demos when selecting 1h and scroll across the night gap.
22586
			if (isSelectedForExport || (isSameRange && i !== selected)) {
22587
				rangeSelector.setSelected(i);
22588
				buttons[i].setState(2);
22589
 
22590
			} else if (!allButtonsEnabled && (isTooGreatRange || isTooSmallRange || isAllButAlreadyShowingAll || isYTDButNotAvailable)) {
22591
				buttons[i].setState(3);
22592
 
22593
			} else if (buttons[i].state === 3) {
22594
				buttons[i].setState(0);
22595
			}
22596
		});
22597
	},
22598
 
22599
	/**
22600
	 * Compute and cache the range for an individual button
22601
	 */
22602
	computeButtonRange: function (rangeOptions) {
22603
		var type = rangeOptions.type,
22604
			count = rangeOptions.count || 1,
22605
 
22606
			// these time intervals have a fixed number of milliseconds, as opposed
22607
			// to month, ytd and year
22608
			fixedTimes = {
22609
				millisecond: 1,
22610
				second: 1000,
22611
				minute: 60 * 1000,
22612
				hour: 3600 * 1000,
22613
				day: 24 * 3600 * 1000,
22614
				week: 7 * 24 * 3600 * 1000
22615
			};
22616
 
22617
		// Store the range on the button object
22618
		if (fixedTimes[type]) {
22619
			rangeOptions._range = fixedTimes[type] * count;
22620
		} else if (type === 'month' || type === 'year') {
22621
			rangeOptions._range = { month: 30, year: 365 }[type] * 24 * 36e5 * count;
22622
		}
22623
	},
22624
 
22625
	/**
22626
	 * Set the internal and displayed value of a HTML input for the dates
22627
	 * @param {String} name
22628
	 * @param {Number} time
22629
	 */
22630
	setInputValue: function (name, time) {
22631
		var options = this.chart.options.rangeSelector;
22632
 
22633
		if (defined(time)) {
22634
			this[name + 'Input'].HCTime = time;
22635
		}
22636
 
22637
		this[name + 'Input'].value = dateFormat(options.inputEditDateFormat || '%Y-%m-%d', this[name + 'Input'].HCTime);
22638
		this[name + 'DateBox'].attr({ text: dateFormat(options.inputDateFormat || '%b %e, %Y', this[name + 'Input'].HCTime) });
22639
	},
22640
 
22641
	showInput: function (name) {
22642
		var inputGroup = this.inputGroup,
22643
			dateBox = this[name + 'DateBox'];
22644
 
22645
		css(this[name + 'Input'], {
22646
			left: (inputGroup.translateX + dateBox.x) + PX,
22647
			top: inputGroup.translateY + PX,
22648
			width: (dateBox.width - 2) + PX,
22649
			height: (dateBox.height - 2) + PX,
22650
			border: '2px solid silver'
22651
		});
22652
	},
22653
 
22654
	hideInput: function (name) {
22655
		css(this[name + 'Input'], {
22656
			border: 0,
22657
			width: '1px',
22658
			height: '1px'
22659
		});
22660
		this.setInputValue(name);
22661
	},
22662
 
22663
	/**
22664
	 * Draw either the 'from' or the 'to' HTML input box of the range selector
22665
	 * @param {Object} name
22666
	 */
22667
	drawInput: function (name) {
22668
		var rangeSelector = this,
22669
			chart = rangeSelector.chart,
22670
			chartStyle = chart.renderer.style,
22671
			renderer = chart.renderer,
22672
			options = chart.options.rangeSelector,
22673
			lang = defaultOptions.lang,
22674
			div = rangeSelector.div,
22675
			isMin = name === 'min',
22676
			input,
22677
			label,
22678
			dateBox,
22679
			inputGroup = this.inputGroup;
22680
 
22681
		// Create the text label
22682
		this[name + 'Label'] = label = renderer.label(lang[isMin ? 'rangeSelectorFrom' : 'rangeSelectorTo'], this.inputGroup.offset)
22683
			.attr({
22684
				padding: 2
22685
			})
22686
			.css(merge(chartStyle, options.labelStyle))
22687
			.add(inputGroup);
22688
		inputGroup.offset += label.width + 5;
22689
 
22690
		// Create an SVG label that shows updated date ranges and and records click events that
22691
		// bring in the HTML input.
22692
		this[name + 'DateBox'] = dateBox = renderer.label('', inputGroup.offset)
22693
			.attr({
22694
				padding: 2,
22695
				width: options.inputBoxWidth || 90,
22696
				height: options.inputBoxHeight || 17,
22697
				stroke: options.inputBoxBorderColor || 'silver',
22698
				'stroke-width': 1
22699
			})
22700
			.css(merge({
22701
				textAlign: 'center',
22702
				color: '#444'
22703
			}, chartStyle, options.inputStyle))
22704
			.on('click', function () {
22705
				rangeSelector.showInput(name); // If it is already focused, the onfocus event doesn't fire (#3713)
22706
				rangeSelector[name + 'Input'].focus();
22707
			})
22708
			.add(inputGroup);
22709
		inputGroup.offset += dateBox.width + (isMin ? 10 : 0);
22710
 
22711
 
22712
		// Create the HTML input element. This is rendered as 1x1 pixel then set to the right size
22713
		// when focused.
22714
		this[name + 'Input'] = input = createElement('input', {
22715
			name: name,
22716
			className: PREFIX + 'range-selector',
22717
			type: 'text'
22718
		}, extend({
22719
			position: ABSOLUTE,
22720
			border: 0,
22721
			width: '1px', // Chrome needs a pixel to see it
22722
			height: '1px',
22723
			padding: 0,
22724
			textAlign: 'center',
22725
			fontSize: chartStyle.fontSize,
22726
			fontFamily: chartStyle.fontFamily,
22727
			top: chart.plotTop + PX // prevent jump on focus in Firefox
22728
		}, options.inputStyle), div);
22729
 
22730
		// Blow up the input box
22731
		input.onfocus = function () {
22732
			rangeSelector.showInput(name);
22733
		};
22734
		// Hide away the input box
22735
		input.onblur = function () {
22736
			rangeSelector.hideInput(name);
22737
		};
22738
 
22739
		// handle changes in the input boxes
22740
		input.onchange = function () {
22741
			var inputValue = input.value,
22742
				value = (options.inputDateParser || Date.parse)(inputValue),
22743
				xAxis = chart.xAxis[0],
22744
				dataMin = xAxis.dataMin,
22745
				dataMax = xAxis.dataMax;
22746
 
22747
			// If the value isn't parsed directly to a value by the browser's Date.parse method,
22748
			// like YYYY-MM-DD in IE, try parsing it a different way
22749
			if (isNaN(value)) {
22750
				value = inputValue.split('-');
22751
				value = Date.UTC(pInt(value[0]), pInt(value[1]) - 1, pInt(value[2]));
22752
			}
22753
 
22754
			if (!isNaN(value)) {
22755
 
22756
				// Correct for timezone offset (#433)
22757
				if (!defaultOptions.global.useUTC) {
22758
					value = value + new Date().getTimezoneOffset() * 60 * 1000;
22759
				}
22760
 
22761
				// Validate the extremes. If it goes beyound the data min or max, use the
22762
				// actual data extreme (#2438).
22763
				if (isMin) {
22764
					if (value > rangeSelector.maxInput.HCTime) {
22765
						value = UNDEFINED;
22766
					} else if (value < dataMin) {
22767
						value = dataMin;
22768
					}
22769
				} else {
22770
					if (value < rangeSelector.minInput.HCTime) {
22771
						value = UNDEFINED;
22772
					} else if (value > dataMax) {
22773
						value = dataMax;
22774
					}
22775
				}
22776
 
22777
				// Set the extremes
22778
				if (value !== UNDEFINED) {
22779
					chart.xAxis[0].setExtremes(
22780
						isMin ? value : xAxis.min,
22781
						isMin ? xAxis.max : value,
22782
						UNDEFINED,
22783
						UNDEFINED,
22784
						{ trigger: 'rangeSelectorInput' }
22785
					);
22786
				}
22787
			}
22788
		};
22789
	},
22790
 
22791
	/**
22792
	 * Get the position of the range selector buttons and inputs. This can be overridden from outside for custom positioning.
22793
	 */
22794
	getPosition: function () {
22795
		var chart = this.chart,
22796
			options = chart.options.rangeSelector,
22797
			buttonTop = pick((options.buttonPosition || {}).y, chart.plotTop - chart.axisOffset[0] - options.height);
22798
 
22799
		return {
22800
			buttonTop: buttonTop,
22801
			inputTop: buttonTop - 10
22802
		};
22803
	},
22804
 
22805
	/**
22806
	 * Render the range selector including the buttons and the inputs. The first time render
22807
	 * is called, the elements are created and positioned. On subsequent calls, they are
22808
	 * moved and updated.
22809
	 * @param {Number} min X axis minimum
22810
	 * @param {Number} max X axis maximum
22811
	 */
22812
	render: function (min, max) {
22813
 
22814
		var rangeSelector = this,
22815
			chart = rangeSelector.chart,
22816
			renderer = chart.renderer,
22817
			container = chart.container,
22818
			chartOptions = chart.options,
22819
			navButtonOptions = chartOptions.exporting && chartOptions.navigation && chartOptions.navigation.buttonOptions,
22820
			options = chartOptions.rangeSelector,
22821
			buttons = rangeSelector.buttons,
22822
			lang = defaultOptions.lang,
22823
			div = rangeSelector.div,
22824
			inputGroup = rangeSelector.inputGroup,
22825
			buttonTheme = options.buttonTheme,
22826
			buttonPosition = options.buttonPosition || {},
22827
			inputEnabled = options.inputEnabled,
22828
			states = buttonTheme && buttonTheme.states,
22829
			plotLeft = chart.plotLeft,
22830
			buttonLeft,
22831
			pos = this.getPosition(),
22832
			buttonGroup = rangeSelector.group,
22833
			buttonBBox,
22834
			rendered = rangeSelector.rendered;
22835
 
22836
 
22837
		// create the elements
22838
		if (!rendered) {
22839
 
22840
			rangeSelector.group = buttonGroup = renderer.g('range-selector-buttons').add();
22841
 
22842
			rangeSelector.zoomText = renderer.text(lang.rangeSelectorZoom, pick(buttonPosition.x, plotLeft), 15)
22843
				.css(options.labelStyle)
22844
				.add(buttonGroup);
22845
 
22846
			// button starting position
22847
			buttonLeft = pick(buttonPosition.x, plotLeft) + rangeSelector.zoomText.getBBox().width + 5;
22848
 
22849
			each(rangeSelector.buttonOptions, function (rangeOptions, i) {
22850
				buttons[i] = renderer.button(
22851
						rangeOptions.text,
22852
						buttonLeft,
22853
						0,
22854
						function () {
22855
							rangeSelector.clickButton(i);
22856
							rangeSelector.isActive = true;
22857
						},
22858
						buttonTheme,
22859
						states && states.hover,
22860
						states && states.select,
22861
						states && states.disabled
22862
					)
22863
					.css({
22864
						textAlign: 'center'
22865
					})
22866
					.add(buttonGroup);
22867
 
22868
				// increase button position for the next button
22869
				buttonLeft += buttons[i].width + pick(options.buttonSpacing, 5);
22870
 
22871
				if (rangeSelector.selected === i) {
22872
					buttons[i].setState(2);
22873
				}
22874
			});
22875
 
22876
			rangeSelector.updateButtonStates();
22877
 
22878
			// first create a wrapper outside the container in order to make
22879
			// the inputs work and make export correct
22880
			if (inputEnabled !== false) {
22881
				rangeSelector.div = div = createElement('div', null, {
22882
					position: 'relative',
22883
					height: 0,
22884
					zIndex: 1 // above container
22885
				});
22886
 
22887
				container.parentNode.insertBefore(div, container);
22888
 
22889
				// Create the group to keep the inputs
22890
				rangeSelector.inputGroup = inputGroup = renderer.g('input-group')
22891
					.add();
22892
				inputGroup.offset = 0;
22893
 
22894
				rangeSelector.drawInput('min');
22895
				rangeSelector.drawInput('max');
22896
			}
22897
		}
22898
 
22899
		// Set or update the group position
22900
		buttonGroup[rendered ? 'animate' : 'attr']({
22901
			translateY: pos.buttonTop
22902
		});
22903
 
22904
		if (inputEnabled !== false) {
22905
 
22906
			// Update the alignment to the updated spacing box
22907
			inputGroup.align(extend({
22908
				y: pos.inputTop,
22909
				width: inputGroup.offset,
22910
				// Detect collision with the exporting buttons
22911
				x: navButtonOptions && (pos.inputTop < (navButtonOptions.y || 0) + navButtonOptions.height - chart.spacing[0]) ?
22912
					-40 : 0
22913
			}, options.inputPosition), true, chart.spacingBox);
22914
 
22915
			// Hide if overlapping - inputEnabled is null or undefined
22916
			if (!defined(inputEnabled)) {
22917
				buttonBBox = buttonGroup.getBBox();
22918
				inputGroup[inputGroup.translateX < buttonBBox.x + buttonBBox.width + 10 ? 'hide' : 'show']();
22919
			}
22920
 
22921
			// Set or reset the input values
22922
			rangeSelector.setInputValue('min', min);
22923
			rangeSelector.setInputValue('max', max);
22924
		}
22925
 
22926
		rangeSelector.rendered = true;
22927
	},
22928
 
22929
	/**
22930
	 * Destroys allocated elements.
22931
	 */
22932
	destroy: function () {
22933
		var minInput = this.minInput,
22934
			maxInput = this.maxInput,
22935
			chart = this.chart,
22936
			blurInputs = this.blurInputs,
22937
			key;
22938
 
22939
		removeEvent(chart.container, 'mousedown', blurInputs);
22940
		removeEvent(chart, 'resize', blurInputs);
22941
 
22942
		// Destroy elements in collections
22943
		destroyObjectProperties(this.buttons);
22944
 
22945
		// Clear input element events
22946
		if (minInput) {
22947
			minInput.onfocus = minInput.onblur = minInput.onchange = null;
22948
		}
22949
		if (maxInput) {
22950
			maxInput.onfocus = maxInput.onblur = maxInput.onchange = null;
22951
		}
22952
 
22953
		// Destroy HTML and SVG elements
22954
		for (key in this) {
22955
			if (this[key] && key !== 'chart') {
22956
				if (this[key].destroy) { // SVGElement
22957
					this[key].destroy();
22958
				} else if (this[key].nodeType) { // HTML element
22959
					discardElement(this[key]);
22960
				}
22961
			}
22962
			this[key] = null;
22963
		}
22964
	}
22965
};
22966
 
22967
/**
22968
 * Add logic to normalize the zoomed range in order to preserve the pressed state of range selector buttons
22969
 */
22970
Axis.prototype.toFixedRange = function (pxMin, pxMax, fixedMin, fixedMax) {
22971
	var fixedRange = this.chart && this.chart.fixedRange,
22972
		newMin = pick(fixedMin, this.translate(pxMin, true)),
22973
		newMax = pick(fixedMax, this.translate(pxMax, true)),
22974
		changeRatio = fixedRange && (newMax - newMin) / fixedRange;
22975
 
22976
	// If the difference between the fixed range and the actual requested range is
22977
	// too great, the user is dragging across an ordinal gap, and we need to release
22978
	// the range selector button.
22979
	if (changeRatio > 0.7 && changeRatio < 1.3) {
22980
		if (fixedMax) {
22981
			newMin = newMax - fixedRange;
22982
		} else {
22983
			newMax = newMin + fixedRange;
22984
		}
22985
	}
22986
 
22987
	return {
22988
		min: newMin,
22989
		max: newMax
22990
	};
22991
};
22992
 
22993
Axis.prototype.minFromRange = function () {
22994
	var rangeOptions = this.range,
22995
		type = rangeOptions.type,
22996
		timeName = { month: 'Month', year: 'FullYear'}[type],
22997
		min,
22998
		max = this.max,
22999
		dataMin,
23000
		range,
23001
		// Get the true range from a start date
23002
		getTrueRange = function (base, count) {
23003
			var date = new Date(base);
23004
			date['set' + timeName](date['get' + timeName]() + count);
23005
			return date.getTime() - base;
23006
		};
23007
 
23008
	if (typeof rangeOptions === 'number') {
23009
		min = this.max - rangeOptions;
23010
		range = rangeOptions;
23011
	} else {
23012
		min = max + getTrueRange(max, -rangeOptions.count);
23013
	}
23014
 
23015
	dataMin = pick(this.dataMin, Number.MIN_VALUE);
23016
	if (isNaN(min)) {
23017
		min = dataMin;
23018
	}
23019
	if (min <= dataMin) {
23020
		min = dataMin;
23021
		if (range === undefined) { // #4501
23022
			range = getTrueRange(min, rangeOptions.count);
23023
		}
23024
		this.newMax = mathMin(min + range, this.dataMax);
23025
	}
23026
	if (isNaN(max)) {
23027
		min = undefined;
23028
	}
23029
	return min;
23030
 
23031
};
23032
 
23033
// Initialize scroller for stock charts
23034
wrap(Chart.prototype, 'init', function (proceed, options, callback) {
23035
 
23036
	addEvent(this, 'init', function () {
23037
		if (this.options.rangeSelector.enabled) {
23038
			this.rangeSelector = new RangeSelector(this);
23039
		}
23040
	});
23041
 
23042
	proceed.call(this, options, callback);
23043
 
23044
});
23045
 
23046
 
23047
Highcharts.RangeSelector = RangeSelector;
23048
 
23049
/* ****************************************************************************
23050
 * End Range Selector code													*
23051
 *****************************************************************************/
23052
 
23053
 
23054
 
23055
Chart.prototype.callbacks.push(function (chart) {
23056
	var extremes,
23057
		scroller = chart.scroller,
23058
		rangeSelector = chart.rangeSelector;
23059
 
23060
	function renderScroller() {
23061
		extremes = chart.xAxis[0].getExtremes();
23062
		scroller.render(extremes.min, extremes.max);
23063
	}
23064
 
23065
	function renderRangeSelector() {
23066
		extremes = chart.xAxis[0].getExtremes();
23067
		if (!isNaN(extremes.min)) {
23068
			rangeSelector.render(extremes.min, extremes.max);
23069
		}
23070
	}
23071
 
23072
	function afterSetExtremesHandlerScroller(e) {
23073
		if (e.triggerOp !== 'navigator-drag') {
23074
			scroller.render(e.min, e.max);
23075
		}
23076
	}
23077
 
23078
	function afterSetExtremesHandlerRangeSelector(e) {
23079
		rangeSelector.render(e.min, e.max);
23080
	}
23081
 
23082
	function destroyEvents() {
23083
		if (scroller) {
23084
			removeEvent(chart.xAxis[0], 'afterSetExtremes', afterSetExtremesHandlerScroller);
23085
		}
23086
		if (rangeSelector) {
23087
			removeEvent(chart, 'resize', renderRangeSelector);
23088
			removeEvent(chart.xAxis[0], 'afterSetExtremes', afterSetExtremesHandlerRangeSelector);
23089
		}
23090
	}
23091
 
23092
	// initiate the scroller
23093
	if (scroller) {
23094
		// redraw the scroller on setExtremes
23095
		addEvent(chart.xAxis[0], 'afterSetExtremes', afterSetExtremesHandlerScroller);
23096
 
23097
		// redraw the scroller on chart resize or box resize
23098
		wrap(chart, 'drawChartBox', function (proceed) {
23099
			var isDirtyBox = this.isDirtyBox;
23100
			proceed.call(this);
23101
			if (isDirtyBox) {
23102
				renderScroller();
23103
			}
23104
		});
23105
 
23106
		// do it now
23107
		renderScroller();
23108
	}
23109
	if (rangeSelector) {
23110
		// redraw the scroller on setExtremes
23111
		addEvent(chart.xAxis[0], 'afterSetExtremes', afterSetExtremesHandlerRangeSelector);
23112
 
23113
		// redraw the scroller chart resize
23114
		addEvent(chart, 'resize', renderRangeSelector);
23115
 
23116
		// do it now
23117
		renderRangeSelector();
23118
	}
23119
 
23120
	// Remove resize/afterSetExtremes at chart destroy
23121
	addEvent(chart, 'destroy', destroyEvents);
23122
});
23123
/**
23124
 * A wrapper for Chart with all the default values for a Stock chart
23125
 */
23126
Highcharts.StockChart = function (options, callback) {
23127
	var seriesOptions = options.series, // to increase performance, don't merge the data
23128
		opposite,
23129
 
23130
		// Always disable startOnTick:true on the main axis when the navigator is enabled (#1090)
23131
		navigatorEnabled = pick(options.navigator && options.navigator.enabled, true),
23132
		disableStartOnTick = navigatorEnabled ? {
23133
			startOnTick: false,
23134
			endOnTick: false
23135
		} : null,
23136
 
23137
		lineOptions = {
23138
 
23139
			marker: {
23140
				enabled: false,
23141
				radius: 2
23142
			}
23143
			// gapSize: 0
23144
		},
23145
		columnOptions = {
23146
			shadow: false,
23147
			borderWidth: 0
23148
		};
23149
 
23150
	// apply X axis options to both single and multi y axes
23151
	options.xAxis = map(splat(options.xAxis || {}), function (xAxisOptions) {
23152
		return merge({ // defaults
23153
				minPadding: 0,
23154
				maxPadding: 0,
23155
				ordinal: true,
23156
				title: {
23157
					text: null
23158
				},
23159
				labels: {
23160
					overflow: 'justify'
23161
				},
23162
				showLastLabel: true
23163
			}, xAxisOptions, // user options
23164
			{ // forced options
23165
				type: 'datetime',
23166
				categories: null
23167
			},
23168
			disableStartOnTick
23169
		);
23170
	});
23171
 
23172
	// apply Y axis options to both single and multi y axes
23173
	options.yAxis = map(splat(options.yAxis || {}), function (yAxisOptions) {
23174
		opposite = pick(yAxisOptions.opposite, true);
23175
		return merge({ // defaults
23176
			labels: {
23177
				y: -2
23178
			},
23179
			opposite: opposite,
23180
			showLastLabel: false,
23181
			title: {
23182
				text: null
23183
			}
23184
		}, yAxisOptions // user options
23185
		);
23186
	});
23187
 
23188
	options.series = null;
23189
 
23190
	options = merge({
23191
		chart: {
23192
			panning: true,
23193
			pinchType: 'x'
23194
		},
23195
		navigator: {
23196
			enabled: true
23197
		},
23198
		scrollbar: {
23199
			enabled: true
23200
		},
23201
		rangeSelector: {
23202
			enabled: true
23203
		},
23204
		title: {
23205
			text: null,
23206
			style: {
23207
				fontSize: '16px'
23208
			}
23209
		},
23210
		tooltip: {
23211
			shared: true,
23212
			crosshairs: true
23213
		},
23214
		legend: {
23215
			enabled: false
23216
		},
23217
 
23218
		plotOptions: {
23219
			line: lineOptions,
23220
			spline: lineOptions,
23221
			area: lineOptions,
23222
			areaspline: lineOptions,
23223
			arearange: lineOptions,
23224
			areasplinerange: lineOptions,
23225
			column: columnOptions,
23226
			columnrange: columnOptions,
23227
			candlestick: columnOptions,
23228
			ohlc: columnOptions
23229
		}
23230
 
23231
	},
23232
	options, // user's options
23233
 
23234
	{ // forced options
23235
		_stock: true, // internal flag
23236
		chart: {
23237
			inverted: false
23238
		}
23239
	});
23240
 
23241
	options.series = seriesOptions;
23242
 
23243
 
23244
	return new Chart(options, callback);
23245
};
23246
 
23247
// Implement the pinchType option
23248
wrap(Pointer.prototype, 'init', function (proceed, chart, options) {
23249
 
23250
	var pinchType = options.chart.pinchType || '';
23251
 
23252
	proceed.call(this, chart, options);
23253
 
23254
	// Pinch status
23255
	this.pinchX = this.pinchHor = pinchType.indexOf('x') !== -1;
23256
	this.pinchY = this.pinchVert = pinchType.indexOf('y') !== -1;
23257
	this.hasZoom = this.hasZoom || this.pinchHor || this.pinchVert;
23258
});
23259
 
23260
// Override the automatic label alignment so that the first Y axis' labels
23261
// are drawn on top of the grid line, and subsequent axes are drawn outside
23262
wrap(Axis.prototype, 'autoLabelAlign', function (proceed) {
23263
	var chart = this.chart,
23264
		options = this.options,
23265
		panes = chart._labelPanes = chart._labelPanes || {},
23266
		key,
23267
		labelOptions = this.options.labels;
23268
	if (this.chart.options._stock && this.coll === 'yAxis') {
23269
		key = options.top + ',' + options.height;
23270
		if (!panes[key] && labelOptions.enabled) { // do it only for the first Y axis of each pane
23271
			if (labelOptions.x === 15) { // default
23272
				labelOptions.x = 0;
23273
			}
23274
			if (labelOptions.align === undefined) {
23275
				labelOptions.align = 'right';
23276
			}
23277
			panes[key] = 1;
23278
			return 'right';
23279
		}
23280
	}
23281
	return proceed.call(this, [].slice.call(arguments, 1));
23282
});
23283
 
23284
// Override getPlotLinePath to allow for multipane charts
23285
wrap(Axis.prototype, 'getPlotLinePath', function (proceed, value, lineWidth, old, force, translatedValue) {
23286
	var axis = this,
23287
		series = (this.isLinked && !this.series ? this.linkedParent.series : this.series),
23288
		chart = axis.chart,
23289
		renderer = chart.renderer,
23290
		axisLeft = axis.left,
23291
		axisTop = axis.top,
23292
		x1,
23293
		y1,
23294
		x2,
23295
		y2,
23296
		result = [],
23297
		axes = [], //#3416 need a default array
23298
		axes2,
23299
		uniqueAxes;
23300
 
23301
	// Ignore in case of color Axis. #3360, #3524
23302
	if (axis.coll === 'colorAxis') {
23303
		return proceed.apply(this, [].slice.call(arguments, 1));
23304
	}
23305
 
23306
	// Get the related axes based on series
23307
	axes = (axis.isXAxis ?
23308
		(defined(axis.options.yAxis) ?
23309
			[chart.yAxis[axis.options.yAxis]] :
23310
			map(series, function (S) { return S.yAxis; })
23311
		) :
23312
		(defined(axis.options.xAxis) ?
23313
			[chart.xAxis[axis.options.xAxis]] :
23314
			map(series, function (S) { return S.xAxis; })
23315
		)
23316
	);
23317
 
23318
	// Get the related axes based options.*Axis setting #2810
23319
	axes2 = (axis.isXAxis ? chart.yAxis : chart.xAxis);
23320
	each(axes2, function (A) {
23321
		if (defined(A.options.id) ? A.options.id.indexOf('navigator') === -1 : true) {
23322
			var a = (A.isXAxis ? 'yAxis' : 'xAxis'),
23323
				rax = (defined(A.options[a]) ? chart[a][A.options[a]] : chart[a][0]);
23324
 
23325
			if (axis === rax) {
23326
				axes.push(A);
23327
			}
23328
		}
23329
	});
23330
 
23331
 
23332
	// Remove duplicates in the axes array. If there are no axes in the axes array,
23333
	// we are adding an axis without data, so we need to populate this with grid
23334
	// lines (#2796).
23335
	uniqueAxes = axes.length ? [] : [axis.isXAxis ? chart.yAxis[0] : chart.xAxis[0]]; //#3742
23336
	each(axes, function (axis2) {
23337
		if (inArray(axis2, uniqueAxes) === -1) {
23338
			uniqueAxes.push(axis2);
23339
		}
23340
	});
23341
 
23342
	translatedValue = pick(translatedValue, axis.translate(value, null, null, old));
23343
 
23344
	if (!isNaN(translatedValue)) {
23345
		if (axis.horiz) {
23346
			each(uniqueAxes, function (axis2) {
23347
				var skip;
23348
 
23349
				y1 = axis2.pos;
23350
				y2 = y1 + axis2.len;
23351
				x1 = x2 = mathRound(translatedValue + axis.transB);
23352
 
23353
				if (x1 < axisLeft || x1 > axisLeft + axis.width) { // outside plot area
23354
					if (force) {
23355
						x1 = x2 = mathMin(mathMax(axisLeft, x1), axisLeft + axis.width);
23356
					} else {
23357
						skip = true;
23358
					}
23359
				}
23360
				if (!skip) {
23361
					result.push('M', x1, y1, 'L', x2, y2);
23362
				}
23363
			});
23364
		} else {
23365
			each(uniqueAxes, function (axis2) {
23366
				var skip;
23367
 
23368
				x1 = axis2.pos;
23369
				x2 = x1 + axis2.len;
23370
				y1 = y2 = mathRound(axisTop + axis.height - translatedValue);
23371
 
23372
				if (y1 < axisTop || y1 > axisTop + axis.height) { // outside plot area
23373
					if (force) {
23374
						y1 = y2 = mathMin(mathMax(axisTop, y1), axis.top + axis.height);
23375
					} else {
23376
						skip = true;
23377
					}
23378
				}
23379
				if (!skip) {
23380
					result.push('M', x1, y1, 'L', x2, y2);
23381
				}
23382
			});
23383
		}
23384
	}
23385
	if (result.length > 0) {
23386
		return renderer.crispPolyLine(result, lineWidth || 1);
23387
	} else {
23388
		return null; //#3557 getPlotLinePath in regular Highcharts also returns null
23389
	}
23390
});
23391
 
23392
// Override getPlotBandPath to allow for multipane charts
23393
Axis.prototype.getPlotBandPath = function (from, to) {
23394
	var toPath = this.getPlotLinePath(to, null, null, true),
23395
		path = this.getPlotLinePath(from, null, null, true),
23396
		result = [],
23397
		i;
23398
 
23399
	if (path && toPath && path.toString() !== toPath.toString()) {
23400
		// Go over each subpath
23401
		for (i = 0; i < path.length; i += 6) {
23402
			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]);
23403
		}
23404
	} else { // outside the axis area
23405
		result = null;
23406
	}
23407
 
23408
	return result;
23409
};
23410
 
23411
// Function to crisp a line with multiple segments
23412
SVGRenderer.prototype.crispPolyLine = function (points, width) {
23413
	// points format: [M, 0, 0, L, 100, 0]
23414
	// normalize to a crisp line
23415
	var i;
23416
	for (i = 0; i < points.length; i = i + 6) {
23417
		if (points[i + 1] === points[i + 4]) {
23418
			// Substract due to #1129. Now bottom and left axis gridlines behave the same.
23419
			points[i + 1] = points[i + 4] = mathRound(points[i + 1]) - (width % 2 / 2);
23420
		}
23421
		if (points[i + 2] === points[i + 5]) {
23422
			points[i + 2] = points[i + 5] = mathRound(points[i + 2]) + (width % 2 / 2);
23423
		}
23424
	}
23425
	return points;
23426
};
23427
if (Renderer === Highcharts.VMLRenderer) {
23428
	VMLRenderer.prototype.crispPolyLine = SVGRenderer.prototype.crispPolyLine;
23429
}
23430
 
23431
 
23432
// Wrapper to hide the label
23433
wrap(Axis.prototype, 'hideCrosshair', function (proceed, i) {
23434
	proceed.call(this, i);
23435
 
23436
	if (!defined(this.crossLabelArray)) { return; }
23437
 
23438
	if (defined(i)) {
23439
		if (this.crossLabelArray[i]) { this.crossLabelArray[i].hide(); }
23440
	} else {
23441
		each(this.crossLabelArray, function (crosslabel) {
23442
			crosslabel.hide();
23443
		});
23444
	}
23445
});
23446
 
23447
// Wrapper to draw the label
23448
wrap(Axis.prototype, 'drawCrosshair', function (proceed, e, point) {
23449
	// Draw the crosshair
23450
	proceed.call(this, e, point);
23451
 
23452
	// Check if the label has to be drawn
23453
	if (!defined(this.crosshair.label) || !this.crosshair.label.enabled || !defined(point)) {
23454
		return;
23455
	}
23456
 
23457
	var chart = this.chart,
23458
		options = this.options.crosshair.label,		// the label's options
23459
		axis = this.isXAxis ? 'x' : 'y',			// axis name
23460
		horiz = this.horiz,							// axis orientation
23461
		opposite = this.opposite,					// axis position
23462
		left = this.left,							// left position
23463
		top = this.top,								// top position
23464
		crossLabel = this.crossLabel,				// reference to the svgElement
23465
		posx,
23466
		posy,
23467
		crossBox,
23468
		formatOption = options.format,
23469
		formatFormat = '',
23470
		limit;
23471
 
23472
	// If the label does not exist yet, create it.
23473
	if (!crossLabel) {
23474
		crossLabel = this.crossLabel = chart.renderer.label()
23475
		.attr({
23476
			align: options.align || (horiz ? 'center' : opposite ? (this.labelAlign === 'right' ? 'right' : 'left') : (this.labelAlign === 'left' ? 'left' : 'center')),
23477
			zIndex: 12,
23478
			height: horiz ? 16 : UNDEFINED,
23479
			fill: options.backgroundColor || (this.series[0] && this.series[0].color) || 'gray',
23480
			padding: pick(options.padding, 2),
23481
			stroke: options.borderColor || null,
23482
			'stroke-width': options.borderWidth || 0
23483
		})
23484
		.css(extend({
23485
			color: 'white',
23486
			fontWeight: 'normal',
23487
			fontSize: '11px',
23488
			textAlign: 'center'
23489
		}, options.style))
23490
		.add();
23491
	}
23492
 
23493
	if (horiz) {
23494
		posx = point.plotX + left;
23495
		posy = top + (opposite ? 0 : this.height);
23496
	} else {
23497
		posx = opposite ? this.width + left : 0;
23498
		posy = point.plotY + top;
23499
	}
23500
 
23501
	// if the crosshair goes out of view (too high or too low, hide it and hide the label)
23502
	if (posy < top || posy > top + this.height) {
23503
		this.hideCrosshair();
23504
		return;
23505
	}
23506
 
23507
	// TODO: Dynamic date formats like in Series.tooltipHeaderFormat.
23508
	if (!formatOption && !options.formatter) {
23509
		if (this.isDatetimeAxis) {
23510
			formatFormat = '%b %d, %Y';
23511
		}
23512
		formatOption = '{value' + (formatFormat ? ':' + formatFormat : '') + '}';
23513
	}
23514
 
23515
	// show the label
23516
	crossLabel.attr({
23517
		text: formatOption ? format(formatOption, {value: point[axis]}) : options.formatter.call(this, point[axis]),
23518
		x: posx,
23519
		y: posy,
23520
		visibility: VISIBLE
23521
	});
23522
	crossBox = crossLabel.getBBox();
23523
 
23524
	// now it is placed we can correct its position
23525
	if (horiz) {
23526
		if (((this.options.tickPosition === 'inside') && !opposite) ||
23527
			((this.options.tickPosition !== 'inside') && opposite)) {
23528
			posy = crossLabel.y - crossBox.height;
23529
		}
23530
	} else {
23531
		posy = crossLabel.y - (crossBox.height / 2);
23532
	}
23533
 
23534
	// check the edges
23535
	if (horiz) {
23536
		limit = {
23537
			left: left - crossBox.x,
23538
			right: left + this.width - crossBox.x
23539
		};
23540
	} else {
23541
		limit = {
23542
			left: this.labelAlign === 'left' ? left : 0,
23543
			right: this.labelAlign === 'right' ? left + this.width : chart.chartWidth
23544
		};
23545
	}
23546
 
23547
	// left edge
23548
	if (crossLabel.translateX < limit.left) {
23549
		posx += limit.left - crossLabel.translateX;
23550
	}
23551
	// right edge
23552
	if (crossLabel.translateX + crossBox.width >= limit.right) {
23553
		posx -= crossLabel.translateX + crossBox.width - limit.right;
23554
	}
23555
 
23556
	// show the crosslabel
23557
	crossLabel.attr({x: posx, y: posy, visibility: VISIBLE});
23558
});
23559
 
23560
/* ****************************************************************************
23561
 * Start value compare logic                                                  *
23562
 *****************************************************************************/
23563
 
23564
var seriesInit = seriesProto.init,
23565
	seriesProcessData = seriesProto.processData,
23566
	pointTooltipFormatter = Point.prototype.tooltipFormatter;
23567
 
23568
/**
23569
 * Extend series.init by adding a method to modify the y value used for plotting
23570
 * on the y axis. This method is called both from the axis when finding dataMin
23571
 * and dataMax, and from the series.translate method.
23572
 */
23573
seriesProto.init = function () {
23574
 
23575
	// Call base method
23576
	seriesInit.apply(this, arguments);
23577
 
23578
	// Set comparison mode
23579
	this.setCompare(this.options.compare);
23580
};
23581
 
23582
/**
23583
 * The setCompare method can be called also from the outside after render time
23584
 */
23585
seriesProto.setCompare = function (compare) {
23586
 
23587
	// Set or unset the modifyValue method
23588
	this.modifyValue = (compare === 'value' || compare === 'percent') ? function (value, point) {
23589
		var compareValue = this.compareValue;
23590
 
23591
		if (value !== UNDEFINED) { // #2601
23592
 
23593
			// get the modified value
23594
			value = compare === 'value' ?
23595
				value - compareValue : // compare value
23596
				value = 100 * (value / compareValue) - 100; // compare percent
23597
 
23598
			// record for tooltip etc.
23599
			if (point) {
23600
				point.change = value;
23601
			}
23602
 
23603
		}
23604
 
23605
		return value;
23606
	} : null;
23607
 
23608
	// Mark dirty
23609
	if (this.chart.hasRendered) {
23610
		this.isDirty = true;
23611
	}
23612
 
23613
};
23614
 
23615
/**
23616
 * Extend series.processData by finding the first y value in the plot area,
23617
 * used for comparing the following values
23618
 */
23619
seriesProto.processData = function () {
23620
	var series = this,
23621
		i = 0,
23622
		processedXData,
23623
		processedYData,
23624
		length;
23625
 
23626
	// call base method
23627
	seriesProcessData.apply(this, arguments);
23628
 
23629
	if (series.xAxis && series.processedYData) { // not pies
23630
 
23631
		// local variables
23632
		processedXData = series.processedXData;
23633
		processedYData = series.processedYData;
23634
		length = processedYData.length;
23635
 
23636
		// find the first value for comparison
23637
		for (; i < length; i++) {
23638
			if (typeof processedYData[i] === NUMBER && processedXData[i] >= series.xAxis.min) {
23639
				series.compareValue = processedYData[i];
23640
				break;
23641
			}
23642
		}
23643
	}
23644
};
23645
 
23646
/**
23647
 * Modify series extremes
23648
 */
23649
wrap(seriesProto, 'getExtremes', function (proceed) {
23650
	proceed.apply(this, [].slice.call(arguments, 1));
23651
 
23652
	if (this.modifyValue) {
23653
		this.dataMax = this.modifyValue(this.dataMax);
23654
		this.dataMin = this.modifyValue(this.dataMin);
23655
	}
23656
});
23657
 
23658
/**
23659
 * Add a utility method, setCompare, to the Y axis
23660
 */
23661
Axis.prototype.setCompare = function (compare, redraw) {
23662
	if (!this.isXAxis) {
23663
		each(this.series, function (series) {
23664
			series.setCompare(compare);
23665
		});
23666
		if (pick(redraw, true)) {
23667
			this.chart.redraw();
23668
		}
23669
	}
23670
};
23671
 
23672
/**
23673
 * Extend the tooltip formatter by adding support for the point.change variable
23674
 * as well as the changeDecimals option
23675
 */
23676
Point.prototype.tooltipFormatter = function (pointFormat) {
23677
	var point = this;
23678
 
23679
	pointFormat = pointFormat.replace(
23680
		'{point.change}',
23681
		(point.change > 0 ? '+' : '') + Highcharts.numberFormat(point.change, pick(point.series.tooltipOptions.changeDecimals, 2))
23682
	);
23683
 
23684
	return pointTooltipFormatter.apply(this, [pointFormat]);
23685
};
23686
 
23687
/* ****************************************************************************
23688
 * End value compare logic                                                    *
23689
 *****************************************************************************/
23690
 
23691
 
23692
/**
23693
 * Extend the Series prototype to create a separate series clip box. This is related
23694
 * to using multiple panes, and a future pane logic should incorporate this feature (#2754).
23695
 */
23696
wrap(Series.prototype, 'render', function (proceed) {
23697
	// Only do this on stock charts (#2939), and only if the series type handles clipping
23698
	// in the animate method (#2975).
23699
	if (this.chart.options._stock && this.xAxis) {
23700
 
23701
		// First render, initial clip box
23702
		if (!this.clipBox && this.animate) {
23703
			this.clipBox = merge(this.chart.clipBox);
23704
			this.clipBox.width = this.xAxis.len;
23705
			this.clipBox.height = this.yAxis.len;
23706
 
23707
		// On redrawing, resizing etc, update the clip rectangle
23708
		} else if (this.chart[this.sharedClipKey]) {
23709
			stop(this.chart[this.sharedClipKey]); // #2998
23710
			this.chart[this.sharedClipKey].attr({
23711
				width: this.xAxis.len,
23712
				height: this.yAxis.len
23713
			});
23714
		}
23715
	}
23716
	proceed.call(this);
23717
});
23718
 
23719
// global variables
23720
extend(Highcharts, {
23721
 
23722
	// Constructors
23723
	Color: Color,
23724
	Point: Point,
23725
	Tick: Tick,
23726
	Renderer: Renderer,
23727
	SVGElement: SVGElement,
23728
	SVGRenderer: SVGRenderer,
23729
 
23730
	// Various
23731
	arrayMin: arrayMin,
23732
	arrayMax: arrayMax,
23733
	charts: charts,
23734
	dateFormat: dateFormat,
23735
	error: error,
23736
	format: format,
23737
	pathAnim: pathAnim,
23738
	getOptions: getOptions,
23739
	hasBidiBug: hasBidiBug,
23740
	isTouchDevice: isTouchDevice,
23741
	setOptions: setOptions,
23742
	addEvent: addEvent,
23743
	removeEvent: removeEvent,
23744
	createElement: createElement,
23745
	discardElement: discardElement,
23746
	css: css,
23747
	each: each,
23748
	map: map,
23749
	merge: merge,
23750
	splat: splat,
23751
	extendClass: extendClass,
23752
	pInt: pInt,
23753
	svg: hasSVG,
23754
	canvas: useCanVG,
23755
	vml: !hasSVG && !useCanVG,
23756
	product: PRODUCT,
23757
	version: VERSION
23758
});
23759
 
23760
}());