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 Highmaps JS v1.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 = 'Highmaps',
58
	VERSION = '1.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/maps/1.1.9/modules/canvas-tools.js',
1273
		VMLRadialGradientURL: 'http://code.highcharts.com/maps/1.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
 * Create a new axis object
6237
 * @param {Object} chart
6238
 * @param {Object} options
6239
 */
6240
var Axis = Highcharts.Axis = function () {
6241
	this.init.apply(this, arguments);
6242
};
6243
 
6244
Axis.prototype = {
6245
 
6246
	/**
6247
	 * Default options for the X axis - the Y axis has extended defaults
6248
	 */
6249
	defaultOptions: {
6250
		// allowDecimals: null,
6251
		// alternateGridColor: null,
6252
		// categories: [],
6253
		dateTimeLabelFormats: {
6254
			millisecond: '%H:%M:%S.%L',
6255
			second: '%H:%M:%S',
6256
			minute: '%H:%M',
6257
			hour: '%H:%M',
6258
			day: '%e. %b',
6259
			week: '%e. %b',
6260
			month: '%b \'%y',
6261
			year: '%Y'
6262
		},
6263
		endOnTick: false,
6264
		gridLineColor: '#D8D8D8',
6265
		// gridLineDashStyle: 'solid',
6266
		// gridLineWidth: 0,
6267
		// reversed: false,
6268
 
6269
		labels: {
6270
			enabled: true,
6271
			// rotation: 0,
6272
			// align: 'center',
6273
			// step: null,
6274
			style: {
6275
				color: '#606060',
6276
				cursor: 'default',
6277
				fontSize: '11px'
6278
			},
6279
			x: 0,
6280
			y: 15
6281
			/*formatter: function () {
6282
				return this.value;
6283
			},*/
6284
		},
6285
		lineColor: '#C0D0E0',
6286
		lineWidth: 1,
6287
		//linkedTo: null,
6288
		//max: undefined,
6289
		//min: undefined,
6290
		minPadding: 0.01,
6291
		maxPadding: 0.01,
6292
		//minRange: null,
6293
		minorGridLineColor: '#E0E0E0',
6294
		// minorGridLineDashStyle: null,
6295
		minorGridLineWidth: 1,
6296
		minorTickColor: '#A0A0A0',
6297
		//minorTickInterval: null,
6298
		minorTickLength: 2,
6299
		minorTickPosition: 'outside', // inside or outside
6300
		//minorTickWidth: 0,
6301
		//opposite: false,
6302
		//offset: 0,
6303
		//plotBands: [{
6304
		//	events: {},
6305
		//	zIndex: 1,
6306
		//	labels: { align, x, verticalAlign, y, style, rotation, textAlign }
6307
		//}],
6308
		//plotLines: [{
6309
		//	events: {}
6310
		//  dashStyle: {}
6311
		//	zIndex:
6312
		//	labels: { align, x, verticalAlign, y, style, rotation, textAlign }
6313
		//}],
6314
		//reversed: false,
6315
		// showFirstLabel: true,
6316
		// showLastLabel: true,
6317
		startOfWeek: 1,
6318
		startOnTick: false,
6319
		tickColor: '#C0D0E0',
6320
		//tickInterval: null,
6321
		tickLength: 10,
6322
		tickmarkPlacement: 'between', // on or between
6323
		tickPixelInterval: 100,
6324
		tickPosition: 'outside',
6325
		//tickWidth: 1,
6326
		title: {
6327
			//text: null,
6328
			align: 'middle', // low, middle or high
6329
			//margin: 0 for horizontal, 10 for vertical axes,
6330
			//rotation: 0,
6331
			//side: 'outside',
6332
			style: {
6333
				color: '#707070'
6334
			}
6335
			//x: 0,
6336
			//y: 0
6337
		},
6338
		type: 'linear' // linear, logarithmic or datetime
6339
		//visible: true
6340
	},
6341
 
6342
	/**
6343
	 * This options set extends the defaultOptions for Y axes
6344
	 */
6345
	defaultYAxisOptions: {
6346
		endOnTick: true,
6347
		gridLineWidth: 1,
6348
		tickPixelInterval: 72,
6349
		showLastLabel: true,
6350
		labels: {
6351
			x: -8,
6352
			y: 3
6353
		},
6354
		lineWidth: 0,
6355
		maxPadding: 0.05,
6356
		minPadding: 0.05,
6357
		startOnTick: true,
6358
		//tickWidth: 0,
6359
		title: {
6360
			rotation: 270,
6361
			text: 'Values'
6362
		},
6363
		stackLabels: {
6364
			enabled: false,
6365
			//align: dynamic,
6366
			//y: dynamic,
6367
			//x: dynamic,
6368
			//verticalAlign: dynamic,
6369
			//textAlign: dynamic,
6370
			//rotation: 0,
6371
			formatter: function () {
6372
				return Highcharts.numberFormat(this.total, -1);
6373
			},
6374
			style: merge(defaultPlotOptions.line.dataLabels.style, { color: '#000000' })
6375
		}
6376
	},
6377
 
6378
	/**
6379
	 * These options extend the defaultOptions for left axes
6380
	 */
6381
	defaultLeftAxisOptions: {
6382
		labels: {
6383
			x: -15,
6384
			y: null
6385
		},
6386
		title: {
6387
			rotation: 270
6388
		}
6389
	},
6390
 
6391
	/**
6392
	 * These options extend the defaultOptions for right axes
6393
	 */
6394
	defaultRightAxisOptions: {
6395
		labels: {
6396
			x: 15,
6397
			y: null
6398
		},
6399
		title: {
6400
			rotation: 90
6401
		}
6402
	},
6403
 
6404
	/**
6405
	 * These options extend the defaultOptions for bottom axes
6406
	 */
6407
	defaultBottomAxisOptions: {
6408
		labels: {
6409
			autoRotation: [-45],
6410
			x: 0,
6411
			y: null // based on font size
6412
			// overflow: undefined,
6413
			// staggerLines: null
6414
		},
6415
		title: {
6416
			rotation: 0
6417
		}
6418
	},
6419
	/**
6420
	 * These options extend the defaultOptions for top axes
6421
	 */
6422
	defaultTopAxisOptions: {
6423
		labels: {
6424
			autoRotation: [-45],
6425
			x: 0,
6426
			y: -15
6427
			// overflow: undefined
6428
			// staggerLines: null
6429
		},
6430
		title: {
6431
			rotation: 0
6432
		}
6433
	},
6434
 
6435
	/**
6436
	 * Initialize the axis
6437
	 */
6438
	init: function (chart, userOptions) {
6439
 
6440
 
6441
		var isXAxis = userOptions.isX,
6442
			axis = this;
6443
 
6444
		axis.chart = chart;
6445
 
6446
		// Flag, is the axis horizontal
6447
		axis.horiz = chart.inverted ? !isXAxis : isXAxis;
6448
 
6449
		// Flag, isXAxis
6450
		axis.isXAxis = isXAxis;
6451
		axis.coll = isXAxis ? 'xAxis' : 'yAxis';
6452
 
6453
		axis.opposite = userOptions.opposite; // needed in setOptions
6454
		axis.side = userOptions.side || (axis.horiz ?
6455
				(axis.opposite ? 0 : 2) : // top : bottom
6456
				(axis.opposite ? 1 : 3));  // right : left
6457
 
6458
		axis.setOptions(userOptions);
6459
 
6460
 
6461
		var options = this.options,
6462
			type = options.type,
6463
			isDatetimeAxis = type === 'datetime';
6464
 
6465
		axis.labelFormatter = options.labels.formatter || axis.defaultLabelFormatter; // can be overwritten by dynamic format
6466
 
6467
 
6468
		// Flag, stagger lines or not
6469
		axis.userOptions = userOptions;
6470
 
6471
		//axis.axisTitleMargin = UNDEFINED,// = options.title.margin,
6472
		axis.minPixelPadding = 0;
6473
 
6474
		axis.reversed = options.reversed;
6475
		axis.visible = options.visible !== false;
6476
		axis.zoomEnabled = options.zoomEnabled !== false;
6477
 
6478
		// Initial categories
6479
		axis.categories = options.categories || type === 'category';
6480
		axis.names = axis.names || []; // Preserve on update (#3830)
6481
 
6482
		// Elements
6483
		//axis.axisGroup = UNDEFINED;
6484
		//axis.gridGroup = UNDEFINED;
6485
		//axis.axisTitle = UNDEFINED;
6486
		//axis.axisLine = UNDEFINED;
6487
 
6488
		// Shorthand types
6489
		axis.isLog = type === 'logarithmic';
6490
		axis.isDatetimeAxis = isDatetimeAxis;
6491
 
6492
		// Flag, if axis is linked to another axis
6493
		axis.isLinked = defined(options.linkedTo);
6494
		// Linked axis.
6495
		//axis.linkedParent = UNDEFINED;
6496
 
6497
		// Tick positions
6498
		//axis.tickPositions = UNDEFINED; // array containing predefined positions
6499
		// Tick intervals
6500
		//axis.tickInterval = UNDEFINED;
6501
		//axis.minorTickInterval = UNDEFINED;
6502
 
6503
 
6504
		// Major ticks
6505
		axis.ticks = {};
6506
		axis.labelEdge = [];
6507
		// Minor ticks
6508
		axis.minorTicks = {};
6509
 
6510
		// List of plotLines/Bands
6511
		axis.plotLinesAndBands = [];
6512
 
6513
		// Alternate bands
6514
		axis.alternateBands = {};
6515
 
6516
		// Axis metrics
6517
		//axis.left = UNDEFINED;
6518
		//axis.top = UNDEFINED;
6519
		//axis.width = UNDEFINED;
6520
		//axis.height = UNDEFINED;
6521
		//axis.bottom = UNDEFINED;
6522
		//axis.right = UNDEFINED;
6523
		//axis.transA = UNDEFINED;
6524
		//axis.transB = UNDEFINED;
6525
		//axis.oldTransA = UNDEFINED;
6526
		axis.len = 0;
6527
		//axis.oldMin = UNDEFINED;
6528
		//axis.oldMax = UNDEFINED;
6529
		//axis.oldUserMin = UNDEFINED;
6530
		//axis.oldUserMax = UNDEFINED;
6531
		//axis.oldAxisLength = UNDEFINED;
6532
		axis.minRange = axis.userMinRange = options.minRange || options.maxZoom;
6533
		axis.range = options.range;
6534
		axis.offset = options.offset || 0;
6535
 
6536
 
6537
		// Dictionary for stacks
6538
		axis.stacks = {};
6539
		axis.oldStacks = {};
6540
		axis.stacksTouched = 0;
6541
 
6542
		// Min and max in the data
6543
		//axis.dataMin = UNDEFINED,
6544
		//axis.dataMax = UNDEFINED,
6545
 
6546
		// The axis range
6547
		axis.max = null;
6548
		axis.min = null;
6549
 
6550
		// User set min and max
6551
		//axis.userMin = UNDEFINED,
6552
		//axis.userMax = UNDEFINED,
6553
 
6554
		// Crosshair options
6555
		axis.crosshair = pick(options.crosshair, splat(chart.options.tooltip.crosshairs)[isXAxis ? 0 : 1], false);
6556
		// Run Axis
6557
 
6558
		var eventType,
6559
			events = axis.options.events;
6560
 
6561
		// Register
6562
		if (inArray(axis, chart.axes) === -1) { // don't add it again on Axis.update()
6563
			if (isXAxis && !this.isColorAxis) { // #2713
6564
				chart.axes.splice(chart.xAxis.length, 0, axis);
6565
			} else {
6566
				chart.axes.push(axis);
6567
			}
6568
 
6569
			chart[axis.coll].push(axis);
6570
		}
6571
 
6572
		axis.series = axis.series || []; // populated by Series
6573
 
6574
		// inverted charts have reversed xAxes as default
6575
		if (chart.inverted && isXAxis && axis.reversed === UNDEFINED) {
6576
			axis.reversed = true;
6577
		}
6578
 
6579
		axis.removePlotBand = axis.removePlotBandOrLine;
6580
		axis.removePlotLine = axis.removePlotBandOrLine;
6581
 
6582
 
6583
		// register event listeners
6584
		for (eventType in events) {
6585
			addEvent(axis, eventType, events[eventType]);
6586
		}
6587
 
6588
		// extend logarithmic axis
6589
		if (axis.isLog) {
6590
			axis.val2lin = log2lin;
6591
			axis.lin2val = lin2log;
6592
		}
6593
	},
6594
 
6595
	/**
6596
	 * Merge and set options
6597
	 */
6598
	setOptions: function (userOptions) {
6599
		this.options = merge(
6600
			this.defaultOptions,
6601
			this.isXAxis ? {} : this.defaultYAxisOptions,
6602
			[this.defaultTopAxisOptions, this.defaultRightAxisOptions,
6603
				this.defaultBottomAxisOptions, this.defaultLeftAxisOptions][this.side],
6604
			merge(
6605
				defaultOptions[this.coll], // if set in setOptions (#1053)
6606
				userOptions
6607
			)
6608
		);
6609
	},
6610
 
6611
	/**
6612
	 * The default label formatter. The context is a special config object for the label.
6613
	 */
6614
	defaultLabelFormatter: function () {
6615
		var axis = this.axis,
6616
			value = this.value,
6617
			categories = axis.categories,
6618
			dateTimeLabelFormat = this.dateTimeLabelFormat,
6619
			numericSymbols = defaultOptions.lang.numericSymbols,
6620
			i = numericSymbols && numericSymbols.length,
6621
			multi,
6622
			ret,
6623
			formatOption = axis.options.labels.format,
6624
 
6625
			// make sure the same symbol is added for all labels on a linear axis
6626
			numericSymbolDetector = axis.isLog ? value : axis.tickInterval;
6627
 
6628
		if (formatOption) {
6629
			ret = format(formatOption, this);
6630
 
6631
		} else if (categories) {
6632
			ret = value;
6633
 
6634
		} else if (dateTimeLabelFormat) { // datetime axis
6635
			ret = dateFormat(dateTimeLabelFormat, value);
6636
 
6637
		} else if (i && numericSymbolDetector >= 1000) {
6638
			// Decide whether we should add a numeric symbol like k (thousands) or M (millions).
6639
			// If we are to enable this in tooltip or other places as well, we can move this
6640
			// logic to the numberFormatter and enable it by a parameter.
6641
			while (i-- && ret === UNDEFINED) {
6642
				multi = Math.pow(1000, i + 1);
6643
				if (numericSymbolDetector >= multi && (value * 10) % multi === 0 && numericSymbols[i] !== null) {
6644
					ret = Highcharts.numberFormat(value / multi, -1) + numericSymbols[i];
6645
				}
6646
			}
6647
		}
6648
 
6649
		if (ret === UNDEFINED) {
6650
			if (mathAbs(value) >= 10000) { // add thousands separators
6651
				ret = Highcharts.numberFormat(value, -1);
6652
 
6653
			} else { // small numbers
6654
				ret = Highcharts.numberFormat(value, -1, UNDEFINED, ''); // #2466
6655
			}
6656
		}
6657
 
6658
		return ret;
6659
	},
6660
 
6661
	/**
6662
	 * Get the minimum and maximum for the series of each axis
6663
	 */
6664
	getSeriesExtremes: function () {
6665
		var axis = this,
6666
			chart = axis.chart;
6667
 
6668
		axis.hasVisibleSeries = false;
6669
 
6670
		// Reset properties in case we're redrawing (#3353)
6671
		axis.dataMin = axis.dataMax = axis.threshold = null;
6672
		axis.softThreshold = !axis.isXAxis;
6673
 
6674
		if (axis.buildStacks) {
6675
			axis.buildStacks();
6676
		}
6677
 
6678
		// loop through this axis' series
6679
		each(axis.series, function (series) {
6680
 
6681
			if (series.visible || !chart.options.chart.ignoreHiddenSeries) {
6682
 
6683
				var seriesOptions = series.options,
6684
					xData,
6685
					threshold = seriesOptions.threshold,
6686
					seriesDataMin,
6687
					seriesDataMax;
6688
 
6689
				axis.hasVisibleSeries = true;
6690
 
6691
				// Validate threshold in logarithmic axes
6692
				if (axis.isLog && threshold <= 0) {
6693
					threshold = null;
6694
				}
6695
 
6696
				// Get dataMin and dataMax for X axes
6697
				if (axis.isXAxis) {
6698
					xData = series.xData;
6699
					if (xData.length) {
6700
						axis.dataMin = mathMin(pick(axis.dataMin, xData[0]), arrayMin(xData));
6701
						axis.dataMax = mathMax(pick(axis.dataMax, xData[0]), arrayMax(xData));
6702
					}
6703
 
6704
				// Get dataMin and dataMax for Y axes, as well as handle stacking and processed data
6705
				} else {
6706
 
6707
					// Get this particular series extremes
6708
					series.getExtremes();
6709
					seriesDataMax = series.dataMax;
6710
					seriesDataMin = series.dataMin;
6711
 
6712
					// Get the dataMin and dataMax so far. If percentage is used, the min and max are
6713
					// always 0 and 100. If seriesDataMin and seriesDataMax is null, then series
6714
					// doesn't have active y data, we continue with nulls
6715
					if (defined(seriesDataMin) && defined(seriesDataMax)) {
6716
						axis.dataMin = mathMin(pick(axis.dataMin, seriesDataMin), seriesDataMin);
6717
						axis.dataMax = mathMax(pick(axis.dataMax, seriesDataMax), seriesDataMax);
6718
					}
6719
 
6720
					// Adjust to threshold
6721
					if (defined(threshold)) {
6722
						axis.threshold = threshold;
6723
					}
6724
					// If any series has a hard threshold, it takes precedence
6725
					if (!seriesOptions.softThreshold || axis.isLog) {
6726
						axis.softThreshold = false;
6727
					}
6728
				}
6729
			}
6730
		});
6731
	},
6732
 
6733
	/**
6734
	 * Translate from axis value to pixel position on the chart, or back
6735
	 *
6736
	 */
6737
	translate: function (val, backwards, cvsCoord, old, handleLog, pointPlacement) {
6738
		var axis = this.linkedParent || this, // #1417
6739
			sign = 1,
6740
			cvsOffset = 0,
6741
			localA = old ? axis.oldTransA : axis.transA,
6742
			localMin = old ? axis.oldMin : axis.min,
6743
			returnValue,
6744
			minPixelPadding = axis.minPixelPadding,
6745
			doPostTranslate = (axis.doPostTranslate || (axis.isLog && handleLog)) && axis.lin2val;
6746
 
6747
		if (!localA) {
6748
			localA = axis.transA;
6749
		}
6750
 
6751
		// In vertical axes, the canvas coordinates start from 0 at the top like in
6752
		// SVG.
6753
		if (cvsCoord) {
6754
			sign *= -1; // canvas coordinates inverts the value
6755
			cvsOffset = axis.len;
6756
		}
6757
 
6758
		// Handle reversed axis
6759
		if (axis.reversed) {
6760
			sign *= -1;
6761
			cvsOffset -= sign * (axis.sector || axis.len);
6762
		}
6763
 
6764
		// From pixels to value
6765
		if (backwards) { // reverse translation
6766
 
6767
			val = val * sign + cvsOffset;
6768
			val -= minPixelPadding;
6769
			returnValue = val / localA + localMin; // from chart pixel to value
6770
			if (doPostTranslate) { // log and ordinal axes
6771
				returnValue = axis.lin2val(returnValue);
6772
			}
6773
 
6774
		// From value to pixels
6775
		} else {
6776
			if (doPostTranslate) { // log and ordinal axes
6777
				val = axis.val2lin(val);
6778
			}
6779
			if (pointPlacement === 'between') {
6780
				pointPlacement = 0.5;
6781
			}
6782
			returnValue = sign * (val - localMin) * localA + cvsOffset + (sign * minPixelPadding) +
6783
				(isNumber(pointPlacement) ? localA * pointPlacement * axis.pointRange : 0);
6784
		}
6785
 
6786
		return returnValue;
6787
	},
6788
 
6789
	/**
6790
	 * Utility method to translate an axis value to pixel position.
6791
	 * @param {Number} value A value in terms of axis units
6792
	 * @param {Boolean} paneCoordinates Whether to return the pixel coordinate relative to the chart
6793
	 *        or just the axis/pane itself.
6794
	 */
6795
	toPixels: function (value, paneCoordinates) {
6796
		return this.translate(value, false, !this.horiz, null, true) + (paneCoordinates ? 0 : this.pos);
6797
	},
6798
 
6799
	/*
6800
	 * Utility method to translate a pixel position in to an axis value
6801
	 * @param {Number} pixel The pixel value coordinate
6802
	 * @param {Boolean} paneCoordiantes Whether the input pixel is relative to the chart or just the
6803
	 *        axis/pane itself.
6804
	 */
6805
	toValue: function (pixel, paneCoordinates) {
6806
		return this.translate(pixel - (paneCoordinates ? 0 : this.pos), true, !this.horiz, null, true);
6807
	},
6808
 
6809
	/**
6810
	 * Create the path for a plot line that goes from the given value on
6811
	 * this axis, across the plot to the opposite side
6812
	 * @param {Number} value
6813
	 * @param {Number} lineWidth Used for calculation crisp line
6814
	 * @param {Number] old Use old coordinates (for resizing and rescaling)
6815
	 */
6816
	getPlotLinePath: function (value, lineWidth, old, force, translatedValue) {
6817
		var axis = this,
6818
			chart = axis.chart,
6819
			axisLeft = axis.left,
6820
			axisTop = axis.top,
6821
			x1,
6822
			y1,
6823
			x2,
6824
			y2,
6825
			cHeight = (old && chart.oldChartHeight) || chart.chartHeight,
6826
			cWidth = (old && chart.oldChartWidth) || chart.chartWidth,
6827
			skip,
6828
			transB = axis.transB,
6829
			/**
6830
			 * Check if x is between a and b. If not, either move to a/b or skip,
6831
			 * depending on the force parameter.
6832
			 */
6833
			between = function (x, a, b) {
6834
				if (x < a || x > b) {
6835
					if (force) {
6836
						x = mathMin(mathMax(a, x), b);
6837
					} else {
6838
						skip = true;
6839
					}
6840
				}
6841
				return x;
6842
			};
6843
 
6844
		translatedValue = pick(translatedValue, axis.translate(value, null, null, old));
6845
		x1 = x2 = mathRound(translatedValue + transB);
6846
		y1 = y2 = mathRound(cHeight - translatedValue - transB);
6847
 
6848
		if (isNaN(translatedValue)) { // no min or max
6849
			skip = true;
6850
 
6851
		} else if (axis.horiz) {
6852
			y1 = axisTop;
6853
			y2 = cHeight - axis.bottom;
6854
			x1 = x2 = between(x1, axisLeft, axisLeft + axis.width);
6855
		} else {
6856
			x1 = axisLeft;
6857
			x2 = cWidth - axis.right;
6858
			y1 = y2 = between(y1, axisTop, axisTop + axis.height);
6859
		}
6860
		return skip && !force ?
6861
			null :
6862
			chart.renderer.crispLine([M, x1, y1, L, x2, y2], lineWidth || 1);
6863
	},
6864
 
6865
	/**
6866
	 * Set the tick positions of a linear axis to round values like whole tens or every five.
6867
	 */
6868
	getLinearTickPositions: function (tickInterval, min, max) {
6869
		var pos,
6870
			lastPos,
6871
			roundedMin = correctFloat(mathFloor(min / tickInterval) * tickInterval),
6872
			roundedMax = correctFloat(mathCeil(max / tickInterval) * tickInterval),
6873
			tickPositions = [];
6874
 
6875
		// For single points, add a tick regardless of the relative position (#2662)
6876
		if (min === max && isNumber(min)) {
6877
			return [min];
6878
		}
6879
 
6880
		// Populate the intermediate values
6881
		pos = roundedMin;
6882
		while (pos <= roundedMax) {
6883
 
6884
			// Place the tick on the rounded value
6885
			tickPositions.push(pos);
6886
 
6887
			// Always add the raw tickInterval, not the corrected one.
6888
			pos = correctFloat(pos + tickInterval);
6889
 
6890
			// If the interval is not big enough in the current min - max range to actually increase
6891
			// the loop variable, we need to break out to prevent endless loop. Issue #619
6892
			if (pos === lastPos) {
6893
				break;
6894
			}
6895
 
6896
			// Record the last value
6897
			lastPos = pos;
6898
		}
6899
		return tickPositions;
6900
	},
6901
 
6902
	/**
6903
	 * Return the minor tick positions. For logarithmic axes, reuse the same logic
6904
	 * as for major ticks.
6905
	 */
6906
	getMinorTickPositions: function () {
6907
		var axis = this,
6908
			options = axis.options,
6909
			tickPositions = axis.tickPositions,
6910
			minorTickInterval = axis.minorTickInterval,
6911
			minorTickPositions = [],
6912
			pos,
6913
			i,
6914
			pointRangePadding = axis.pointRangePadding || 0,
6915
			min = axis.min - pointRangePadding, // #1498
6916
			max = axis.max + pointRangePadding, // #1498
6917
			range = max - min,
6918
			len;
6919
 
6920
		// If minor ticks get too dense, they are hard to read, and may cause long running script. So we don't draw them.
6921
		if (range && range / minorTickInterval < axis.len / 3) { // #3875
6922
 
6923
			if (axis.isLog) {
6924
				len = tickPositions.length;
6925
				for (i = 1; i < len; i++) {
6926
					minorTickPositions = minorTickPositions.concat(
6927
						axis.getLogTickPositions(minorTickInterval, tickPositions[i - 1], tickPositions[i], true)
6928
					);
6929
				}
6930
			} else if (axis.isDatetimeAxis && options.minorTickInterval === 'auto') { // #1314
6931
				minorTickPositions = minorTickPositions.concat(
6932
					axis.getTimeTicks(
6933
						axis.normalizeTimeTickInterval(minorTickInterval),
6934
						min,
6935
						max,
6936
						options.startOfWeek
6937
					)
6938
				);
6939
			} else {
6940
				for (pos = min + (tickPositions[0] - min) % minorTickInterval; pos <= max; pos += minorTickInterval) {
6941
					minorTickPositions.push(pos);
6942
				}
6943
			}
6944
		}
6945
 
6946
		if(minorTickPositions.length !== 0) { // don't change the extremes, when there is no minor ticks
6947
			axis.trimTicks(minorTickPositions, options.startOnTick, options.endOnTick); // #3652 #3743 #1498
6948
		}
6949
		return minorTickPositions;
6950
	},
6951
 
6952
	/**
6953
	 * Adjust the min and max for the minimum range. Keep in mind that the series data is
6954
	 * not yet processed, so we don't have information on data cropping and grouping, or
6955
	 * updated axis.pointRange or series.pointRange. The data can't be processed until
6956
	 * we have finally established min and max.
6957
	 */
6958
	adjustForMinRange: function () {
6959
		var axis = this,
6960
			options = axis.options,
6961
			min = axis.min,
6962
			max = axis.max,
6963
			zoomOffset,
6964
			spaceAvailable = axis.dataMax - axis.dataMin >= axis.minRange,
6965
			closestDataRange,
6966
			i,
6967
			distance,
6968
			xData,
6969
			loopLength,
6970
			minArgs,
6971
			maxArgs,
6972
			minRange;
6973
 
6974
		// Set the automatic minimum range based on the closest point distance
6975
		if (axis.isXAxis && axis.minRange === UNDEFINED && !axis.isLog) {
6976
 
6977
			if (defined(options.min) || defined(options.max)) {
6978
				axis.minRange = null; // don't do this again
6979
 
6980
			} else {
6981
 
6982
				// Find the closest distance between raw data points, as opposed to
6983
				// closestPointRange that applies to processed points (cropped and grouped)
6984
				each(axis.series, function (series) {
6985
					xData = series.xData;
6986
					loopLength = series.xIncrement ? 1 : xData.length - 1;
6987
					for (i = loopLength; i > 0; i--) {
6988
						distance = xData[i] - xData[i - 1];
6989
						if (closestDataRange === UNDEFINED || distance < closestDataRange) {
6990
							closestDataRange = distance;
6991
						}
6992
					}
6993
				});
6994
				axis.minRange = mathMin(closestDataRange * 5, axis.dataMax - axis.dataMin);
6995
			}
6996
		}
6997
 
6998
		// if minRange is exceeded, adjust
6999
		if (max - min < axis.minRange) {
7000
			minRange = axis.minRange;
7001
			zoomOffset = (minRange - max + min) / 2;
7002
 
7003
			// if min and max options have been set, don't go beyond it
7004
			minArgs = [min - zoomOffset, pick(options.min, min - zoomOffset)];
7005
			if (spaceAvailable) { // if space is available, stay within the data range
7006
				minArgs[2] = axis.dataMin;
7007
			}
7008
			min = arrayMax(minArgs);
7009
 
7010
			maxArgs = [min + minRange, pick(options.max, min + minRange)];
7011
			if (spaceAvailable) { // if space is availabe, stay within the data range
7012
				maxArgs[2] = axis.dataMax;
7013
			}
7014
 
7015
			max = arrayMin(maxArgs);
7016
 
7017
			// now if the max is adjusted, adjust the min back
7018
			if (max - min < minRange) {
7019
				minArgs[0] = max - minRange;
7020
				minArgs[1] = pick(options.min, max - minRange);
7021
				min = arrayMax(minArgs);
7022
			}
7023
		}
7024
 
7025
		// Record modified extremes
7026
		axis.min = min;
7027
		axis.max = max;
7028
	},
7029
 
7030
	/**
7031
	 * Update translation information
7032
	 */
7033
	setAxisTranslation: function (saveOld) {
7034
		var axis = this,
7035
			range = axis.max - axis.min,
7036
			pointRange = axis.axisPointRange || 0,
7037
			closestPointRange,
7038
			minPointOffset = 0,
7039
			pointRangePadding = 0,
7040
			linkedParent = axis.linkedParent,
7041
			ordinalCorrection,
7042
			hasCategories = !!axis.categories,
7043
			transA = axis.transA,
7044
			isXAxis = axis.isXAxis;
7045
 
7046
		// Adjust translation for padding. Y axis with categories need to go through the same (#1784).
7047
		if (isXAxis || hasCategories || pointRange) {
7048
			if (linkedParent) {
7049
				minPointOffset = linkedParent.minPointOffset;
7050
				pointRangePadding = linkedParent.pointRangePadding;
7051
 
7052
			} else {
7053
				each(axis.series, function (series) {
7054
					var seriesPointRange = hasCategories ? 1 : (isXAxis ? series.pointRange : (axis.axisPointRange || 0)), // #2806
7055
						pointPlacement = series.options.pointPlacement,
7056
						seriesClosestPointRange = series.closestPointRange;
7057
 
7058
					pointRange = mathMax(pointRange, seriesPointRange);
7059
 
7060
					if (!axis.single) {
7061
						// minPointOffset is the value padding to the left of the axis in order to make
7062
						// room for points with a pointRange, typically columns. When the pointPlacement option
7063
						// is 'between' or 'on', this padding does not apply.
7064
						minPointOffset = mathMax(
7065
							minPointOffset,
7066
							isString(pointPlacement) ? 0 : seriesPointRange / 2
7067
						);
7068
 
7069
						// Determine the total padding needed to the length of the axis to make room for the
7070
						// pointRange. If the series' pointPlacement is 'on', no padding is added.
7071
						pointRangePadding = mathMax(
7072
							pointRangePadding,
7073
							pointPlacement === 'on' ? 0 : seriesPointRange
7074
						);
7075
					}
7076
 
7077
					// Set the closestPointRange
7078
					if (!series.noSharedTooltip && defined(seriesClosestPointRange)) {
7079
						closestPointRange = defined(closestPointRange) ?
7080
							mathMin(closestPointRange, seriesClosestPointRange) :
7081
							seriesClosestPointRange;
7082
					}
7083
				});
7084
			}
7085
 
7086
			// Record minPointOffset and pointRangePadding
7087
			ordinalCorrection = axis.ordinalSlope && closestPointRange ? axis.ordinalSlope / closestPointRange : 1; // #988, #1853
7088
			axis.minPointOffset = minPointOffset = minPointOffset * ordinalCorrection;
7089
			axis.pointRangePadding = pointRangePadding = pointRangePadding * ordinalCorrection;
7090
 
7091
			// pointRange means the width reserved for each point, like in a column chart
7092
			axis.pointRange = mathMin(pointRange, range);
7093
 
7094
			// closestPointRange means the closest distance between points. In columns
7095
			// it is mostly equal to pointRange, but in lines pointRange is 0 while closestPointRange
7096
			// is some other value
7097
			if (isXAxis) {
7098
				axis.closestPointRange = closestPointRange;
7099
			}
7100
		}
7101
 
7102
		// Secondary values
7103
		if (saveOld) {
7104
			axis.oldTransA = transA;
7105
		}
7106
		axis.translationSlope = axis.transA = transA = axis.len / ((range + pointRangePadding) || 1);
7107
		axis.transB = axis.horiz ? axis.left : axis.bottom; // translation addend
7108
		axis.minPixelPadding = transA * minPointOffset;
7109
	},
7110
 
7111
	minFromRange: function () {
7112
		return this.max - this.range;
7113
	},
7114
 
7115
	/**
7116
	 * Set the tick positions to round values and optionally extend the extremes
7117
	 * to the nearest tick
7118
	 */
7119
	setTickInterval: function (secondPass) {
7120
		var axis = this,
7121
			chart = axis.chart,
7122
			options = axis.options,
7123
			isLog = axis.isLog,
7124
			isDatetimeAxis = axis.isDatetimeAxis,
7125
			isXAxis = axis.isXAxis,
7126
			isLinked = axis.isLinked,
7127
			maxPadding = options.maxPadding,
7128
			minPadding = options.minPadding,
7129
			length,
7130
			linkedParentExtremes,
7131
			tickIntervalOption = options.tickInterval,
7132
			minTickInterval,
7133
			tickPixelIntervalOption = options.tickPixelInterval,
7134
			categories = axis.categories,
7135
			threshold = axis.threshold,
7136
			softThreshold = axis.softThreshold,
7137
			thresholdMin,
7138
			thresholdMax,
7139
			hardMin,
7140
			hardMax;
7141
 
7142
		if (!isDatetimeAxis && !categories && !isLinked) {
7143
			this.getTickAmount();
7144
		}
7145
 
7146
		// Min or max set either by zooming/setExtremes or initial options
7147
		hardMin = pick(axis.userMin, options.min);
7148
		hardMax = pick(axis.userMax, options.max);
7149
 
7150
		// Linked axis gets the extremes from the parent axis
7151
		if (isLinked) {
7152
			axis.linkedParent = chart[axis.coll][options.linkedTo];
7153
			linkedParentExtremes = axis.linkedParent.getExtremes();
7154
			axis.min = pick(linkedParentExtremes.min, linkedParentExtremes.dataMin);
7155
			axis.max = pick(linkedParentExtremes.max, linkedParentExtremes.dataMax);
7156
			if (options.type !== axis.linkedParent.options.type) {
7157
				error(11, 1); // Can't link axes of different type
7158
			}
7159
 
7160
		// Initial min and max from the extreme data values
7161
		} else {
7162
 
7163
			// Adjust to hard threshold
7164
			if (!softThreshold && defined(threshold)) {
7165
				if (axis.dataMin >= threshold) {
7166
					thresholdMin = threshold;
7167
					minPadding = 0;
7168
				} else if (axis.dataMax <= threshold) {
7169
					thresholdMax = threshold;
7170
					maxPadding = 0;
7171
				}
7172
			}
7173
 
7174
			axis.min = pick(hardMin, thresholdMin, axis.dataMin);
7175
			axis.max = pick(hardMax, thresholdMax, axis.dataMax);
7176
 
7177
		}
7178
 
7179
		if (isLog) {
7180
			if (!secondPass && mathMin(axis.min, pick(axis.dataMin, axis.min)) <= 0) { // #978
7181
				error(10, 1); // Can't plot negative values on log axis
7182
			}
7183
			// The correctFloat cures #934, float errors on full tens. But it
7184
			// was too aggressive for #4360 because of conversion back to lin,
7185
			// therefore use precision 15.
7186
			axis.min = correctFloat(log2lin(axis.min), 15);
7187
			axis.max = correctFloat(log2lin(axis.max), 15);
7188
		}
7189
 
7190
		// handle zoomed range
7191
		if (axis.range && defined(axis.max)) {
7192
			axis.userMin = axis.min = hardMin = mathMax(axis.min, axis.minFromRange()); // #618
7193
			axis.userMax = hardMax = axis.max;
7194
 
7195
			axis.range = null;  // don't use it when running setExtremes
7196
		}
7197
 
7198
		// Hook for adjusting this.min and this.max. Used by bubble series.
7199
		if (axis.beforePadding) {
7200
			axis.beforePadding();
7201
		}
7202
 
7203
		// adjust min and max for the minimum range
7204
		axis.adjustForMinRange();
7205
 
7206
		// Pad the values to get clear of the chart's edges. To avoid tickInterval taking the padding
7207
		// into account, we do this after computing tick interval (#1337).
7208
		if (!categories && !axis.axisPointRange && !axis.usePercentage && !isLinked && defined(axis.min) && defined(axis.max)) {
7209
			length = axis.max - axis.min;
7210
			if (length) {
7211
				if (!defined(hardMin) && minPadding) {
7212
					axis.min -= length * minPadding;
7213
				}
7214
				if (!defined(hardMax)  && maxPadding) {
7215
					axis.max += length * maxPadding;
7216
				}
7217
			}
7218
		}
7219
 
7220
		// Stay within floor and ceiling
7221
		if (isNumber(options.floor)) {
7222
			axis.min = mathMax(axis.min, options.floor);
7223
		}
7224
		if (isNumber(options.ceiling)) {
7225
			axis.max = mathMin(axis.max, options.ceiling);
7226
		}
7227
 
7228
		// When the threshold is soft, adjust the extreme value only if
7229
		// the data extreme and the padded extreme land on either side of the threshold. For example,
7230
		// a series of [0, 1, 2, 3] would make the yAxis add a tick for -1 because of the
7231
		// default minPadding and startOnTick options. This is prevented by the softThreshold
7232
		// option.
7233
		if (softThreshold && defined(axis.dataMin)) {
7234
			threshold = threshold || 0;
7235
			if (!defined(hardMin) && axis.min < threshold && axis.dataMin >= threshold) {
7236
				axis.min = threshold;
7237
			} else if (!defined(hardMax) && axis.max > threshold && axis.dataMax <= threshold) {
7238
				axis.max = threshold;
7239
			}
7240
		}
7241
 
7242
 
7243
		// get tickInterval
7244
		if (axis.min === axis.max || axis.min === undefined || axis.max === undefined) {
7245
			axis.tickInterval = 1;
7246
		} else if (isLinked && !tickIntervalOption &&
7247
				tickPixelIntervalOption === axis.linkedParent.options.tickPixelInterval) {
7248
			axis.tickInterval = tickIntervalOption = axis.linkedParent.tickInterval;
7249
		} else {
7250
			axis.tickInterval = pick(
7251
				tickIntervalOption,
7252
				this.tickAmount ? ((axis.max - axis.min) / mathMax(this.tickAmount - 1, 1)) : undefined,
7253
				categories ? // for categoried axis, 1 is default, for linear axis use tickPix
7254
					1 :
7255
					// don't let it be more than the data range
7256
					(axis.max - axis.min) * tickPixelIntervalOption / mathMax(axis.len, tickPixelIntervalOption)
7257
			);
7258
		}
7259
 
7260
		// Now we're finished detecting min and max, crop and group series data. This
7261
		// is in turn needed in order to find tick positions in ordinal axes.
7262
		if (isXAxis && !secondPass) {
7263
			each(axis.series, function (series) {
7264
				series.processData(axis.min !== axis.oldMin || axis.max !== axis.oldMax);
7265
			});
7266
		}
7267
 
7268
		// set the translation factor used in translate function
7269
		axis.setAxisTranslation(true);
7270
 
7271
		// hook for ordinal axes and radial axes
7272
		if (axis.beforeSetTickPositions) {
7273
			axis.beforeSetTickPositions();
7274
		}
7275
 
7276
		// hook for extensions, used in Highstock ordinal axes
7277
		if (axis.postProcessTickInterval) {
7278
			axis.tickInterval = axis.postProcessTickInterval(axis.tickInterval);
7279
		}
7280
 
7281
		// In column-like charts, don't cramp in more ticks than there are points (#1943)
7282
		if (axis.pointRange) {
7283
			axis.tickInterval = mathMax(axis.pointRange, axis.tickInterval);
7284
		}
7285
 
7286
		// Before normalizing the tick interval, handle minimum tick interval. This applies only if tickInterval is not defined.
7287
		minTickInterval = pick(options.minTickInterval, axis.isDatetimeAxis && axis.closestPointRange);
7288
		if (!tickIntervalOption && axis.tickInterval < minTickInterval) {
7289
			axis.tickInterval = minTickInterval;
7290
		}
7291
 
7292
		// for linear axes, get magnitude and normalize the interval
7293
		if (!isDatetimeAxis && !isLog && !tickIntervalOption) {
7294
			axis.tickInterval = normalizeTickInterval(
7295
				axis.tickInterval,
7296
				null,
7297
				getMagnitude(axis.tickInterval),
7298
				// If the tick interval is between 0.5 and 5 and the axis max is in the order of
7299
				// thousands, chances are we are dealing with years. Don't allow decimals. #3363.
7300
				pick(options.allowDecimals, !(axis.tickInterval > 0.5 && axis.tickInterval < 5 && axis.max > 1000 && axis.max < 9999)),
7301
				!!this.tickAmount
7302
			);
7303
		}
7304
 
7305
		// Prevent ticks from getting so close that we can't draw the labels
7306
		if (!this.tickAmount && this.len) { // Color axis with disabled legend has no length
7307
			axis.tickInterval = axis.unsquish();
7308
		}
7309
 
7310
		this.setTickPositions();
7311
	},
7312
 
7313
	/**
7314
	 * Now we have computed the normalized tickInterval, get the tick positions
7315
	 */
7316
	setTickPositions: function () {
7317
 
7318
		var options = this.options,
7319
			tickPositions,
7320
			tickPositionsOption = options.tickPositions,
7321
			tickPositioner = options.tickPositioner,
7322
			startOnTick = options.startOnTick,
7323
			endOnTick = options.endOnTick,
7324
			single;
7325
 
7326
		// Set the tickmarkOffset
7327
		this.tickmarkOffset = (this.categories && options.tickmarkPlacement === 'between' &&
7328
			this.tickInterval === 1) ? 0.5 : 0; // #3202
7329
 
7330
 
7331
		// get minorTickInterval
7332
		this.minorTickInterval = options.minorTickInterval === 'auto' && this.tickInterval ?
7333
			this.tickInterval / 5 : options.minorTickInterval;
7334
 
7335
		// Find the tick positions
7336
		this.tickPositions = tickPositions = tickPositionsOption && tickPositionsOption.slice(); // Work on a copy (#1565)
7337
		if (!tickPositions) {
7338
 
7339
			if (this.isDatetimeAxis) {
7340
				tickPositions = this.getTimeTicks(
7341
					this.normalizeTimeTickInterval(this.tickInterval, options.units),
7342
					this.min,
7343
					this.max,
7344
					options.startOfWeek,
7345
					this.ordinalPositions,
7346
					this.closestPointRange,
7347
					true
7348
				);
7349
			} else if (this.isLog) {
7350
				tickPositions = this.getLogTickPositions(this.tickInterval, this.min, this.max);
7351
			} else {
7352
				tickPositions = this.getLinearTickPositions(this.tickInterval, this.min, this.max);
7353
			}
7354
 
7355
			// Too dense ticks, keep only the first and last (#4477)
7356
			if (tickPositions.length > this.len) {
7357
				tickPositions = [tickPositions[0], tickPositions.pop()];
7358
			}
7359
 
7360
			this.tickPositions = tickPositions;
7361
 
7362
			// Run the tick positioner callback, that allows modifying auto tick positions.
7363
			if (tickPositioner) {
7364
				tickPositioner = tickPositioner.apply(this, [this.min, this.max]);
7365
				if (tickPositioner) {
7366
					this.tickPositions = tickPositions = tickPositioner;
7367
				}
7368
			}
7369
 
7370
		}
7371
 
7372
		if (!this.isLinked) {
7373
 
7374
			// reset min/max or remove extremes based on start/end on tick
7375
			this.trimTicks(tickPositions, startOnTick, endOnTick);
7376
 
7377
			// When there is only one point, or all points have the same value on this axis, then min
7378
			// and max are equal and tickPositions.length is 0 or 1. In this case, add some padding
7379
			// in order to center the point, but leave it with one tick. #1337.
7380
			if (this.min === this.max && defined(this.min) && !this.tickAmount) {
7381
				// Substract half a unit (#2619, #2846, #2515, #3390)
7382
				single = true;
7383
				this.min -= 0.5;
7384
				this.max += 0.5;
7385
			}
7386
			this.single = single;
7387
 
7388
			if (!tickPositionsOption && !tickPositioner) {
7389
				this.adjustTickAmount();
7390
			}
7391
		}
7392
	},
7393
 
7394
	/**
7395
	 * Handle startOnTick and endOnTick by either adapting to padding min/max or rounded min/max
7396
	 */
7397
	trimTicks: function (tickPositions, startOnTick, endOnTick) {
7398
		var roundedMin = tickPositions[0],
7399
			roundedMax = tickPositions[tickPositions.length - 1],
7400
			minPointOffset = this.minPointOffset || 0;
7401
 
7402
		if (startOnTick) {
7403
			this.min = roundedMin;
7404
		} else if (this.min - minPointOffset > roundedMin) {
7405
			tickPositions.shift();
7406
		}
7407
 
7408
		if (endOnTick) {
7409
			this.max = roundedMax;
7410
		} else if (this.max + minPointOffset < roundedMax) {
7411
			tickPositions.pop();
7412
		}
7413
 
7414
		// If no tick are left, set one tick in the middle (#3195)
7415
		if (tickPositions.length === 0 && defined(roundedMin)) {
7416
			tickPositions.push((roundedMax + roundedMin) / 2);
7417
		}
7418
	},
7419
 
7420
	/**
7421
	 * Set the max ticks of either the x and y axis collection
7422
	 */
7423
	getTickAmount: function () {
7424
		var others = {}, // Whether there is another axis to pair with this one
7425
			hasOther,
7426
			options = this.options,
7427
			tickAmount = options.tickAmount,
7428
			tickPixelInterval = options.tickPixelInterval;
7429
 
7430
		if (!defined(options.tickInterval) && this.len < tickPixelInterval && !this.isRadial &&
7431
				!this.isLog && options.startOnTick && options.endOnTick) {
7432
			tickAmount = 2;
7433
		}
7434
 
7435
		if (!tickAmount && this.chart.options.chart.alignTicks !== false && options.alignTicks !== false) {
7436
			// Check if there are multiple axes in the same pane
7437
			each(this.chart[this.coll], function (axis) {
7438
				var options = axis.options,
7439
					horiz = axis.horiz,
7440
					key = [horiz ? options.left : options.top, horiz ? options.width : options.height, options.pane].join(',');
7441
 
7442
				if (axis.series.length) { // #4442
7443
					if (others[key]) {
7444
						hasOther = true; // #4201
7445
					} else {
7446
						others[key] = 1;
7447
					}
7448
				}
7449
			});
7450
 
7451
			if (hasOther) {
7452
				// Add 1 because 4 tick intervals require 5 ticks (including first and last)
7453
				tickAmount = mathCeil(this.len / tickPixelInterval) + 1;
7454
			}
7455
		}
7456
 
7457
		// For tick amounts of 2 and 3, compute five ticks and remove the intermediate ones. This
7458
		// prevents the axis from adding ticks that are too far away from the data extremes.
7459
		if (tickAmount < 4) {
7460
			this.finalTickAmt = tickAmount;
7461
			tickAmount = 5;
7462
		}
7463
 
7464
		this.tickAmount = tickAmount;
7465
	},
7466
 
7467
	/**
7468
	 * When using multiple axes, adjust the number of ticks to match the highest
7469
	 * number of ticks in that group
7470
	 */
7471
	adjustTickAmount: function () {
7472
		var tickInterval = this.tickInterval,
7473
			tickPositions = this.tickPositions,
7474
			tickAmount = this.tickAmount,
7475
			finalTickAmt = this.finalTickAmt,
7476
			currentTickAmount = tickPositions && tickPositions.length,
7477
			i,
7478
			len;
7479
 
7480
		if (currentTickAmount < tickAmount) { // TODO: Check #3411
7481
			while (tickPositions.length < tickAmount) {
7482
				tickPositions.push(correctFloat(
7483
					tickPositions[tickPositions.length - 1] + tickInterval
7484
				));
7485
			}
7486
			this.transA *= (currentTickAmount - 1) / (tickAmount - 1);
7487
			this.max = tickPositions[tickPositions.length - 1];
7488
 
7489
		// We have too many ticks, run second pass to try to reduce ticks
7490
		} else if (currentTickAmount > tickAmount) {
7491
			this.tickInterval *= 2;
7492
			this.setTickPositions();
7493
		}
7494
 
7495
		// The finalTickAmt property is set in getTickAmount
7496
		if (defined(finalTickAmt)) {
7497
			i = len = tickPositions.length;
7498
			while (i--) {
7499
				if (
7500
					(finalTickAmt === 3 && i % 2 === 1) || // Remove every other tick
7501
					(finalTickAmt <= 2 && i > 0 && i < len - 1) // Remove all but first and last
7502
				) {
7503
					tickPositions.splice(i, 1);
7504
				}
7505
			}
7506
			this.finalTickAmt = UNDEFINED;
7507
		}
7508
	},
7509
 
7510
	/**
7511
	 * Set the scale based on data min and max, user set min and max or options
7512
	 *
7513
	 */
7514
	setScale: function () {
7515
		var axis = this,
7516
			isDirtyData,
7517
			isDirtyAxisLength;
7518
 
7519
		axis.oldMin = axis.min;
7520
		axis.oldMax = axis.max;
7521
		axis.oldAxisLength = axis.len;
7522
 
7523
		// set the new axisLength
7524
		axis.setAxisSize();
7525
		//axisLength = horiz ? axisWidth : axisHeight;
7526
		isDirtyAxisLength = axis.len !== axis.oldAxisLength;
7527
 
7528
		// is there new data?
7529
		each(axis.series, function (series) {
7530
			if (series.isDirtyData || series.isDirty ||
7531
					series.xAxis.isDirty) { // when x axis is dirty, we need new data extremes for y as well
7532
				isDirtyData = true;
7533
			}
7534
		});
7535
 
7536
		// do we really need to go through all this?
7537
		if (isDirtyAxisLength || isDirtyData || axis.isLinked || axis.forceRedraw ||
7538
			axis.userMin !== axis.oldUserMin || axis.userMax !== axis.oldUserMax) {
7539
 
7540
			if (axis.resetStacks) {
7541
				axis.resetStacks();
7542
			}
7543
 
7544
			axis.forceRedraw = false;
7545
 
7546
			// get data extremes if needed
7547
			axis.getSeriesExtremes();
7548
 
7549
			// get fixed positions based on tickInterval
7550
			axis.setTickInterval();
7551
 
7552
			// record old values to decide whether a rescale is necessary later on (#540)
7553
			axis.oldUserMin = axis.userMin;
7554
			axis.oldUserMax = axis.userMax;
7555
 
7556
			// Mark as dirty if it is not already set to dirty and extremes have changed. #595.
7557
			if (!axis.isDirty) {
7558
				axis.isDirty = isDirtyAxisLength || axis.min !== axis.oldMin || axis.max !== axis.oldMax;
7559
			}
7560
		} else if (axis.cleanStacks) {
7561
			axis.cleanStacks();
7562
		}
7563
	},
7564
 
7565
	/**
7566
	 * Set the extremes and optionally redraw
7567
	 * @param {Number} newMin
7568
	 * @param {Number} newMax
7569
	 * @param {Boolean} redraw
7570
	 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
7571
	 *    configuration
7572
	 * @param {Object} eventArguments
7573
	 *
7574
	 */
7575
	setExtremes: function (newMin, newMax, redraw, animation, eventArguments) {
7576
		var axis = this,
7577
			chart = axis.chart;
7578
 
7579
		redraw = pick(redraw, true); // defaults to true
7580
 
7581
		each(axis.series, function (serie) {
7582
			delete serie.kdTree;
7583
		});
7584
 
7585
		// Extend the arguments with min and max
7586
		eventArguments = extend(eventArguments, {
7587
			min: newMin,
7588
			max: newMax
7589
		});
7590
 
7591
		// Fire the event
7592
		fireEvent(axis, 'setExtremes', eventArguments, function () { // the default event handler
7593
 
7594
			axis.userMin = newMin;
7595
			axis.userMax = newMax;
7596
			axis.eventArgs = eventArguments;
7597
 
7598
			if (redraw) {
7599
				chart.redraw(animation);
7600
			}
7601
		});
7602
	},
7603
 
7604
	/**
7605
	 * Overridable method for zooming chart. Pulled out in a separate method to allow overriding
7606
	 * in stock charts.
7607
	 */
7608
	zoom: function (newMin, newMax) {
7609
		var dataMin = this.dataMin,
7610
			dataMax = this.dataMax,
7611
			options = this.options,
7612
			min = mathMin(dataMin, pick(options.min, dataMin)),
7613
			max = mathMax(dataMax, pick(options.max, dataMax));
7614
 
7615
		// Prevent pinch zooming out of range. Check for defined is for #1946. #1734.
7616
		if (!this.allowZoomOutside) {
7617
			if (defined(dataMin) && newMin <= min) {
7618
				newMin = min;
7619
			}
7620
			if (defined(dataMax) && newMax >= max) {
7621
				newMax = max;
7622
			}
7623
		}
7624
 
7625
		// In full view, displaying the reset zoom button is not required
7626
		this.displayBtn = newMin !== UNDEFINED || newMax !== UNDEFINED;
7627
 
7628
		// Do it
7629
		this.setExtremes(
7630
			newMin,
7631
			newMax,
7632
			false,
7633
			UNDEFINED,
7634
			{ trigger: 'zoom' }
7635
		);
7636
		return true;
7637
	},
7638
 
7639
	/**
7640
	 * Update the axis metrics
7641
	 */
7642
	setAxisSize: function () {
7643
		var chart = this.chart,
7644
			options = this.options,
7645
			offsetLeft = options.offsetLeft || 0,
7646
			offsetRight = options.offsetRight || 0,
7647
			horiz = this.horiz,
7648
			width = pick(options.width, chart.plotWidth - offsetLeft + offsetRight),
7649
			height = pick(options.height, chart.plotHeight),
7650
			top = pick(options.top, chart.plotTop),
7651
			left = pick(options.left, chart.plotLeft + offsetLeft),
7652
			percentRegex = /%$/;
7653
 
7654
		// Check for percentage based input values
7655
		if (percentRegex.test(height)) {
7656
			height = parseFloat(height) / 100 * chart.plotHeight;
7657
		}
7658
		if (percentRegex.test(top)) {
7659
			top = parseFloat(top) / 100 * chart.plotHeight + chart.plotTop;
7660
		}
7661
 
7662
		// Expose basic values to use in Series object and navigator
7663
		this.left = left;
7664
		this.top = top;
7665
		this.width = width;
7666
		this.height = height;
7667
		this.bottom = chart.chartHeight - height - top;
7668
		this.right = chart.chartWidth - width - left;
7669
 
7670
		// Direction agnostic properties
7671
		this.len = mathMax(horiz ? width : height, 0); // mathMax fixes #905
7672
		this.pos = horiz ? left : top; // distance from SVG origin
7673
	},
7674
 
7675
	/**
7676
	 * Get the actual axis extremes
7677
	 */
7678
	getExtremes: function () {
7679
		var axis = this,
7680
			isLog = axis.isLog;
7681
 
7682
		return {
7683
			min: isLog ? correctFloat(lin2log(axis.min)) : axis.min,
7684
			max: isLog ? correctFloat(lin2log(axis.max)) : axis.max,
7685
			dataMin: axis.dataMin,
7686
			dataMax: axis.dataMax,
7687
			userMin: axis.userMin,
7688
			userMax: axis.userMax
7689
		};
7690
	},
7691
 
7692
	/**
7693
	 * Get the zero plane either based on zero or on the min or max value.
7694
	 * Used in bar and area plots
7695
	 */
7696
	getThreshold: function (threshold) {
7697
		var axis = this,
7698
			isLog = axis.isLog,
7699
			realMin = isLog ? lin2log(axis.min) : axis.min,
7700
			realMax = isLog ? lin2log(axis.max) : axis.max;
7701
 
7702
		// With a threshold of null, make the columns/areas rise from the top or bottom
7703
		// depending on the value, assuming an actual threshold of 0 (#4233).
7704
		if (threshold === null) {
7705
			threshold = realMax < 0 ? realMax : realMin;
7706
		} else if (realMin > threshold) {
7707
			threshold = realMin;
7708
		} else if (realMax < threshold) {
7709
			threshold = realMax;
7710
		}
7711
 
7712
		return axis.translate(threshold, 0, 1, 0, 1);
7713
	},
7714
 
7715
	/**
7716
	 * Compute auto alignment for the axis label based on which side the axis is on
7717
	 * and the given rotation for the label
7718
	 */
7719
	autoLabelAlign: function (rotation) {
7720
		var ret,
7721
			angle = (pick(rotation, 0) - (this.side * 90) + 720) % 360;
7722
 
7723
		if (angle > 15 && angle < 165) {
7724
			ret = 'right';
7725
		} else if (angle > 195 && angle < 345) {
7726
			ret = 'left';
7727
		} else {
7728
			ret = 'center';
7729
		}
7730
		return ret;
7731
	},
7732
 
7733
	/**
7734
	 * Prevent the ticks from getting so close we can't draw the labels. On a horizontal
7735
	 * axis, this is handled by rotating the labels, removing ticks and adding ellipsis.
7736
	 * On a vertical axis remove ticks and add ellipsis.
7737
	 */
7738
	unsquish: function () {
7739
		var chart = this.chart,
7740
			ticks = this.ticks,
7741
			labelOptions = this.options.labels,
7742
			horiz = this.horiz,
7743
			tickInterval = this.tickInterval,
7744
			newTickInterval = tickInterval,
7745
			slotSize = this.len / (((this.categories ? 1 : 0) + this.max - this.min) / tickInterval),
7746
			rotation,
7747
			rotationOption = labelOptions.rotation,
7748
			labelMetrics = chart.renderer.fontMetrics(labelOptions.style.fontSize, ticks[0] && ticks[0].label),
7749
			step,
7750
			bestScore = Number.MAX_VALUE,
7751
			autoRotation,
7752
			// Return the multiple of tickInterval that is needed to avoid collision
7753
			getStep = function (spaceNeeded) {
7754
				var step = spaceNeeded / (slotSize || 1);
7755
				step = step > 1 ? mathCeil(step) : 1;
7756
				return step * tickInterval;
7757
			};
7758
 
7759
		if (horiz) {
7760
			autoRotation = !labelOptions.staggerLines && !labelOptions.step && ( // #3971
7761
				defined(rotationOption) ?
7762
					[rotationOption] :
7763
					slotSize < pick(labelOptions.autoRotationLimit, 80) && labelOptions.autoRotation
7764
			);
7765
 
7766
			if (autoRotation) {
7767
 
7768
				// Loop over the given autoRotation options, and determine which gives the best score. The
7769
				// best score is that with the lowest number of steps and a rotation closest to horizontal.
7770
				each(autoRotation, function (rot) {
7771
					var score;
7772
 
7773
					if (rot === rotationOption || (rot && rot >= -90 && rot <= 90)) { // #3891
7774
 
7775
						step = getStep(mathAbs(labelMetrics.h / mathSin(deg2rad * rot)));
7776
 
7777
						score = step + mathAbs(rot / 360);
7778
 
7779
						if (score < bestScore) {
7780
							bestScore = score;
7781
							rotation = rot;
7782
							newTickInterval = step;
7783
						}
7784
					}
7785
				});
7786
			}
7787
 
7788
		} else if (!labelOptions.step) { // #4411
7789
			newTickInterval = getStep(labelMetrics.h);
7790
		}
7791
 
7792
		this.autoRotation = autoRotation;
7793
		this.labelRotation = pick(rotation, rotationOption);
7794
 
7795
		return newTickInterval;
7796
	},
7797
 
7798
	renderUnsquish: function () {
7799
		var chart = this.chart,
7800
			renderer = chart.renderer,
7801
			tickPositions = this.tickPositions,
7802
			ticks = this.ticks,
7803
			labelOptions = this.options.labels,
7804
			horiz = this.horiz,
7805
			margin = chart.margin,
7806
			slotCount = this.categories ? tickPositions.length : tickPositions.length - 1,
7807
			slotWidth = this.slotWidth = (horiz && !labelOptions.step && !labelOptions.rotation &&
7808
				((this.staggerLines || 1) * chart.plotWidth) / slotCount) ||
7809
				(!horiz && ((margin[3] && (margin[3] - chart.spacing[3])) || chart.chartWidth * 0.33)), // #1580, #1931,
7810
			innerWidth = mathMax(1, mathRound(slotWidth - 2 * (labelOptions.padding || 5))),
7811
			attr = {},
7812
			labelMetrics = renderer.fontMetrics(labelOptions.style.fontSize, ticks[0] && ticks[0].label),
7813
			textOverflowOption = labelOptions.style.textOverflow,
7814
			css,
7815
			labelLength = 0,
7816
			label,
7817
			i,
7818
			pos;
7819
 
7820
		// Set rotation option unless it is "auto", like in gauges
7821
		if (!isString(labelOptions.rotation)) {
7822
			attr.rotation = labelOptions.rotation || 0; // #4443
7823
		}
7824
 
7825
		// Handle auto rotation on horizontal axis
7826
		if (this.autoRotation) {
7827
 
7828
			// Get the longest label length
7829
			each(tickPositions, function (tick) {
7830
				tick = ticks[tick];
7831
				if (tick && tick.labelLength > labelLength) {
7832
					labelLength = tick.labelLength;
7833
				}
7834
			});
7835
 
7836
			// Apply rotation only if the label is too wide for the slot, and
7837
			// the label is wider than its height.
7838
			if (labelLength > innerWidth && labelLength > labelMetrics.h) {
7839
				attr.rotation = this.labelRotation;
7840
			} else {
7841
				this.labelRotation = 0;
7842
			}
7843
 
7844
		// Handle word-wrap or ellipsis on vertical axis
7845
		} else if (slotWidth) {
7846
			// For word-wrap or ellipsis
7847
			css = { width: innerWidth + PX };
7848
 
7849
			if (!textOverflowOption) {
7850
				css.textOverflow = 'clip';
7851
 
7852
				// On vertical axis, only allow word wrap if there is room for more lines.
7853
				i = tickPositions.length;
7854
				while (!horiz && i--) {
7855
					pos = tickPositions[i];
7856
					label = ticks[pos].label;
7857
					if (label) {
7858
						// Reset ellipsis in order to get the correct bounding box (#4070)
7859
						if (label.styles.textOverflow === 'ellipsis') {
7860
							label.css({ textOverflow: 'clip' });
7861
						}
7862
						if (label.getBBox().height > this.len / tickPositions.length - (labelMetrics.h - labelMetrics.f)) {
7863
							label.specCss = { textOverflow: 'ellipsis' };
7864
						}
7865
					}
7866
				}
7867
			}
7868
		}
7869
 
7870
 
7871
		// Add ellipsis if the label length is significantly longer than ideal
7872
		if (attr.rotation) {
7873
			css = {
7874
				width: (labelLength > chart.chartHeight * 0.5 ? chart.chartHeight * 0.33 : chart.chartHeight) + PX
7875
			};
7876
			if (!textOverflowOption) {
7877
				css.textOverflow = 'ellipsis';
7878
			}
7879
		}
7880
 
7881
		// Set the explicit or automatic label alignment
7882
		this.labelAlign = attr.align = labelOptions.align || this.autoLabelAlign(this.labelRotation);
7883
 
7884
		// Apply general and specific CSS
7885
		each(tickPositions, function (pos) {
7886
			var tick = ticks[pos],
7887
				label = tick && tick.label;
7888
			if (label) {
7889
				label.attr(attr); // This needs to go before the CSS in old IE (#4502)
7890
				if (css) {
7891
					label.css(merge(css, label.specCss));
7892
				}
7893
				delete label.specCss;
7894
				tick.rotation = attr.rotation;
7895
			}
7896
		});
7897
 
7898
		// TODO: Why not part of getLabelPosition?
7899
		this.tickRotCorr = renderer.rotCorr(labelMetrics.b, this.labelRotation || 0, this.side === 2);
7900
	},
7901
 
7902
	/**
7903
	 * Return true if the axis has associated data
7904
	 */
7905
	hasData: function () {
7906
		return this.hasVisibleSeries || (defined(this.min) && defined(this.max) && !!this.tickPositions);
7907
	},
7908
 
7909
	/**
7910
	 * Render the tick labels to a preliminary position to get their sizes
7911
	 */
7912
	getOffset: function () {
7913
		var axis = this,
7914
			chart = axis.chart,
7915
			renderer = chart.renderer,
7916
			options = axis.options,
7917
			tickPositions = axis.tickPositions,
7918
			ticks = axis.ticks,
7919
			horiz = axis.horiz,
7920
			side = axis.side,
7921
			invertedSide = chart.inverted ? [1, 0, 3, 2][side] : side,
7922
			hasData,
7923
			showAxis,
7924
			titleOffset = 0,
7925
			titleOffsetOption,
7926
			titleMargin = 0,
7927
			axisTitleOptions = options.title,
7928
			labelOptions = options.labels,
7929
			labelOffset = 0, // reset
7930
			labelOffsetPadded,
7931
			axisOffset = chart.axisOffset,
7932
			clipOffset = chart.clipOffset,
7933
			clip,
7934
			directionFactor = [-1, 1, 1, -1][side],
7935
			n,
7936
			axisParent = axis.axisParent, // Used in color axis
7937
			lineHeightCorrection;
7938
 
7939
		// For reuse in Axis.render
7940
		hasData = axis.hasData();
7941
		axis.showAxis = showAxis = hasData || pick(options.showEmpty, true);
7942
 
7943
		// Set/reset staggerLines
7944
		axis.staggerLines = axis.horiz && labelOptions.staggerLines;
7945
 
7946
		// Create the axisGroup and gridGroup elements on first iteration
7947
		if (!axis.axisGroup) {
7948
			axis.gridGroup = renderer.g('grid')
7949
				.attr({ zIndex: options.gridZIndex || 1 })
7950
				.add(axisParent);
7951
			axis.axisGroup = renderer.g('axis')
7952
				.attr({ zIndex: options.zIndex || 2 })
7953
				.add(axisParent);
7954
			axis.labelGroup = renderer.g('axis-labels')
7955
				.attr({ zIndex: labelOptions.zIndex || 7 })
7956
				.addClass(PREFIX + axis.coll.toLowerCase() + '-labels')
7957
				.add(axisParent);
7958
		}
7959
 
7960
		if (hasData || axis.isLinked) {
7961
 
7962
			// Generate ticks
7963
			each(tickPositions, function (pos) {
7964
				if (!ticks[pos]) {
7965
					ticks[pos] = new Tick(axis, pos);
7966
				} else {
7967
					ticks[pos].addLabel(); // update labels depending on tick interval
7968
				}
7969
			});
7970
 
7971
			axis.renderUnsquish();
7972
 
7973
			each(tickPositions, function (pos) {
7974
				// left side must be align: right and right side must have align: left for labels
7975
				if (side === 0 || side === 2 || { 1: 'left', 3: 'right' }[side] === axis.labelAlign) {
7976
 
7977
					// get the highest offset
7978
					labelOffset = mathMax(
7979
						ticks[pos].getLabelSize(),
7980
						labelOffset
7981
					);
7982
				}
7983
			});
7984
 
7985
			if (axis.staggerLines) {
7986
				labelOffset *= axis.staggerLines;
7987
				axis.labelOffset = labelOffset;
7988
			}
7989
 
7990
 
7991
		} else { // doesn't have data
7992
			for (n in ticks) {
7993
				ticks[n].destroy();
7994
				delete ticks[n];
7995
			}
7996
		}
7997
 
7998
		if (axisTitleOptions && axisTitleOptions.text && axisTitleOptions.enabled !== false) {
7999
			if (!axis.axisTitle) {
8000
				axis.axisTitle = renderer.text(
8001
					axisTitleOptions.text,
8002
					0,
8003
					0,
8004
					axisTitleOptions.useHTML
8005
				)
8006
				.attr({
8007
					zIndex: 7,
8008
					rotation: axisTitleOptions.rotation || 0,
8009
					align:
8010
						axisTitleOptions.textAlign ||
8011
						{ low: 'left', middle: 'center', high: 'right' }[axisTitleOptions.align]
8012
				})
8013
				.addClass(PREFIX + this.coll.toLowerCase() + '-title')
8014
				.css(axisTitleOptions.style)
8015
				.add(axis.axisGroup);
8016
				axis.axisTitle.isNew = true;
8017
			}
8018
 
8019
			if (showAxis) {
8020
				titleOffset = axis.axisTitle.getBBox()[horiz ? 'height' : 'width'];
8021
				titleOffsetOption = axisTitleOptions.offset;
8022
				titleMargin = defined(titleOffsetOption) ? 0 : pick(axisTitleOptions.margin, horiz ? 5 : 10);
8023
			}
8024
 
8025
			// hide or show the title depending on whether showEmpty is set
8026
			axis.axisTitle[showAxis ? 'show' : 'hide']();
8027
		}
8028
 
8029
		// handle automatic or user set offset
8030
		axis.offset = directionFactor * pick(options.offset, axisOffset[side]);
8031
 
8032
		axis.tickRotCorr = axis.tickRotCorr || { x: 0, y: 0 }; // polar
8033
		lineHeightCorrection = side === 2 ? axis.tickRotCorr.y : 0;
8034
		labelOffsetPadded = labelOffset + titleMargin +
8035
			(labelOffset && (directionFactor * (horiz ? pick(labelOptions.y, axis.tickRotCorr.y + 8) : labelOptions.x) - lineHeightCorrection));
8036
		axis.axisTitleMargin = pick(titleOffsetOption, labelOffsetPadded);
8037
 
8038
		axisOffset[side] = mathMax(
8039
			axisOffset[side],
8040
			axis.axisTitleMargin + titleOffset + directionFactor * axis.offset,
8041
			labelOffsetPadded // #3027
8042
		);
8043
 
8044
		// Decide the clipping needed to keep the graph inside the plot area and axis lines
8045
		clip = options.offset ? 0 : mathFloor(options.lineWidth / 2) * 2; // #4308, #4371
8046
		clipOffset[invertedSide] = mathMax(clipOffset[invertedSide], clip);
8047
	},
8048
 
8049
	/**
8050
	 * Get the path for the axis line
8051
	 */
8052
	getLinePath: function (lineWidth) {
8053
		var chart = this.chart,
8054
			opposite = this.opposite,
8055
			offset = this.offset,
8056
			horiz = this.horiz,
8057
			lineLeft = this.left + (opposite ? this.width : 0) + offset,
8058
			lineTop = chart.chartHeight - this.bottom - (opposite ? this.height : 0) + offset;
8059
 
8060
		if (opposite) {
8061
			lineWidth *= -1; // crispify the other way - #1480, #1687
8062
		}
8063
 
8064
		return chart.renderer.crispLine([
8065
				M,
8066
				horiz ?
8067
					this.left :
8068
					lineLeft,
8069
				horiz ?
8070
					lineTop :
8071
					this.top,
8072
				L,
8073
				horiz ?
8074
					chart.chartWidth - this.right :
8075
					lineLeft,
8076
				horiz ?
8077
					lineTop :
8078
					chart.chartHeight - this.bottom
8079
			], lineWidth);
8080
	},
8081
 
8082
	/**
8083
	 * Position the title
8084
	 */
8085
	getTitlePosition: function () {
8086
		// compute anchor points for each of the title align options
8087
		var horiz = this.horiz,
8088
			axisLeft = this.left,
8089
			axisTop = this.top,
8090
			axisLength = this.len,
8091
			axisTitleOptions = this.options.title,
8092
			margin = horiz ? axisLeft : axisTop,
8093
			opposite = this.opposite,
8094
			offset = this.offset,
8095
			xOption = axisTitleOptions.x || 0,
8096
			yOption = axisTitleOptions.y || 0,
8097
			fontSize = pInt(axisTitleOptions.style.fontSize || 12),
8098
 
8099
			// the position in the length direction of the axis
8100
			alongAxis = {
8101
				low: margin + (horiz ? 0 : axisLength),
8102
				middle: margin + axisLength / 2,
8103
				high: margin + (horiz ? axisLength : 0)
8104
			}[axisTitleOptions.align],
8105
 
8106
			// the position in the perpendicular direction of the axis
8107
			offAxis = (horiz ? axisTop + this.height : axisLeft) +
8108
				(horiz ? 1 : -1) * // horizontal axis reverses the margin
8109
				(opposite ? -1 : 1) * // so does opposite axes
8110
				this.axisTitleMargin +
8111
				(this.side === 2 ? fontSize : 0);
8112
 
8113
		return {
8114
			x: horiz ?
8115
				alongAxis + xOption :
8116
				offAxis + (opposite ? this.width : 0) + offset + xOption,
8117
			y: horiz ?
8118
				offAxis + yOption - (opposite ? this.height : 0) + offset :
8119
				alongAxis + yOption
8120
		};
8121
	},
8122
 
8123
	/**
8124
	 * Render the axis
8125
	 */
8126
	render: function () {
8127
		var axis = this,
8128
			chart = axis.chart,
8129
			renderer = chart.renderer,
8130
			options = axis.options,
8131
			isLog = axis.isLog,
8132
			isLinked = axis.isLinked,
8133
			tickPositions = axis.tickPositions,
8134
			axisTitle = axis.axisTitle,
8135
			ticks = axis.ticks,
8136
			minorTicks = axis.minorTicks,
8137
			alternateBands = axis.alternateBands,
8138
			stackLabelOptions = options.stackLabels,
8139
			alternateGridColor = options.alternateGridColor,
8140
			tickmarkOffset = axis.tickmarkOffset,
8141
			lineWidth = options.lineWidth,
8142
			linePath,
8143
			hasRendered = chart.hasRendered,
8144
			slideInTicks = hasRendered && defined(axis.oldMin) && !isNaN(axis.oldMin),
8145
			showAxis = axis.showAxis,
8146
			globalAnimation = renderer.globalAnimation,
8147
			from,
8148
			to;
8149
 
8150
		// Reset
8151
		axis.labelEdge.length = 0;
8152
		//axis.justifyToPlot = overflow === 'justify';
8153
		axis.overlap = false;
8154
 
8155
		// Mark all elements inActive before we go over and mark the active ones
8156
		each([ticks, minorTicks, alternateBands], function (coll) {
8157
			var pos;
8158
			for (pos in coll) {
8159
				coll[pos].isActive = false;
8160
			}
8161
		});
8162
 
8163
		// If the series has data draw the ticks. Else only the line and title
8164
		if (axis.hasData() || isLinked) {
8165
 
8166
			// minor ticks
8167
			if (axis.minorTickInterval && !axis.categories) {
8168
				each(axis.getMinorTickPositions(), function (pos) {
8169
					if (!minorTicks[pos]) {
8170
						minorTicks[pos] = new Tick(axis, pos, 'minor');
8171
					}
8172
 
8173
					// render new ticks in old position
8174
					if (slideInTicks && minorTicks[pos].isNew) {
8175
						minorTicks[pos].render(null, true);
8176
					}
8177
 
8178
					minorTicks[pos].render(null, false, 1);
8179
				});
8180
			}
8181
 
8182
			// Major ticks. Pull out the first item and render it last so that
8183
			// we can get the position of the neighbour label. #808.
8184
			if (tickPositions.length) { // #1300
8185
				each(tickPositions, function (pos, i) {
8186
 
8187
					// linked axes need an extra check to find out if
8188
					if (!isLinked || (pos >= axis.min && pos <= axis.max)) {
8189
 
8190
						if (!ticks[pos]) {
8191
							ticks[pos] = new Tick(axis, pos);
8192
						}
8193
 
8194
						// render new ticks in old position
8195
						if (slideInTicks && ticks[pos].isNew) {
8196
							ticks[pos].render(i, true, 0.1);
8197
						}
8198
 
8199
						ticks[pos].render(i);
8200
					}
8201
 
8202
				});
8203
				// In a categorized axis, the tick marks are displayed between labels. So
8204
				// we need to add a tick mark and grid line at the left edge of the X axis.
8205
				if (tickmarkOffset && (axis.min === 0 || axis.single)) {
8206
					if (!ticks[-1]) {
8207
						ticks[-1] = new Tick(axis, -1, null, true);
8208
					}
8209
					ticks[-1].render(-1);
8210
				}
8211
 
8212
			}
8213
 
8214
			// alternate grid color
8215
			if (alternateGridColor) {
8216
				each(tickPositions, function (pos, i) {
8217
					to = tickPositions[i + 1] !== UNDEFINED ? tickPositions[i + 1] + tickmarkOffset : axis.max - tickmarkOffset;
8218
					if (i % 2 === 0 && pos < axis.max && to <= axis.max - tickmarkOffset) { // #2248
8219
						if (!alternateBands[pos]) {
8220
							alternateBands[pos] = new Highcharts.PlotLineOrBand(axis);
8221
						}
8222
						from = pos + tickmarkOffset; // #949
8223
						alternateBands[pos].options = {
8224
							from: isLog ? lin2log(from) : from,
8225
							to: isLog ? lin2log(to) : to,
8226
							color: alternateGridColor
8227
						};
8228
						alternateBands[pos].render();
8229
						alternateBands[pos].isActive = true;
8230
					}
8231
				});
8232
			}
8233
 
8234
			// custom plot lines and bands
8235
			if (!axis._addedPlotLB) { // only first time
8236
				each((options.plotLines || []).concat(options.plotBands || []), function (plotLineOptions) {
8237
					axis.addPlotBandOrLine(plotLineOptions);
8238
				});
8239
				axis._addedPlotLB = true;
8240
			}
8241
 
8242
		} // end if hasData
8243
 
8244
		// Remove inactive ticks
8245
		each([ticks, minorTicks, alternateBands], function (coll) {
8246
			var pos,
8247
				i,
8248
				forDestruction = [],
8249
				delay = globalAnimation ? globalAnimation.duration || 500 : 0,
8250
				destroyInactiveItems = function () {
8251
					i = forDestruction.length;
8252
					while (i--) {
8253
						// When resizing rapidly, the same items may be destroyed in different timeouts,
8254
						// or the may be reactivated
8255
						if (coll[forDestruction[i]] && !coll[forDestruction[i]].isActive) {
8256
							coll[forDestruction[i]].destroy();
8257
							delete coll[forDestruction[i]];
8258
						}
8259
					}
8260
 
8261
				};
8262
 
8263
			for (pos in coll) {
8264
 
8265
				if (!coll[pos].isActive) {
8266
					// Render to zero opacity
8267
					coll[pos].render(pos, false, 0);
8268
					coll[pos].isActive = false;
8269
					forDestruction.push(pos);
8270
				}
8271
			}
8272
 
8273
			// When the objects are finished fading out, destroy them
8274
			if (coll === alternateBands || !chart.hasRendered || !delay) {
8275
				destroyInactiveItems();
8276
			} else if (delay) {
8277
				setTimeout(destroyInactiveItems, delay);
8278
			}
8279
		});
8280
 
8281
		// Static items. As the axis group is cleared on subsequent calls
8282
		// to render, these items are added outside the group.
8283
		// axis line
8284
		if (lineWidth) {
8285
			linePath = axis.getLinePath(lineWidth);
8286
			if (!axis.axisLine) {
8287
				axis.axisLine = renderer.path(linePath)
8288
					.attr({
8289
						stroke: options.lineColor,
8290
						'stroke-width': lineWidth,
8291
						zIndex: 7
8292
					})
8293
					.add(axis.axisGroup);
8294
			} else {
8295
				axis.axisLine.animate({ d: linePath });
8296
			}
8297
 
8298
			// show or hide the line depending on options.showEmpty
8299
			axis.axisLine[showAxis ? 'show' : 'hide']();
8300
		}
8301
 
8302
		if (axisTitle && showAxis) {
8303
 
8304
			axisTitle[axisTitle.isNew ? 'attr' : 'animate'](
8305
				axis.getTitlePosition()
8306
			);
8307
			axisTitle.isNew = false;
8308
		}
8309
 
8310
		// Stacked totals:
8311
		if (stackLabelOptions && stackLabelOptions.enabled) {
8312
			axis.renderStackTotals();
8313
		}
8314
		// End stacked totals
8315
 
8316
		axis.isDirty = false;
8317
	},
8318
 
8319
	/**
8320
	 * Redraw the axis to reflect changes in the data or axis extremes
8321
	 */
8322
	redraw: function () {
8323
 
8324
		if (this.visible) {
8325
			// render the axis
8326
			this.render();
8327
 
8328
			// move plot lines and bands
8329
			each(this.plotLinesAndBands, function (plotLine) {
8330
				plotLine.render();
8331
			});
8332
		}
8333
 
8334
		// mark associated series as dirty and ready for redraw
8335
		each(this.series, function (series) {
8336
			series.isDirty = true;
8337
		});
8338
 
8339
	},
8340
 
8341
	/**
8342
	 * Destroys an Axis instance.
8343
	 */
8344
	destroy: function (keepEvents) {
8345
		var axis = this,
8346
			stacks = axis.stacks,
8347
			stackKey,
8348
			plotLinesAndBands = axis.plotLinesAndBands,
8349
			i;
8350
 
8351
		// Remove the events
8352
		if (!keepEvents) {
8353
			removeEvent(axis);
8354
		}
8355
 
8356
		// Destroy each stack total
8357
		for (stackKey in stacks) {
8358
			destroyObjectProperties(stacks[stackKey]);
8359
 
8360
			stacks[stackKey] = null;
8361
		}
8362
 
8363
		// Destroy collections
8364
		each([axis.ticks, axis.minorTicks, axis.alternateBands], function (coll) {
8365
			destroyObjectProperties(coll);
8366
		});
8367
		i = plotLinesAndBands.length;
8368
		while (i--) { // #1975
8369
			plotLinesAndBands[i].destroy();
8370
		}
8371
 
8372
		// Destroy local variables
8373
		each(['stackTotalGroup', 'axisLine', 'axisTitle', 'axisGroup', 'cross', 'gridGroup', 'labelGroup'], function (prop) {
8374
			if (axis[prop]) {
8375
				axis[prop] = axis[prop].destroy();
8376
			}
8377
		});
8378
 
8379
		// Destroy crosshair
8380
		if (this.cross) {
8381
			this.cross.destroy();
8382
		}
8383
	},
8384
 
8385
	/**
8386
	 * Draw the crosshair
8387
	 */
8388
	drawCrosshair: function (e, point) { // docs: Missing docs for Axis.crosshair. Also for properties.
8389
 
8390
		var path,
8391
			options = this.crosshair,
8392
			animation = options.animation,
8393
			pos,
8394
			attribs,
8395
			categorized;
8396
 
8397
		if (
8398
			// Disabled in options
8399
			!this.crosshair ||
8400
			// Snap
8401
			((defined(point) || !pick(this.crosshair.snap, true)) === false) ||
8402
			// Not on this axis (#4095, #2888)
8403
			(point && point.series && point.series[this.coll] !== this)
8404
		) {
8405
			this.hideCrosshair();
8406
 
8407
		} else {
8408
 
8409
			// Get the path
8410
			if (!pick(options.snap, true)) {
8411
				pos = (this.horiz ? e.chartX - this.pos : this.len - e.chartY + this.pos);
8412
			} else if (defined(point)) {
8413
				/*jslint eqeq: true*/
8414
				pos = this.isXAxis ? point.plotX : this.len - point.plotY; // #3834
8415
				/*jslint eqeq: false*/
8416
			}
8417
 
8418
			if (this.isRadial) {
8419
				path = this.getPlotLinePath(this.isXAxis ? point.x : pick(point.stackY, point.y)) || null; // #3189
8420
			} else {
8421
				path = this.getPlotLinePath(null, null, null, null, pos) || null; // #3189
8422
			}
8423
 
8424
			if (path === null) {
8425
				this.hideCrosshair();
8426
				return;
8427
			}
8428
 
8429
			// Draw the cross
8430
			if (this.cross) {
8431
				this.cross
8432
					.attr({ visibility: VISIBLE })[animation ? 'animate' : 'attr']({ d: path }, animation);
8433
			} else {
8434
				categorized = this.categories && !this.isRadial;
8435
				attribs = {
8436
					'stroke-width': options.width || (categorized ? this.transA : 1),
8437
					stroke: options.color || (categorized ? 'rgba(155,200,255,0.2)' : '#C0C0C0'),
8438
					zIndex: options.zIndex || 2
8439
				};
8440
				if (options.dashStyle) {
8441
					attribs.dashstyle = options.dashStyle;
8442
				}
8443
				this.cross = this.chart.renderer.path(path).attr(attribs).add();
8444
			}
8445
 
8446
		}
8447
 
8448
	},
8449
 
8450
	/**
8451
	 *	Hide the crosshair.
8452
	 */
8453
	hideCrosshair: function () {
8454
		if (this.cross) {
8455
			this.cross.hide();
8456
		}
8457
	}
8458
}; // end Axis
8459
 
8460
extend(Axis.prototype, AxisPlotLineOrBandExtension);
8461
/**
8462
 * Methods defined on the Axis prototype
8463
 */
8464
 
8465
/**
8466
 * Set the tick positions of a logarithmic axis
8467
 */
8468
Axis.prototype.getLogTickPositions = function (interval, min, max, minor) {
8469
	var axis = this,
8470
		options = axis.options,
8471
		axisLength = axis.len,
8472
		// Since we use this method for both major and minor ticks,
8473
		// use a local variable and return the result
8474
		positions = [];
8475
 
8476
	// Reset
8477
	if (!minor) {
8478
		axis._minorAutoInterval = null;
8479
	}
8480
 
8481
	// First case: All ticks fall on whole logarithms: 1, 10, 100 etc.
8482
	if (interval >= 0.5) {
8483
		interval = mathRound(interval);
8484
		positions = axis.getLinearTickPositions(interval, min, max);
8485
 
8486
	// Second case: We need intermediary ticks. For example
8487
	// 1, 2, 4, 6, 8, 10, 20, 40 etc.
8488
	} else if (interval >= 0.08) {
8489
		var roundedMin = mathFloor(min),
8490
			intermediate,
8491
			i,
8492
			j,
8493
			len,
8494
			pos,
8495
			lastPos,
8496
			break2;
8497
 
8498
		if (interval > 0.3) {
8499
			intermediate = [1, 2, 4];
8500
		} else if (interval > 0.15) { // 0.2 equals five minor ticks per 1, 10, 100 etc
8501
			intermediate = [1, 2, 4, 6, 8];
8502
		} else { // 0.1 equals ten minor ticks per 1, 10, 100 etc
8503
			intermediate = [1, 2, 3, 4, 5, 6, 7, 8, 9];
8504
		}
8505
 
8506
		for (i = roundedMin; i < max + 1 && !break2; i++) {
8507
			len = intermediate.length;
8508
			for (j = 0; j < len && !break2; j++) {
8509
				pos = log2lin(lin2log(i) * intermediate[j]);
8510
				if (pos > min && (!minor || lastPos <= max) && lastPos !== UNDEFINED) { // #1670, lastPos is #3113
8511
					positions.push(lastPos);
8512
				}
8513
 
8514
				if (lastPos > max) {
8515
					break2 = true;
8516
				}
8517
				lastPos = pos;
8518
			}
8519
		}
8520
 
8521
	// Third case: We are so deep in between whole logarithmic values that
8522
	// we might as well handle the tick positions like a linear axis. For
8523
	// example 1.01, 1.02, 1.03, 1.04.
8524
	} else {
8525
		var realMin = lin2log(min),
8526
			realMax = lin2log(max),
8527
			tickIntervalOption = options[minor ? 'minorTickInterval' : 'tickInterval'],
8528
			filteredTickIntervalOption = tickIntervalOption === 'auto' ? null : tickIntervalOption,
8529
			tickPixelIntervalOption = options.tickPixelInterval / (minor ? 5 : 1),
8530
			totalPixelLength = minor ? axisLength / axis.tickPositions.length : axisLength;
8531
 
8532
		interval = pick(
8533
			filteredTickIntervalOption,
8534
			axis._minorAutoInterval,
8535
			(realMax - realMin) * tickPixelIntervalOption / (totalPixelLength || 1)
8536
		);
8537
 
8538
		interval = normalizeTickInterval(
8539
			interval,
8540
			null,
8541
			getMagnitude(interval)
8542
		);
8543
 
8544
		positions = map(axis.getLinearTickPositions(
8545
			interval,
8546
			realMin,
8547
			realMax
8548
		), log2lin);
8549
 
8550
		if (!minor) {
8551
			axis._minorAutoInterval = interval / 5;
8552
		}
8553
	}
8554
 
8555
	// Set the axis-level tickInterval variable
8556
	if (!minor) {
8557
		axis.tickInterval = interval;
8558
	}
8559
	return positions;
8560
};/**
8561
 * The tooltip object
8562
 * @param {Object} chart The chart instance
8563
 * @param {Object} options Tooltip options
8564
 */
8565
var Tooltip = Highcharts.Tooltip = function () {
8566
	this.init.apply(this, arguments);
8567
};
8568
 
8569
Tooltip.prototype = {
8570
 
8571
	init: function (chart, options) {
8572
 
8573
		var borderWidth = options.borderWidth,
8574
			style = options.style,
8575
			padding = pInt(style.padding);
8576
 
8577
		// Save the chart and options
8578
		this.chart = chart;
8579
		this.options = options;
8580
 
8581
		// Keep track of the current series
8582
		//this.currentSeries = UNDEFINED;
8583
 
8584
		// List of crosshairs
8585
		this.crosshairs = [];
8586
 
8587
		// Current values of x and y when animating
8588
		this.now = { x: 0, y: 0 };
8589
 
8590
		// The tooltip is initially hidden
8591
		this.isHidden = true;
8592
 
8593
 
8594
		// create the label
8595
		this.label = chart.renderer.label('', 0, 0, options.shape || 'callout', null, null, options.useHTML, null, 'tooltip')
8596
			.attr({
8597
				padding: padding,
8598
				fill: options.backgroundColor,
8599
				'stroke-width': borderWidth,
8600
				r: options.borderRadius,
8601
				zIndex: 8
8602
			})
8603
			.css(style)
8604
			.css({ padding: 0 }) // Remove it from VML, the padding is applied as an attribute instead (#1117)
8605
			.add()
8606
			.attr({ y: -9999 }); // #2301, #2657
8607
 
8608
		// When using canVG the shadow shows up as a gray circle
8609
		// even if the tooltip is hidden.
8610
		if (!useCanVG) {
8611
			this.label.shadow(options.shadow);
8612
		}
8613
 
8614
		// Public property for getting the shared state.
8615
		this.shared = options.shared;
8616
	},
8617
 
8618
	/**
8619
	 * Destroy the tooltip and its elements.
8620
	 */
8621
	destroy: function () {
8622
		// Destroy and clear local variables
8623
		if (this.label) {
8624
			this.label = this.label.destroy();
8625
		}
8626
		clearTimeout(this.hideTimer);
8627
		clearTimeout(this.tooltipTimeout);
8628
	},
8629
 
8630
	/**
8631
	 * Provide a soft movement for the tooltip
8632
	 *
8633
	 * @param {Number} x
8634
	 * @param {Number} y
8635
	 * @private
8636
	 */
8637
	move: function (x, y, anchorX, anchorY) {
8638
		var tooltip = this,
8639
			now = tooltip.now,
8640
			animate = tooltip.options.animation !== false && !tooltip.isHidden &&
8641
				// When we get close to the target position, abort animation and land on the right place (#3056)
8642
				(mathAbs(x - now.x) > 1 || mathAbs(y - now.y) > 1),
8643
			skipAnchor = tooltip.followPointer || tooltip.len > 1;
8644
 
8645
		// Get intermediate values for animation
8646
		extend(now, {
8647
			x: animate ? (2 * now.x + x) / 3 : x,
8648
			y: animate ? (now.y + y) / 2 : y,
8649
			anchorX: skipAnchor ? UNDEFINED : animate ? (2 * now.anchorX + anchorX) / 3 : anchorX,
8650
			anchorY: skipAnchor ? UNDEFINED : animate ? (now.anchorY + anchorY) / 2 : anchorY
8651
		});
8652
 
8653
		// Move to the intermediate value
8654
		tooltip.label.attr(now);
8655
 
8656
 
8657
		// Run on next tick of the mouse tracker
8658
		if (animate) {
8659
 
8660
			// Never allow two timeouts
8661
			clearTimeout(this.tooltipTimeout);
8662
 
8663
			// Set the fixed interval ticking for the smooth tooltip
8664
			this.tooltipTimeout = setTimeout(function () {
8665
				// The interval function may still be running during destroy, so check that the chart is really there before calling.
8666
				if (tooltip) {
8667
					tooltip.move(x, y, anchorX, anchorY);
8668
				}
8669
			}, 32);
8670
 
8671
		}
8672
	},
8673
 
8674
	/**
8675
	 * Hide the tooltip
8676
	 */
8677
	hide: function (delay) {
8678
		var tooltip = this,
8679
			hoverPoints;
8680
 
8681
		clearTimeout(this.hideTimer); // disallow duplicate timers (#1728, #1766)
8682
		if (!this.isHidden) {
8683
			hoverPoints = this.chart.hoverPoints;
8684
 
8685
			this.hideTimer = setTimeout(function () {
8686
				tooltip.label.fadeOut();
8687
				tooltip.isHidden = true;
8688
			}, pick(delay, this.options.hideDelay, 500));
8689
		}
8690
	},
8691
 
8692
	/**
8693
	 * Extendable method to get the anchor position of the tooltip
8694
	 * from a point or set of points
8695
	 */
8696
	getAnchor: function (points, mouseEvent) {
8697
		var ret,
8698
			chart = this.chart,
8699
			inverted = chart.inverted,
8700
			plotTop = chart.plotTop,
8701
			plotLeft = chart.plotLeft,
8702
			plotX = 0,
8703
			plotY = 0,
8704
			yAxis,
8705
			xAxis;
8706
 
8707
		points = splat(points);
8708
 
8709
		// Pie uses a special tooltipPos
8710
		ret = points[0].tooltipPos;
8711
 
8712
		// When tooltip follows mouse, relate the position to the mouse
8713
		if (this.followPointer && mouseEvent) {
8714
			if (mouseEvent.chartX === UNDEFINED) {
8715
				mouseEvent = chart.pointer.normalize(mouseEvent);
8716
			}
8717
			ret = [
8718
				mouseEvent.chartX - chart.plotLeft,
8719
				mouseEvent.chartY - plotTop
8720
			];
8721
		}
8722
		// When shared, use the average position
8723
		if (!ret) {
8724
			each(points, function (point) {
8725
				yAxis = point.series.yAxis;
8726
				xAxis = point.series.xAxis;
8727
				plotX += point.plotX  + (!inverted && xAxis ? xAxis.left - plotLeft : 0);
8728
				plotY += (point.plotLow ? (point.plotLow + point.plotHigh) / 2 : point.plotY) +
8729
					(!inverted && yAxis ? yAxis.top - plotTop : 0); // #1151
8730
			});
8731
 
8732
			plotX /= points.length;
8733
			plotY /= points.length;
8734
 
8735
			ret = [
8736
				inverted ? chart.plotWidth - plotY : plotX,
8737
				this.shared && !inverted && points.length > 1 && mouseEvent ?
8738
					mouseEvent.chartY - plotTop : // place shared tooltip next to the mouse (#424)
8739
					inverted ? chart.plotHeight - plotX : plotY
8740
			];
8741
		}
8742
 
8743
		return map(ret, mathRound);
8744
	},
8745
 
8746
	/**
8747
	 * Place the tooltip in a chart without spilling over
8748
	 * and not covering the point it self.
8749
	 */
8750
	getPosition: function (boxWidth, boxHeight, point) {
8751
 
8752
		var chart = this.chart,
8753
			distance = this.distance,
8754
			ret = {},
8755
			h = point.h || 0, // #4117
8756
			swapped,
8757
			first = ['y', chart.chartHeight, boxHeight, point.plotY + chart.plotTop, chart.plotTop, chart.plotTop + chart.plotHeight],
8758
			second = ['x', chart.chartWidth, boxWidth, point.plotX + chart.plotLeft, chart.plotLeft, chart.plotLeft + chart.plotWidth],
8759
			// The far side is right or bottom
8760
			preferFarSide = pick(point.ttBelow, (chart.inverted && !point.negative) || (!chart.inverted && point.negative)),
8761
			/**
8762
			 * Handle the preferred dimension. When the preferred dimension is tooltip
8763
			 * on top or bottom of the point, it will look for space there.
8764
			 */
8765
			firstDimension = function (dim, outerSize, innerSize, point, min, max) {
8766
				var roomLeft = innerSize < point - distance,
8767
					roomRight = point + distance + innerSize < outerSize,
8768
					alignedLeft = point - distance - innerSize,
8769
					alignedRight = point + distance;
8770
 
8771
				if (preferFarSide && roomRight) {
8772
					ret[dim] = alignedRight;
8773
				} else if (!preferFarSide && roomLeft) {
8774
					ret[dim] = alignedLeft;
8775
				} else if (roomLeft) {
8776
					ret[dim] = mathMin(max - innerSize, alignedLeft - h < 0 ? alignedLeft : alignedLeft - h);
8777
				} else if (roomRight) {
8778
					ret[dim] = mathMax(min, alignedRight + h + innerSize > outerSize ? alignedRight : alignedRight + h);
8779
				} else {
8780
					return false;
8781
				}
8782
			},
8783
			/**
8784
			 * Handle the secondary dimension. If the preferred dimension is tooltip
8785
			 * on top or bottom of the point, the second dimension is to align the tooltip
8786
			 * above the point, trying to align center but allowing left or right
8787
			 * align within the chart box.
8788
			 */
8789
			secondDimension = function (dim, outerSize, innerSize, point) {
8790
				// Too close to the edge, return false and swap dimensions
8791
				if (point < distance || point > outerSize - distance) {
8792
					return false;
8793
 
8794
				// Align left/top
8795
				} else if (point < innerSize / 2) {
8796
					ret[dim] = 1;
8797
				// Align right/bottom
8798
				} else if (point > outerSize - innerSize / 2) {
8799
					ret[dim] = outerSize - innerSize - 2;
8800
				// Align center
8801
				} else {
8802
					ret[dim] = point - innerSize / 2;
8803
				}
8804
			},
8805
			/**
8806
			 * Swap the dimensions
8807
			 */
8808
			swap = function (count) {
8809
				var temp = first;
8810
				first = second;
8811
				second = temp;
8812
				swapped = count;
8813
			},
8814
			run = function () {
8815
				if (firstDimension.apply(0, first) !== false) {
8816
					if (secondDimension.apply(0, second) === false && !swapped) {
8817
						swap(true);
8818
						run();
8819
					}
8820
				} else if (!swapped) {
8821
					swap(true);
8822
					run();
8823
				} else {
8824
					ret.x = ret.y = 0;
8825
				}
8826
			};
8827
 
8828
		// Under these conditions, prefer the tooltip on the side of the point
8829
		if (chart.inverted || this.len > 1) {
8830
			swap();
8831
		}
8832
		run();
8833
 
8834
		return ret;
8835
 
8836
	},
8837
 
8838
	/**
8839
	 * In case no user defined formatter is given, this will be used. Note that the context
8840
	 * here is an object holding point, series, x, y etc.
8841
	 */
8842
	defaultFormatter: function (tooltip) {
8843
		var items = this.points || splat(this),
8844
			s;
8845
 
8846
		// build the header
8847
		s = [tooltip.tooltipFooterHeaderFormatter(items[0])]; //#3397: abstraction to enable formatting of footer and header
8848
 
8849
		// build the values
8850
		s = s.concat(tooltip.bodyFormatter(items));
8851
 
8852
		// footer
8853
		s.push(tooltip.tooltipFooterHeaderFormatter(items[0], true)); //#3397: abstraction to enable formatting of footer and header
8854
 
8855
		return s.join('');
8856
	},
8857
 
8858
	/**
8859
	 * Refresh the tooltip's text and position.
8860
	 * @param {Object} point
8861
	 */
8862
	refresh: function (point, mouseEvent) {
8863
		var tooltip = this,
8864
			chart = tooltip.chart,
8865
			label = tooltip.label,
8866
			options = tooltip.options,
8867
			x,
8868
			y,
8869
			anchor,
8870
			textConfig = {},
8871
			text,
8872
			pointConfig = [],
8873
			formatter = options.formatter || tooltip.defaultFormatter,
8874
			hoverPoints = chart.hoverPoints,
8875
			borderColor,
8876
			shared = tooltip.shared,
8877
			currentSeries;
8878
 
8879
		clearTimeout(this.hideTimer);
8880
 
8881
		// get the reference point coordinates (pie charts use tooltipPos)
8882
		tooltip.followPointer = splat(point)[0].series.tooltipOptions.followPointer;
8883
		anchor = tooltip.getAnchor(point, mouseEvent);
8884
		x = anchor[0];
8885
		y = anchor[1];
8886
 
8887
		// shared tooltip, array is sent over
8888
		if (shared && !(point.series && point.series.noSharedTooltip)) {
8889
 
8890
			// hide previous hoverPoints and set new
8891
 
8892
			chart.hoverPoints = point;
8893
			if (hoverPoints) {
8894
				each(hoverPoints, function (point) {
8895
					point.setState();
8896
				});
8897
			}
8898
 
8899
			each(point, function (item) {
8900
				item.setState(HOVER_STATE);
8901
 
8902
				pointConfig.push(item.getLabelConfig());
8903
			});
8904
 
8905
			textConfig = {
8906
				x: point[0].category,
8907
				y: point[0].y
8908
			};
8909
			textConfig.points = pointConfig;
8910
			this.len = pointConfig.length;
8911
			point = point[0];
8912
 
8913
		// single point tooltip
8914
		} else {
8915
			textConfig = point.getLabelConfig();
8916
		}
8917
		text = formatter.call(textConfig, tooltip);
8918
 
8919
		// register the current series
8920
		currentSeries = point.series;
8921
		this.distance = pick(currentSeries.tooltipOptions.distance, 16);
8922
 
8923
		// update the inner HTML
8924
		if (text === false) {
8925
			this.hide();
8926
		} else {
8927
 
8928
			// show it
8929
			if (tooltip.isHidden) {
8930
				stop(label);
8931
				label.attr('opacity', 1).show();
8932
			}
8933
 
8934
			// update text
8935
			label.attr({
8936
				text: text
8937
			});
8938
 
8939
			// set the stroke color of the box
8940
			borderColor = options.borderColor || point.color || currentSeries.color || '#606060';
8941
			label.attr({
8942
				stroke: borderColor
8943
			});
8944
			tooltip.updatePosition({
8945
				plotX: x,
8946
				plotY: y,
8947
				negative: point.negative,
8948
				ttBelow: point.ttBelow,
8949
				h: anchor[2] || 0
8950
			});
8951
 
8952
			this.isHidden = false;
8953
		}
8954
		fireEvent(chart, 'tooltipRefresh', {
8955
				text: text,
8956
				x: x + chart.plotLeft,
8957
				y: y + chart.plotTop,
8958
				borderColor: borderColor
8959
			});
8960
	},
8961
 
8962
	/**
8963
	 * Find the new position and perform the move
8964
	 */
8965
	updatePosition: function (point) {
8966
		var chart = this.chart,
8967
			label = this.label,
8968
			pos = (this.options.positioner || this.getPosition).call(
8969
				this,
8970
				label.width,
8971
				label.height,
8972
				point
8973
			);
8974
 
8975
		// do the move
8976
		this.move(
8977
			mathRound(pos.x),
8978
			mathRound(pos.y || 0), // can be undefined (#3977)
8979
			point.plotX + chart.plotLeft,
8980
			point.plotY + chart.plotTop
8981
		);
8982
	},
8983
 
8984
	/**
8985
	 * Get the best X date format based on the closest point range on the axis.
8986
	 */
8987
	getXDateFormat: function (point, options, xAxis) {
8988
		var xDateFormat,
8989
			dateTimeLabelFormats = options.dateTimeLabelFormats,
8990
			closestPointRange = xAxis && xAxis.closestPointRange,
8991
			n,
8992
			blank = '01-01 00:00:00.000',
8993
			strpos = {
8994
				millisecond: 15,
8995
				second: 12,
8996
				minute: 9,
8997
				hour: 6,
8998
				day: 3
8999
			},
9000
			date,
9001
			lastN = 'millisecond'; // for sub-millisecond data, #4223
9002
 
9003
		if (closestPointRange) {
9004
			date = dateFormat('%m-%d %H:%M:%S.%L', point.x);
9005
			for (n in timeUnits) {
9006
 
9007
				// If the range is exactly one week and we're looking at a Sunday/Monday, go for the week format
9008
				if (closestPointRange === timeUnits.week && +dateFormat('%w', point.x) === xAxis.options.startOfWeek &&
9009
						date.substr(6) === blank.substr(6)) {
9010
					n = 'week';
9011
					break;
9012
 
9013
				// The first format that is too great for the range
9014
				} else if (timeUnits[n] > closestPointRange) {
9015
					n = lastN;
9016
					break;
9017
 
9018
				// If the point is placed every day at 23:59, we need to show
9019
				// the minutes as well. #2637.
9020
				} else if (strpos[n] && date.substr(strpos[n]) !== blank.substr(strpos[n])) {
9021
					break;
9022
				}
9023
 
9024
				// Weeks are outside the hierarchy, only apply them on Mondays/Sundays like in the first condition
9025
				if (n !== 'week') {
9026
					lastN = n;
9027
				}
9028
			}
9029
 
9030
			if (n) {
9031
				xDateFormat = dateTimeLabelFormats[n];
9032
			}
9033
		} else {
9034
			xDateFormat = dateTimeLabelFormats.day;
9035
		}
9036
 
9037
		return xDateFormat || dateTimeLabelFormats.year; // #2546, 2581
9038
	},
9039
 
9040
	/**
9041
	 * Format the footer/header of the tooltip
9042
	 * #3397: abstraction to enable formatting of footer and header
9043
	 */
9044
	tooltipFooterHeaderFormatter: function (point, isFooter) {
9045
		var footOrHead = isFooter ? 'footer' : 'header',
9046
			series = point.series,
9047
			tooltipOptions = series.tooltipOptions,
9048
			xDateFormat = tooltipOptions.xDateFormat,
9049
			xAxis = series.xAxis,
9050
			isDateTime = xAxis && xAxis.options.type === 'datetime' && isNumber(point.key),
9051
			formatString = tooltipOptions[footOrHead+'Format'];
9052
 
9053
		// Guess the best date format based on the closest point distance (#568, #3418)
9054
		if (isDateTime && !xDateFormat) {
9055
			xDateFormat = this.getXDateFormat(point, tooltipOptions, xAxis);
9056
		}
9057
 
9058
		// Insert the footer date format if any
9059
		if (isDateTime && xDateFormat) {
9060
			formatString = formatString.replace('{point.key}', '{point.key:' + xDateFormat + '}');
9061
		}
9062
 
9063
		return format(formatString, {
9064
			point: point,
9065
			series: series
9066
		});
9067
	},
9068
 
9069
	/**
9070
     * Build the body (lines) of the tooltip by iterating over the items and returning one entry for each item,
9071
     * abstracting this functionality allows to easily overwrite and extend it.
9072
	 */
9073
	bodyFormatter: function (items) {
9074
        return map(items, function (item) {
9075
            var tooltipOptions = item.series.tooltipOptions;
9076
            return (tooltipOptions.pointFormatter || item.point.tooltipFormatter).call(item.point, tooltipOptions.pointFormat);
9077
        });
9078
    }
9079
 
9080
};
9081
 
9082
var hoverChartIndex;
9083
 
9084
// Global flag for touch support
9085
hasTouch = doc.documentElement.ontouchstart !== UNDEFINED;
9086
 
9087
/**
9088
 * The mouse tracker object. All methods starting with "on" are primary DOM event handlers.
9089
 * Subsequent methods should be named differently from what they are doing.
9090
 * @param {Object} chart The Chart instance
9091
 * @param {Object} options The root options object
9092
 */
9093
var Pointer = Highcharts.Pointer = function (chart, options) {
9094
	this.init(chart, options);
9095
};
9096
 
9097
Pointer.prototype = {
9098
	/**
9099
	 * Initialize Pointer
9100
	 */
9101
	init: function (chart, options) {
9102
 
9103
		var chartOptions = options.chart,
9104
			chartEvents = chartOptions.events,
9105
			zoomType = useCanVG ? '' : chartOptions.zoomType,
9106
			inverted = chart.inverted,
9107
			zoomX,
9108
			zoomY;
9109
 
9110
		// Store references
9111
		this.options = options;
9112
		this.chart = chart;
9113
 
9114
		// Zoom status
9115
		this.zoomX = zoomX = /x/.test(zoomType);
9116
		this.zoomY = zoomY = /y/.test(zoomType);
9117
		this.zoomHor = (zoomX && !inverted) || (zoomY && inverted);
9118
		this.zoomVert = (zoomY && !inverted) || (zoomX && inverted);
9119
		this.hasZoom = zoomX || zoomY;
9120
 
9121
		// Do we need to handle click on a touch device?
9122
		this.runChartClick = chartEvents && !!chartEvents.click;
9123
 
9124
		this.pinchDown = [];
9125
		this.lastValidTouch = {};
9126
 
9127
		if (Highcharts.Tooltip && options.tooltip.enabled) {
9128
			chart.tooltip = new Tooltip(chart, options.tooltip);
9129
			this.followTouchMove = pick(options.tooltip.followTouchMove, true);
9130
		}
9131
 
9132
		this.setDOMEvents();
9133
	},
9134
 
9135
	/**
9136
	 * Add crossbrowser support for chartX and chartY
9137
	 * @param {Object} e The event object in standard browsers
9138
	 */
9139
	normalize: function (e, chartPosition) {
9140
		var chartX,
9141
			chartY,
9142
			ePos;
9143
 
9144
		// common IE normalizing
9145
		e = e || window.event;
9146
 
9147
		// Framework specific normalizing (#1165)
9148
		e = washMouseEvent(e);
9149
 
9150
		// More IE normalizing, needs to go after washMouseEvent
9151
		if (!e.target) {
9152
			e.target = e.srcElement;
9153
		}
9154
 
9155
		// iOS (#2757)
9156
		ePos = e.touches ?  (e.touches.length ? e.touches.item(0) : e.changedTouches[0]) : e;
9157
 
9158
		// Get mouse position
9159
		if (!chartPosition) {
9160
			this.chartPosition = chartPosition = offset(this.chart.container);
9161
		}
9162
 
9163
		// chartX and chartY
9164
		if (ePos.pageX === UNDEFINED) { // IE < 9. #886.
9165
			chartX = mathMax(e.x, e.clientX - chartPosition.left); // #2005, #2129: the second case is
9166
				// for IE10 quirks mode within framesets
9167
			chartY = e.y;
9168
		} else {
9169
			chartX = ePos.pageX - chartPosition.left;
9170
			chartY = ePos.pageY - chartPosition.top;
9171
		}
9172
 
9173
		return extend(e, {
9174
			chartX: mathRound(chartX),
9175
			chartY: mathRound(chartY)
9176
		});
9177
	},
9178
 
9179
	/**
9180
	 * Get the click position in terms of axis values.
9181
	 *
9182
	 * @param {Object} e A pointer event
9183
	 */
9184
	getCoordinates: function (e) {
9185
		var coordinates = {
9186
				xAxis: [],
9187
				yAxis: []
9188
			};
9189
 
9190
		each(this.chart.axes, function (axis) {
9191
			coordinates[axis.isXAxis ? 'xAxis' : 'yAxis'].push({
9192
				axis: axis,
9193
				value: axis.toValue(e[axis.horiz ? 'chartX' : 'chartY'])
9194
			});
9195
		});
9196
		return coordinates;
9197
	},
9198
 
9199
	/**
9200
	 * With line type charts with a single tracker, get the point closest to the mouse.
9201
	 * Run Point.onMouseOver and display tooltip for the point or points.
9202
	 */
9203
	runPointActions: function (e) {
9204
 
9205
		var pointer = this,
9206
			chart = pointer.chart,
9207
			series = chart.series,
9208
			tooltip = chart.tooltip,
9209
			shared = tooltip ? tooltip.shared : false,
9210
			followPointer,
9211
			hoverPoint = chart.hoverPoint,
9212
			hoverSeries = chart.hoverSeries,
9213
			i,
9214
			distance = Number.MAX_VALUE, // #4511
9215
			anchor,
9216
			noSharedTooltip,
9217
			stickToHoverSeries,
9218
			directTouch,
9219
			kdpoints = [],
9220
			kdpoint,
9221
			kdpointT;
9222
 
9223
		// For hovering over the empty parts of the plot area (hoverSeries is undefined).
9224
		// If there is one series with point tracking (combo chart), don't go to nearest neighbour.
9225
		if (!shared && !hoverSeries) {
9226
			for (i = 0; i < series.length; i++) {
9227
				if (series[i].directTouch || !series[i].options.stickyTracking) {
9228
					series = [];
9229
				}
9230
			}
9231
		}
9232
 
9233
		// If it has a hoverPoint and that series requires direct touch (like columns, #3899), or we're on
9234
		// a noSharedTooltip series among shared tooltip series (#4546), use the hoverPoint . Otherwise,
9235
		// search the k-d tree.
9236
		stickToHoverSeries = hoverSeries && (shared ? hoverSeries.noSharedTooltip : hoverSeries.directTouch);
9237
		if (stickToHoverSeries && hoverPoint) {
9238
			kdpoint = hoverPoint;
9239
 
9240
		// Handle shared tooltip or cases where a series is not yet hovered
9241
		} else {
9242
			// Find nearest points on all series
9243
			each(series, function (s) {
9244
				// Skip hidden series
9245
				noSharedTooltip = s.noSharedTooltip && shared;
9246
				directTouch = !shared && s.directTouch;
9247
				if (s.visible && !noSharedTooltip && !directTouch && pick(s.options.enableMouseTracking, true)) { // #3821
9248
					kdpointT = s.searchPoint(e, !noSharedTooltip && s.kdDimensions === 1); // #3828
9249
					if (kdpointT) {
9250
						kdpoints.push(kdpointT);
9251
					}
9252
				}
9253
			});
9254
			// Find absolute nearest point
9255
			each(kdpoints, function (p) {
9256
				if (p && typeof p.dist === 'number' && p.dist < distance) {
9257
					distance = p.dist;
9258
					kdpoint = p;
9259
				}
9260
			});
9261
		}
9262
 
9263
		// Refresh tooltip for kdpoint if new hover point or tooltip was hidden // #3926, #4200
9264
		if (kdpoint && (kdpoint !== this.prevKDPoint || (tooltip && tooltip.isHidden))) {
9265
			// Draw tooltip if necessary
9266
			if (shared && !kdpoint.series.noSharedTooltip) {
9267
				i = kdpoints.length;
9268
				while (i--) {
9269
					if (kdpoints[i].clientX !== kdpoint.clientX || kdpoints[i].series.noSharedTooltip) {
9270
						kdpoints.splice(i, 1);
9271
					}
9272
				}
9273
				if (kdpoints.length && tooltip) {
9274
					tooltip.refresh(kdpoints, e);
9275
				}
9276
 
9277
				// Do mouseover on all points (#3919, #3985, #4410)
9278
				each(kdpoints, function (point) {
9279
					point.onMouseOver(e, point !== ((hoverSeries && hoverSeries.directTouch && hoverPoint) || kdpoint));
9280
				});
9281
			} else {
9282
				if (tooltip) {
9283
					tooltip.refresh(kdpoint, e);
9284
				}
9285
				if(!hoverSeries || !hoverSeries.directTouch) { // #4448
9286
					kdpoint.onMouseOver(e);
9287
				}
9288
			}
9289
			this.prevKDPoint = kdpoint;
9290
 
9291
		// Update positions (regardless of kdpoint or hoverPoint)
9292
		} else {
9293
			followPointer = hoverSeries && hoverSeries.tooltipOptions.followPointer;
9294
			if (tooltip && followPointer && !tooltip.isHidden) {
9295
				anchor = tooltip.getAnchor([{}], e);
9296
				tooltip.updatePosition({ plotX: anchor[0], plotY: anchor[1] });
9297
			}
9298
		}
9299
 
9300
		// Start the event listener to pick up the tooltip
9301
		if (tooltip && !pointer._onDocumentMouseMove) {
9302
			pointer._onDocumentMouseMove = function (e) {
9303
				if (charts[hoverChartIndex]) {
9304
					charts[hoverChartIndex].pointer.onDocumentMouseMove(e);
9305
				}
9306
			};
9307
			addEvent(doc, 'mousemove', pointer._onDocumentMouseMove);
9308
		}
9309
 
9310
		// Crosshair
9311
		each(chart.axes, function (axis) {
9312
			axis.drawCrosshair(e, pick(kdpoint, hoverPoint));
9313
		});
9314
 
9315
 
9316
	},
9317
 
9318
 
9319
 
9320
	/**
9321
	 * Reset the tracking by hiding the tooltip, the hover series state and the hover point
9322
	 *
9323
	 * @param allowMove {Boolean} Instead of destroying the tooltip altogether, allow moving it if possible
9324
	 */
9325
	reset: function (allowMove, delay) {
9326
		var pointer = this,
9327
			chart = pointer.chart,
9328
			hoverSeries = chart.hoverSeries,
9329
			hoverPoint = chart.hoverPoint,
9330
			hoverPoints = chart.hoverPoints,
9331
			tooltip = chart.tooltip,
9332
			tooltipPoints = tooltip && tooltip.shared ? hoverPoints : hoverPoint;
9333
 
9334
		// Narrow in allowMove
9335
		allowMove = allowMove && tooltip && tooltipPoints;
9336
 
9337
		// Check if the points have moved outside the plot area, #1003
9338
		if (allowMove  && splat(tooltipPoints)[0].plotX === UNDEFINED) {
9339
			allowMove = false;
9340
		}
9341
		// Just move the tooltip, #349
9342
		if (allowMove) {
9343
			tooltip.refresh(tooltipPoints);
9344
			if (hoverPoint) { // #2500
9345
				hoverPoint.setState(hoverPoint.state, true);
9346
				each(chart.axes, function (axis) {
9347
					if (pick(axis.options.crosshair && axis.options.crosshair.snap, true)) {
9348
						axis.drawCrosshair(null, hoverPoint);
9349
					}  else {
9350
						axis.hideCrosshair();
9351
					}
9352
				});
9353
 
9354
			}
9355
 
9356
		// Full reset
9357
		} else {
9358
 
9359
			if (hoverPoint) {
9360
				hoverPoint.onMouseOut();
9361
			}
9362
 
9363
			if (hoverPoints) {
9364
				each(hoverPoints, function (point) {
9365
					point.setState();
9366
				});
9367
			}
9368
 
9369
			if (hoverSeries) {
9370
				hoverSeries.onMouseOut();
9371
			}
9372
 
9373
			if (tooltip) {
9374
				tooltip.hide(delay);
9375
			}
9376
 
9377
			if (pointer._onDocumentMouseMove) {
9378
				removeEvent(doc, 'mousemove', pointer._onDocumentMouseMove);
9379
				pointer._onDocumentMouseMove = null;
9380
			}
9381
 
9382
			// Remove crosshairs
9383
			each(chart.axes, function (axis) {
9384
				axis.hideCrosshair();
9385
			});
9386
 
9387
			pointer.hoverX = chart.hoverPoints = chart.hoverPoint = null;
9388
 
9389
		}
9390
	},
9391
 
9392
	/**
9393
	 * Scale series groups to a certain scale and translation
9394
	 */
9395
	scaleGroups: function (attribs, clip) {
9396
 
9397
		var chart = this.chart,
9398
			seriesAttribs;
9399
 
9400
		// Scale each series
9401
		each(chart.series, function (series) {
9402
			seriesAttribs = attribs || series.getPlotBox(); // #1701
9403
			if (series.xAxis && series.xAxis.zoomEnabled) {
9404
				series.group.attr(seriesAttribs);
9405
				if (series.markerGroup) {
9406
					series.markerGroup.attr(seriesAttribs);
9407
					series.markerGroup.clip(clip ? chart.clipRect : null);
9408
				}
9409
				if (series.dataLabelsGroup) {
9410
					series.dataLabelsGroup.attr(seriesAttribs);
9411
				}
9412
			}
9413
		});
9414
 
9415
		// Clip
9416
		chart.clipRect.attr(clip || chart.clipBox);
9417
	},
9418
 
9419
	/**
9420
	 * Start a drag operation
9421
	 */
9422
	dragStart: function (e) {
9423
		var chart = this.chart;
9424
 
9425
		// Record the start position
9426
		chart.mouseIsDown = e.type;
9427
		chart.cancelClick = false;
9428
		chart.mouseDownX = this.mouseDownX = e.chartX;
9429
		chart.mouseDownY = this.mouseDownY = e.chartY;
9430
	},
9431
 
9432
	/**
9433
	 * Perform a drag operation in response to a mousemove event while the mouse is down
9434
	 */
9435
	drag: function (e) {
9436
 
9437
		var chart = this.chart,
9438
			chartOptions = chart.options.chart,
9439
			chartX = e.chartX,
9440
			chartY = e.chartY,
9441
			zoomHor = this.zoomHor,
9442
			zoomVert = this.zoomVert,
9443
			plotLeft = chart.plotLeft,
9444
			plotTop = chart.plotTop,
9445
			plotWidth = chart.plotWidth,
9446
			plotHeight = chart.plotHeight,
9447
			clickedInside,
9448
			size,
9449
			selectionMarker = this.selectionMarker,
9450
			mouseDownX = this.mouseDownX,
9451
			mouseDownY = this.mouseDownY,
9452
			panKey = chartOptions.panKey && e[chartOptions.panKey + 'Key'];
9453
 
9454
		// If the device supports both touch and mouse (like IE11), and we are touch-dragging
9455
		// inside the plot area, don't handle the mouse event. #4339.
9456
		if (selectionMarker && selectionMarker.touch) {
9457
			return;
9458
		}
9459
 
9460
		// If the mouse is outside the plot area, adjust to cooordinates
9461
		// inside to prevent the selection marker from going outside
9462
		if (chartX < plotLeft) {
9463
			chartX = plotLeft;
9464
		} else if (chartX > plotLeft + plotWidth) {
9465
			chartX = plotLeft + plotWidth;
9466
		}
9467
 
9468
		if (chartY < plotTop) {
9469
			chartY = plotTop;
9470
		} else if (chartY > plotTop + plotHeight) {
9471
			chartY = plotTop + plotHeight;
9472
		}
9473
 
9474
		// determine if the mouse has moved more than 10px
9475
		this.hasDragged = Math.sqrt(
9476
			Math.pow(mouseDownX - chartX, 2) +
9477
			Math.pow(mouseDownY - chartY, 2)
9478
		);
9479
 
9480
		if (this.hasDragged > 10) {
9481
			clickedInside = chart.isInsidePlot(mouseDownX - plotLeft, mouseDownY - plotTop);
9482
 
9483
			// make a selection
9484
			if (chart.hasCartesianSeries && (this.zoomX || this.zoomY) && clickedInside && !panKey) {
9485
				if (!selectionMarker) {
9486
					this.selectionMarker = selectionMarker = chart.renderer.rect(
9487
						plotLeft,
9488
						plotTop,
9489
						zoomHor ? 1 : plotWidth,
9490
						zoomVert ? 1 : plotHeight,
9491
 
9492
					)
9493
					.attr({
9494
						fill: chartOptions.selectionMarkerFill || 'rgba(69,114,167,0.25)',
9495
						zIndex: 7
9496
					})
9497
					.add();
9498
				}
9499
			}
9500
 
9501
			// adjust the width of the selection marker
9502
			if (selectionMarker && zoomHor) {
9503
				size = chartX - mouseDownX;
9504
				selectionMarker.attr({
9505
					width: mathAbs(size),
9506
					x: (size > 0 ? 0 : size) + mouseDownX
9507
				});
9508
			}
9509
			// adjust the height of the selection marker
9510
			if (selectionMarker && zoomVert) {
9511
				size = chartY - mouseDownY;
9512
				selectionMarker.attr({
9513
					height: mathAbs(size),
9514
					y: (size > 0 ? 0 : size) + mouseDownY
9515
				});
9516
			}
9517
 
9518
			// panning
9519
			if (clickedInside && !selectionMarker && chartOptions.panning) {
9520
				chart.pan(e, chartOptions.panning);
9521
			}
9522
		}
9523
	},
9524
 
9525
	/**
9526
	 * On mouse up or touch end across the entire document, drop the selection.
9527
	 */
9528
	drop: function (e) {
9529
		var pointer = this,
9530
			chart = this.chart,
9531
			hasPinched = this.hasPinched;
9532
 
9533
		if (this.selectionMarker) {
9534
			var selectionData = {
9535
					xAxis: [],
9536
					yAxis: [],
9537
					originalEvent: e.originalEvent || e
9538
				},
9539
				selectionBox = this.selectionMarker,
9540
				selectionLeft = selectionBox.attr ? selectionBox.attr('x') : selectionBox.x,
9541
				selectionTop = selectionBox.attr ? selectionBox.attr('y') : selectionBox.y,
9542
				selectionWidth = selectionBox.attr ? selectionBox.attr('width') : selectionBox.width,
9543
				selectionHeight = selectionBox.attr ? selectionBox.attr('height') : selectionBox.height,
9544
				runZoom;
9545
 
9546
			// a selection has been made
9547
			if (this.hasDragged || hasPinched) {
9548
 
9549
				// record each axis' min and max
9550
				each(chart.axes, function (axis) {
9551
					if (axis.zoomEnabled && defined(axis.min) && (hasPinched || pointer[{ xAxis: 'zoomX', yAxis: 'zoomY' }[axis.coll]])) { // #859, #3569
9552
						var horiz = axis.horiz,
9553
							minPixelPadding = e.type === 'touchend' ? axis.minPixelPadding: 0, // #1207, #3075
9554
							selectionMin = axis.toValue((horiz ? selectionLeft : selectionTop) + minPixelPadding),
9555
							selectionMax = axis.toValue((horiz ? selectionLeft + selectionWidth : selectionTop + selectionHeight) - minPixelPadding);
9556
 
9557
						selectionData[axis.coll].push({
9558
							axis: axis,
9559
							min: mathMin(selectionMin, selectionMax), // for reversed axes
9560
							max: mathMax(selectionMin, selectionMax)
9561
						});
9562
						runZoom = true;
9563
					}
9564
				});
9565
				if (runZoom) {
9566
					fireEvent(chart, 'selection', selectionData, function (args) {
9567
						chart.zoom(extend(args, hasPinched ? { animation: false } : null));
9568
					});
9569
				}
9570
 
9571
			}
9572
			this.selectionMarker = this.selectionMarker.destroy();
9573
 
9574
			// Reset scaling preview
9575
			if (hasPinched) {
9576
				this.scaleGroups();
9577
			}
9578
		}
9579
 
9580
		// Reset all
9581
		if (chart) { // it may be destroyed on mouse up - #877
9582
			css(chart.container, { cursor: chart._cursor });
9583
			chart.cancelClick = this.hasDragged > 10; // #370
9584
			chart.mouseIsDown = this.hasDragged = this.hasPinched = false;
9585
			this.pinchDown = [];
9586
		}
9587
	},
9588
 
9589
	onContainerMouseDown: function (e) {
9590
 
9591
		e = this.normalize(e);
9592
 
9593
		// issue #295, dragging not always working in Firefox
9594
		if (e.preventDefault) {
9595
			e.preventDefault();
9596
		}
9597
 
9598
		this.dragStart(e);
9599
	},
9600
 
9601
 
9602
 
9603
	onDocumentMouseUp: function (e) {
9604
		if (charts[hoverChartIndex]) {
9605
			charts[hoverChartIndex].pointer.drop(e);
9606
		}
9607
	},
9608
 
9609
	/**
9610
	 * Special handler for mouse move that will hide the tooltip when the mouse leaves the plotarea.
9611
	 * Issue #149 workaround. The mouseleave event does not always fire.
9612
	 */
9613
	onDocumentMouseMove: function (e) {
9614
		var chart = this.chart,
9615
			chartPosition = this.chartPosition;
9616
 
9617
		e = this.normalize(e, chartPosition);
9618
 
9619
		// If we're outside, hide the tooltip
9620
		if (chartPosition && !this.inClass(e.target, 'highcharts-tracker') &&
9621
				!chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) {
9622
			this.reset();
9623
		}
9624
	},
9625
 
9626
	/**
9627
	 * When mouse leaves the container, hide the tooltip.
9628
	 */
9629
	onContainerMouseLeave: function () {
9630
		var chart = charts[hoverChartIndex];
9631
		if (chart) {
9632
			chart.pointer.reset();
9633
			chart.pointer.chartPosition = null; // also reset the chart position, used in #149 fix
9634
		}
9635
	},
9636
 
9637
	// The mousemove, touchmove and touchstart event handler
9638
	onContainerMouseMove: function (e) {
9639
 
9640
		var chart = this.chart;
9641
 
9642
		hoverChartIndex = chart.index;
9643
 
9644
		e = this.normalize(e);
9645
		e.returnValue = false; // #2251, #3224
9646
 
9647
		if (chart.mouseIsDown === 'mousedown') {
9648
			this.drag(e);
9649
		}
9650
 
9651
		// Show the tooltip and run mouse over events (#977)
9652
		if ((this.inClass(e.target, 'highcharts-tracker') ||
9653
				chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) && !chart.openMenu) {
9654
			this.runPointActions(e);
9655
		}
9656
	},
9657
 
9658
	/**
9659
	 * Utility to detect whether an element has, or has a parent with, a specific
9660
	 * class name. Used on detection of tracker objects and on deciding whether
9661
	 * hovering the tooltip should cause the active series to mouse out.
9662
	 */
9663
	inClass: function (element, className) {
9664
		var elemClassName;
9665
		while (element) {
9666
			elemClassName = attr(element, 'class');
9667
			if (elemClassName) {
9668
				if (elemClassName.indexOf(className) !== -1) {
9669
					return true;
9670
				} else if (elemClassName.indexOf(PREFIX + 'container') !== -1) {
9671
					return false;
9672
				}
9673
			}
9674
			element = element.parentNode;
9675
		}
9676
	},
9677
 
9678
	onTrackerMouseOut: function (e) {
9679
		var series = this.chart.hoverSeries,
9680
			relatedTarget = e.relatedTarget || e.toElement;
9681
 
9682
		if (series && !series.options.stickyTracking &&
9683
				!this.inClass(relatedTarget, PREFIX + 'tooltip') &&
9684
				!this.inClass(relatedTarget, PREFIX + 'series-' + series.index)) { // #2499, #4465
9685
			series.onMouseOut();
9686
		}
9687
	},
9688
 
9689
	onContainerClick: function (e) {
9690
		var chart = this.chart,
9691
			hoverPoint = chart.hoverPoint,
9692
			plotLeft = chart.plotLeft,
9693
			plotTop = chart.plotTop;
9694
 
9695
		e = this.normalize(e);
9696
		e.originalEvent = e; // #3913
9697
 
9698
		if (!chart.cancelClick) {
9699
 
9700
			// On tracker click, fire the series and point events. #783, #1583
9701
			if (hoverPoint && this.inClass(e.target, PREFIX + 'tracker')) {
9702
 
9703
				// the series click event
9704
				fireEvent(hoverPoint.series, 'click', extend(e, {
9705
					point: hoverPoint
9706
				}));
9707
 
9708
				// the point click event
9709
				if (chart.hoverPoint) { // it may be destroyed (#1844)
9710
					hoverPoint.firePointEvent('click', e);
9711
				}
9712
 
9713
			// When clicking outside a tracker, fire a chart event
9714
			} else {
9715
				extend(e, this.getCoordinates(e));
9716
 
9717
				// fire a click event in the chart
9718
				if (chart.isInsidePlot(e.chartX - plotLeft, e.chartY - plotTop)) {
9719
					fireEvent(chart, 'click', e);
9720
				}
9721
			}
9722
 
9723
 
9724
		}
9725
	},
9726
 
9727
	/**
9728
	 * Set the JS DOM events on the container and document. This method should contain
9729
	 * a one-to-one assignment between methods and their handlers. Any advanced logic should
9730
	 * be moved to the handler reflecting the event's name.
9731
	 */
9732
	setDOMEvents: function () {
9733
 
9734
		var pointer = this,
9735
			container = pointer.chart.container;
9736
 
9737
		container.onmousedown = function (e) {
9738
			pointer.onContainerMouseDown(e);
9739
		};
9740
		container.onmousemove = function (e) {
9741
			pointer.onContainerMouseMove(e);
9742
		};
9743
		container.onclick = function (e) {
9744
			pointer.onContainerClick(e);
9745
		};
9746
		addEvent(container, 'mouseleave', pointer.onContainerMouseLeave);
9747
		if (chartCount === 1) {
9748
			addEvent(doc, 'mouseup', pointer.onDocumentMouseUp);
9749
		}
9750
		if (hasTouch) {
9751
			container.ontouchstart = function (e) {
9752
				pointer.onContainerTouchStart(e);
9753
			};
9754
			container.ontouchmove = function (e) {
9755
				pointer.onContainerTouchMove(e);
9756
			};
9757
			if (chartCount === 1) {
9758
				addEvent(doc, 'touchend', pointer.onDocumentTouchEnd);
9759
			}
9760
		}
9761
 
9762
	},
9763
 
9764
	/**
9765
	 * Destroys the Pointer object and disconnects DOM events.
9766
	 */
9767
	destroy: function () {
9768
		var prop;
9769
 
9770
		removeEvent(this.chart.container, 'mouseleave', this.onContainerMouseLeave);
9771
		if (!chartCount) {
9772
			removeEvent(doc, 'mouseup', this.onDocumentMouseUp);
9773
			removeEvent(doc, 'touchend', this.onDocumentTouchEnd);
9774
		}
9775
 
9776
		// memory and CPU leak
9777
		clearInterval(this.tooltipTimeout);
9778
 
9779
		for (prop in this) {
9780
			this[prop] = null;
9781
		}
9782
	}
9783
};
9784
 
9785
 
9786
/* Support for touch devices */
9787
extend(Highcharts.Pointer.prototype, {
9788
 
9789
	/**
9790
	 * Run translation operations
9791
	 */
9792
	pinchTranslate: function (pinchDown, touches, transform, selectionMarker, clip, lastValidTouch) {
9793
		if (this.zoomHor || this.pinchHor) {
9794
			this.pinchTranslateDirection(true, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch);
9795
		}
9796
		if (this.zoomVert || this.pinchVert) {
9797
			this.pinchTranslateDirection(false, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch);
9798
		}
9799
	},
9800
 
9801
	/**
9802
	 * Run translation operations for each direction (horizontal and vertical) independently
9803
	 */
9804
	pinchTranslateDirection: function (horiz, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch, forcedScale) {
9805
		var chart = this.chart,
9806
			xy = horiz ? 'x' : 'y',
9807
			XY = horiz ? 'X' : 'Y',
9808
			sChartXY = 'chart' + XY,
9809
			wh = horiz ? 'width' : 'height',
9810
			plotLeftTop = chart['plot' + (horiz ? 'Left' : 'Top')],
9811
			selectionWH,
9812
			selectionXY,
9813
			clipXY,
9814
			scale = forcedScale || 1,
9815
			inverted = chart.inverted,
9816
			bounds = chart.bounds[horiz ? 'h' : 'v'],
9817
			singleTouch = pinchDown.length === 1,
9818
			touch0Start = pinchDown[0][sChartXY],
9819
			touch0Now = touches[0][sChartXY],
9820
			touch1Start = !singleTouch && pinchDown[1][sChartXY],
9821
			touch1Now = !singleTouch && touches[1][sChartXY],
9822
			outOfBounds,
9823
			transformScale,
9824
			scaleKey,
9825
			setScale = function () {
9826
				if (!singleTouch && mathAbs(touch0Start - touch1Start) > 20) { // Don't zoom if fingers are too close on this axis
9827
					scale = forcedScale || mathAbs(touch0Now - touch1Now) / mathAbs(touch0Start - touch1Start);
9828
				}
9829
 
9830
				clipXY = ((plotLeftTop - touch0Now) / scale) + touch0Start;
9831
				selectionWH = chart['plot' + (horiz ? 'Width' : 'Height')] / scale;
9832
			};
9833
 
9834
		// Set the scale, first pass
9835
		setScale();
9836
 
9837
		selectionXY = clipXY; // the clip position (x or y) is altered if out of bounds, the selection position is not
9838
 
9839
		// Out of bounds
9840
		if (selectionXY < bounds.min) {
9841
			selectionXY = bounds.min;
9842
			outOfBounds = true;
9843
		} else if (selectionXY + selectionWH > bounds.max) {
9844
			selectionXY = bounds.max - selectionWH;
9845
			outOfBounds = true;
9846
		}
9847
 
9848
		// Is the chart dragged off its bounds, determined by dataMin and dataMax?
9849
		if (outOfBounds) {
9850
 
9851
			// Modify the touchNow position in order to create an elastic drag movement. This indicates
9852
			// to the user that the chart is responsive but can't be dragged further.
9853
			touch0Now -= 0.8 * (touch0Now - lastValidTouch[xy][0]);
9854
			if (!singleTouch) {
9855
				touch1Now -= 0.8 * (touch1Now - lastValidTouch[xy][1]);
9856
			}
9857
 
9858
			// Set the scale, second pass to adapt to the modified touchNow positions
9859
			setScale();
9860
 
9861
		} else {
9862
			lastValidTouch[xy] = [touch0Now, touch1Now];
9863
		}
9864
 
9865
		// Set geometry for clipping, selection and transformation
9866
		if (!inverted) { // TODO: implement clipping for inverted charts
9867
			clip[xy] = clipXY - plotLeftTop;
9868
			clip[wh] = selectionWH;
9869
		}
9870
		scaleKey = inverted ? (horiz ? 'scaleY' : 'scaleX') : 'scale' + XY;
9871
		transformScale = inverted ? 1 / scale : scale;
9872
 
9873
		selectionMarker[wh] = selectionWH;
9874
		selectionMarker[xy] = selectionXY;
9875
		transform[scaleKey] = scale;
9876
		transform['translate' + XY] = (transformScale * plotLeftTop) + (touch0Now - (transformScale * touch0Start));
9877
	},
9878
 
9879
	/**
9880
	 * Handle touch events with two touches
9881
	 */
9882
	pinch: function (e) {
9883
 
9884
		var self = this,
9885
			chart = self.chart,
9886
			pinchDown = self.pinchDown,
9887
			touches = e.touches,
9888
			touchesLength = touches.length,
9889
			lastValidTouch = self.lastValidTouch,
9890
			hasZoom = self.hasZoom,
9891
			selectionMarker = self.selectionMarker,
9892
			transform = {},
9893
			fireClickEvent = touchesLength === 1 && ((self.inClass(e.target, PREFIX + 'tracker') &&
9894
				chart.runTrackerClick) || self.runChartClick),
9895
			clip = {};
9896
 
9897
		// Don't initiate panning until the user has pinched. This prevents us from
9898
		// blocking page scrolling as users scroll down a long page (#4210).
9899
		if (touchesLength > 1) {
9900
			self.initiated = true;
9901
		}
9902
 
9903
		// On touch devices, only proceed to trigger click if a handler is defined
9904
		if (hasZoom && self.initiated && !fireClickEvent) {
9905
			e.preventDefault();
9906
		}
9907
 
9908
		// Normalize each touch
9909
		map(touches, function (e) {
9910
			return self.normalize(e);
9911
		});
9912
 
9913
		// Register the touch start position
9914
		if (e.type === 'touchstart') {
9915
			each(touches, function (e, i) {
9916
				pinchDown[i] = { chartX: e.chartX, chartY: e.chartY };
9917
			});
9918
			lastValidTouch.x = [pinchDown[0].chartX, pinchDown[1] && pinchDown[1].chartX];
9919
			lastValidTouch.y = [pinchDown[0].chartY, pinchDown[1] && pinchDown[1].chartY];
9920
 
9921
			// Identify the data bounds in pixels
9922
			each(chart.axes, function (axis) {
9923
				if (axis.zoomEnabled) {
9924
					var bounds = chart.bounds[axis.horiz ? 'h' : 'v'],
9925
						minPixelPadding = axis.minPixelPadding,
9926
						min = axis.toPixels(pick(axis.options.min, axis.dataMin)),
9927
						max = axis.toPixels(pick(axis.options.max, axis.dataMax)),
9928
						absMin = mathMin(min, max),
9929
						absMax = mathMax(min, max);
9930
 
9931
					// Store the bounds for use in the touchmove handler
9932
					bounds.min = mathMin(axis.pos, absMin - minPixelPadding);
9933
					bounds.max = mathMax(axis.pos + axis.len, absMax + minPixelPadding);
9934
				}
9935
			});
9936
			self.res = true; // reset on next move
9937
 
9938
		// Event type is touchmove, handle panning and pinching
9939
		} else if (pinchDown.length) { // can be 0 when releasing, if touchend fires first
9940
 
9941
 
9942
			// Set the marker
9943
			if (!selectionMarker) {
9944
				self.selectionMarker = selectionMarker = extend({
9945
					destroy: noop,
9946
					touch: true
9947
				}, chart.plotBox);
9948
			}
9949
 
9950
			self.pinchTranslate(pinchDown, touches, transform, selectionMarker, clip, lastValidTouch);
9951
 
9952
			self.hasPinched = hasZoom;
9953
 
9954
			// Scale and translate the groups to provide visual feedback during pinching
9955
			self.scaleGroups(transform, clip);
9956
 
9957
			// Optionally move the tooltip on touchmove
9958
			if (!hasZoom && self.followTouchMove && touchesLength === 1) {
9959
				this.runPointActions(self.normalize(e));
9960
			} else if (self.res) {
9961
				self.res = false;
9962
				this.reset(false, 0);
9963
			}
9964
		}
9965
	},
9966
 
9967
	/**
9968
	 * General touch handler shared by touchstart and touchmove.
9969
	 */
9970
	touch: function (e, start) {
9971
		var chart = this.chart;
9972
 
9973
		hoverChartIndex = chart.index;
9974
 
9975
		if (e.touches.length === 1) {
9976
 
9977
			e = this.normalize(e);
9978
 
9979
			if (chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop) && !chart.openMenu) {
9980
 
9981
				// Run mouse events and display tooltip etc
9982
				if (start) {
9983
					this.runPointActions(e);
9984
				}
9985
 
9986
				this.pinch(e);
9987
 
9988
			} else if (start) {
9989
				// Hide the tooltip on touching outside the plot area (#1203)
9990
				this.reset();
9991
			}
9992
 
9993
		} else if (e.touches.length === 2) {
9994
			this.pinch(e);
9995
		}
9996
	},
9997
 
9998
	onContainerTouchStart: function (e) {
9999
		this.touch(e, true);
10000
	},
10001
 
10002
	onContainerTouchMove: function (e) {
10003
		this.touch(e);
10004
	},
10005
 
10006
	onDocumentTouchEnd: function (e) {
10007
		if (charts[hoverChartIndex]) {
10008
			charts[hoverChartIndex].pointer.drop(e);
10009
		}
10010
	}
10011
 
10012
});
10013
if (win.PointerEvent || win.MSPointerEvent) {
10014
 
10015
	// The touches object keeps track of the points being touched at all times
10016
	var touches = {},
10017
		hasPointerEvent = !!win.PointerEvent,
10018
		getWebkitTouches = function () {
10019
			var key, fake = [];
10020
			fake.item = function (i) { return this[i]; };
10021
			for (key in touches) {
10022
				if (touches.hasOwnProperty(key)) {
10023
					fake.push({
10024
						pageX: touches[key].pageX,
10025
						pageY: touches[key].pageY,
10026
						target: touches[key].target
10027
					});
10028
				}
10029
			}
10030
			return fake;
10031
		},
10032
		translateMSPointer = function (e, method, wktype, callback) {
10033
			var p;
10034
			e = e.originalEvent || e;
10035
			if ((e.pointerType === 'touch' || e.pointerType === e.MSPOINTER_TYPE_TOUCH) && charts[hoverChartIndex]) {
10036
				callback(e);
10037
				p = charts[hoverChartIndex].pointer;
10038
				p[method]({
10039
					type: wktype,
10040
					target: e.currentTarget,
10041
					preventDefault: noop,
10042
					touches: getWebkitTouches()
10043
				});
10044
			}
10045
		};
10046
 
10047
	/**
10048
	 * Extend the Pointer prototype with methods for each event handler and more
10049
	 */
10050
	extend(Pointer.prototype, {
10051
		onContainerPointerDown: function (e) {
10052
			translateMSPointer(e, 'onContainerTouchStart', 'touchstart', function (e) {
10053
				touches[e.pointerId] = { pageX: e.pageX, pageY: e.pageY, target: e.currentTarget };
10054
			});
10055
		},
10056
		onContainerPointerMove: function (e) {
10057
			translateMSPointer(e, 'onContainerTouchMove', 'touchmove', function (e) {
10058
				touches[e.pointerId] = { pageX: e.pageX, pageY: e.pageY };
10059
				if (!touches[e.pointerId].target) {
10060
					touches[e.pointerId].target = e.currentTarget;
10061
				}
10062
			});
10063
		},
10064
		onDocumentPointerUp: function (e) {
10065
			translateMSPointer(e, 'onDocumentTouchEnd', 'touchend', function (e) {
10066
				delete touches[e.pointerId];
10067
			});
10068
		},
10069
 
10070
		/**
10071
		 * Add or remove the MS Pointer specific events
10072
		 */
10073
		batchMSEvents: function (fn) {
10074
			fn(this.chart.container, hasPointerEvent ? 'pointerdown' : 'MSPointerDown', this.onContainerPointerDown);
10075
			fn(this.chart.container, hasPointerEvent ? 'pointermove' : 'MSPointerMove', this.onContainerPointerMove);
10076
			fn(doc, hasPointerEvent ? 'pointerup' : 'MSPointerUp', this.onDocumentPointerUp);
10077
		}
10078
	});
10079
 
10080
	// Disable default IE actions for pinch and such on chart element
10081
	wrap(Pointer.prototype, 'init', function (proceed, chart, options) {
10082
		proceed.call(this, chart, options);
10083
		if (this.hasZoom) { // #4014
10084
			css(chart.container, {
10085
				'-ms-touch-action': NONE,
10086
				'touch-action': NONE
10087
			});
10088
		}
10089
	});
10090
 
10091
	// Add IE specific touch events to chart
10092
	wrap(Pointer.prototype, 'setDOMEvents', function (proceed) {
10093
		proceed.apply(this);
10094
		if (this.hasZoom || this.followTouchMove) {
10095
			this.batchMSEvents(addEvent);
10096
		}
10097
	});
10098
	// Destroy MS events also
10099
	wrap(Pointer.prototype, 'destroy', function (proceed) {
10100
		this.batchMSEvents(removeEvent);
10101
		proceed.call(this);
10102
	});
10103
}
10104
/**
10105
 * The overview of the chart's series
10106
 */
10107
var Legend = Highcharts.Legend = function (chart, options) {
10108
	this.init(chart, options);
10109
};
10110
 
10111
Legend.prototype = {
10112
 
10113
	/**
10114
	 * Initialize the legend
10115
	 */
10116
	init: function (chart, options) {
10117
 
10118
		var legend = this,
10119
			itemStyle = options.itemStyle,
10120
			padding,
10121
			itemMarginTop = options.itemMarginTop || 0;
10122
 
10123
		this.options = options;
10124
 
10125
		if (!options.enabled) {
10126
			return;
10127
		}
10128
 
10129
		legend.itemStyle = itemStyle;
10130
		legend.itemHiddenStyle = merge(itemStyle, options.itemHiddenStyle);
10131
		legend.itemMarginTop = itemMarginTop;
10132
		legend.padding = padding = pick(options.padding, 8);
10133
		legend.initialItemX = padding;
10134
		legend.initialItemY = padding - 5; // 5 is the number of pixels above the text
10135
		legend.maxItemWidth = 0;
10136
		legend.chart = chart;
10137
		legend.itemHeight = 0;
10138
		legend.symbolWidth = pick(options.symbolWidth, 16);
10139
		legend.pages = [];
10140
 
10141
 
10142
		// Render it
10143
		legend.render();
10144
 
10145
		// move checkboxes
10146
		addEvent(legend.chart, 'endResize', function () {
10147
			legend.positionCheckboxes();
10148
		});
10149
 
10150
	},
10151
 
10152
	/**
10153
	 * Set the colors for the legend item
10154
	 * @param {Object} item A Series or Point instance
10155
	 * @param {Object} visible Dimmed or colored
10156
	 */
10157
	colorizeItem: function (item, visible) {
10158
		var legend = this,
10159
			options = legend.options,
10160
			legendItem = item.legendItem,
10161
			legendLine = item.legendLine,
10162
			legendSymbol = item.legendSymbol,
10163
			hiddenColor = legend.itemHiddenStyle.color,
10164
			textColor = visible ? options.itemStyle.color : hiddenColor,
10165
			symbolColor = visible ? (item.legendColor || item.color || '#CCC') : hiddenColor,
10166
			markerOptions = item.options && item.options.marker,
10167
			symbolAttr = { fill: symbolColor },
10168
			key,
10169
			val;
10170
 
10171
		if (legendItem) {
10172
			legendItem.css({ fill: textColor, color: textColor }); // color for #1553, oldIE
10173
		}
10174
		if (legendLine) {
10175
			legendLine.attr({ stroke: symbolColor });
10176
		}
10177
 
10178
		if (legendSymbol) {
10179
 
10180
			// Apply marker options
10181
			if (markerOptions && legendSymbol.isMarker) { // #585
10182
				symbolAttr.stroke = symbolColor;
10183
				markerOptions = item.convertAttribs(markerOptions);
10184
				for (key in markerOptions) {
10185
					val = markerOptions[key];
10186
					if (val !== UNDEFINED) {
10187
						symbolAttr[key] = val;
10188
					}
10189
				}
10190
			}
10191
 
10192
			legendSymbol.attr(symbolAttr);
10193
		}
10194
	},
10195
 
10196
	/**
10197
	 * Position the legend item
10198
	 * @param {Object} item A Series or Point instance
10199
	 */
10200
	positionItem: function (item) {
10201
		var legend = this,
10202
			options = legend.options,
10203
			symbolPadding = options.symbolPadding,
10204
			ltr = !options.rtl,
10205
			legendItemPos = item._legendItemPos,
10206
			itemX = legendItemPos[0],
10207
			itemY = legendItemPos[1],
10208
			checkbox = item.checkbox,
10209
			legendGroup = item.legendGroup;
10210
 
10211
		if (legendGroup && legendGroup.element) {
10212
			legendGroup.translate(
10213
				ltr ? itemX : legend.legendWidth - itemX - 2 * symbolPadding - 4,
10214
				itemY
10215
			);
10216
		}
10217
 
10218
		if (checkbox) {
10219
			checkbox.x = itemX;
10220
			checkbox.y = itemY;
10221
		}
10222
	},
10223
 
10224
	/**
10225
	 * Destroy a single legend item
10226
	 * @param {Object} item The series or point
10227
	 */
10228
	destroyItem: function (item) {
10229
		var checkbox = item.checkbox;
10230
 
10231
		// destroy SVG elements
10232
		each(['legendItem', 'legendLine', 'legendSymbol', 'legendGroup'], function (key) {
10233
			if (item[key]) {
10234
				item[key] = item[key].destroy();
10235
			}
10236
		});
10237
 
10238
		if (checkbox) {
10239
			discardElement(item.checkbox);
10240
		}
10241
	},
10242
 
10243
	/**
10244
	 * Destroys the legend.
10245
	 */
10246
	destroy: function () {
10247
		var legend = this,
10248
			legendGroup = legend.group,
10249
			box = legend.box;
10250
 
10251
		if (box) {
10252
			legend.box = box.destroy();
10253
		}
10254
 
10255
		if (legendGroup) {
10256
			legend.group = legendGroup.destroy();
10257
		}
10258
	},
10259
 
10260
	/**
10261
	 * Position the checkboxes after the width is determined
10262
	 */
10263
	positionCheckboxes: function (scrollOffset) {
10264
		var alignAttr = this.group.alignAttr,
10265
			translateY,
10266
			clipHeight = this.clipHeight || this.legendHeight;
10267
 
10268
		if (alignAttr) {
10269
			translateY = alignAttr.translateY;
10270
			each(this.allItems, function (item) {
10271
				var checkbox = item.checkbox,
10272
					top;
10273
 
10274
				if (checkbox) {
10275
					top = (translateY + checkbox.y + (scrollOffset || 0) + 3);
10276
					css(checkbox, {
10277
						left: (alignAttr.translateX + item.checkboxOffset + checkbox.x - 20) + PX,
10278
						top: top + PX,
10279
						display: top > translateY - 6 && top < translateY + clipHeight - 6 ? '' : NONE
10280
					});
10281
				}
10282
			});
10283
		}
10284
	},
10285
 
10286
	/**
10287
	 * Render the legend title on top of the legend
10288
	 */
10289
	renderTitle: function () {
10290
		var options = this.options,
10291
			padding = this.padding,
10292
			titleOptions = options.title,
10293
			titleHeight = 0,
10294
			bBox;
10295
 
10296
		if (titleOptions.text) {
10297
			if (!this.title) {
10298
				this.title = this.chart.renderer.label(titleOptions.text, padding - 3, padding - 4, null, null, null, null, null, 'legend-title')
10299
					.attr({ zIndex: 1 })
10300
					.css(titleOptions.style)
10301
					.add(this.group);
10302
			}
10303
			bBox = this.title.getBBox();
10304
			titleHeight = bBox.height;
10305
			this.offsetWidth = bBox.width; // #1717
10306
			this.contentGroup.attr({ translateY: titleHeight });
10307
		}
10308
		this.titleHeight = titleHeight;
10309
	},
10310
 
10311
	/**
10312
	 * Set the legend item text
10313
	 */
10314
	setText: function (item) {
10315
		var options = this.options;
10316
		item.legendItem.attr({
10317
			text: options.labelFormat ? format(options.labelFormat, item) : options.labelFormatter.call(item)
10318
		});
10319
	},
10320
 
10321
	/**
10322
	 * Render a single specific legend item
10323
	 * @param {Object} item A series or point
10324
	 */
10325
	renderItem: function (item) {
10326
		var legend = this,
10327
			chart = legend.chart,
10328
			renderer = chart.renderer,
10329
			options = legend.options,
10330
			horizontal = options.layout === 'horizontal',
10331
			symbolWidth = legend.symbolWidth,
10332
			symbolPadding = options.symbolPadding,
10333
			itemStyle = legend.itemStyle,
10334
			itemHiddenStyle = legend.itemHiddenStyle,
10335
			padding = legend.padding,
10336
			itemDistance = horizontal ? pick(options.itemDistance, 20) : 0,
10337
			ltr = !options.rtl,
10338
			itemHeight,
10339
			widthOption = options.width,
10340
			itemMarginBottom = options.itemMarginBottom || 0,
10341
			itemMarginTop = legend.itemMarginTop,
10342
			initialItemX = legend.initialItemX,
10343
			bBox,
10344
			itemWidth,
10345
			li = item.legendItem,
10346
			series = item.series && item.series.drawLegendSymbol ? item.series : item,
10347
			seriesOptions = series.options,
10348
			showCheckbox = legend.createCheckboxForItem && seriesOptions && seriesOptions.showCheckbox,
10349
			useHTML = options.useHTML;
10350
 
10351
		if (!li) { // generate it once, later move it
10352
 
10353
			// Generate the group box
10354
			// A group to hold the symbol and text. Text is to be appended in Legend class.
10355
			item.legendGroup = renderer.g('legend-item')
10356
				.attr({ zIndex: 1 })
10357
				.add(legend.scrollGroup);
10358
 
10359
			// Generate the list item text and add it to the group
10360
			item.legendItem = li = renderer.text(
10361
					'',
10362
					ltr ? symbolWidth + symbolPadding : -symbolPadding,
10363
					legend.baseline || 0,
10364
					useHTML
10365
				)
10366
				.css(merge(item.visible ? itemStyle : itemHiddenStyle)) // merge to prevent modifying original (#1021)
10367
				.attr({
10368
					align: ltr ? 'left' : 'right',
10369
					zIndex: 2
10370
				})
10371
				.add(item.legendGroup);
10372
 
10373
			// Get the baseline for the first item - the font size is equal for all
10374
			if (!legend.baseline) {
10375
				legend.fontMetrics = renderer.fontMetrics(itemStyle.fontSize, li);
10376
				legend.baseline = legend.fontMetrics.f + 3 + itemMarginTop;
10377
				li.attr('y', legend.baseline);
10378
			}
10379
 
10380
			// Draw the legend symbol inside the group box
10381
			series.drawLegendSymbol(legend, item);
10382
 
10383
			if (legend.setItemEvents) {
10384
				legend.setItemEvents(item, li, useHTML, itemStyle, itemHiddenStyle);
10385
			}
10386
 
10387
			// Colorize the items
10388
			legend.colorizeItem(item, item.visible);
10389
 
10390
			// add the HTML checkbox on top
10391
			if (showCheckbox) {
10392
				legend.createCheckboxForItem(item);
10393
			}
10394
		}
10395
 
10396
		// Always update the text
10397
		legend.setText(item);
10398
 
10399
		// calculate the positions for the next line
10400
		bBox = li.getBBox();
10401
 
10402
		itemWidth = item.checkboxOffset =
10403
			options.itemWidth ||
10404
			item.legendItemWidth ||
10405
			symbolWidth + symbolPadding + bBox.width + itemDistance + (showCheckbox ? 20 : 0);
10406
		legend.itemHeight = itemHeight = mathRound(item.legendItemHeight || bBox.height);
10407
 
10408
		// if the item exceeds the width, start a new line
10409
		if (horizontal && legend.itemX - initialItemX + itemWidth >
10410
				(widthOption || (chart.chartWidth - 2 * padding - initialItemX - options.x))) {
10411
			legend.itemX = initialItemX;
10412
			legend.itemY += itemMarginTop + legend.lastLineHeight + itemMarginBottom;
10413
			legend.lastLineHeight = 0; // reset for next line (#915, #3976)
10414
		}
10415
 
10416
		// If the item exceeds the height, start a new column
10417
		/*if (!horizontal && legend.itemY + options.y + itemHeight > chart.chartHeight - spacingTop - spacingBottom) {
10418
			legend.itemY = legend.initialItemY;
10419
			legend.itemX += legend.maxItemWidth;
10420
			legend.maxItemWidth = 0;
10421
		}*/
10422
 
10423
		// Set the edge positions
10424
		legend.maxItemWidth = mathMax(legend.maxItemWidth, itemWidth);
10425
		legend.lastItemY = itemMarginTop + legend.itemY + itemMarginBottom;
10426
		legend.lastLineHeight = mathMax(itemHeight, legend.lastLineHeight); // #915
10427
 
10428
		// cache the position of the newly generated or reordered items
10429
		item._legendItemPos = [legend.itemX, legend.itemY];
10430
 
10431
		// advance
10432
		if (horizontal) {
10433
			legend.itemX += itemWidth;
10434
 
10435
		} else {
10436
			legend.itemY += itemMarginTop + itemHeight + itemMarginBottom;
10437
			legend.lastLineHeight = itemHeight;
10438
		}
10439
 
10440
		// the width of the widest item
10441
		legend.offsetWidth = widthOption || mathMax(
10442
			(horizontal ? legend.itemX - initialItemX - itemDistance : itemWidth) + padding,
10443
			legend.offsetWidth
10444
		);
10445
	},
10446
 
10447
	/**
10448
	 * Get all items, which is one item per series for normal series and one item per point
10449
	 * for pie series.
10450
	 */
10451
	getAllItems: function () {
10452
		var allItems = [];
10453
		each(this.chart.series, function (series) {
10454
			var seriesOptions = series.options;
10455
 
10456
			// Handle showInLegend. If the series is linked to another series, defaults to false.
10457
			if (!pick(seriesOptions.showInLegend, !defined(seriesOptions.linkedTo) ? UNDEFINED : false, true)) {
10458
				return;
10459
			}
10460
 
10461
			// use points or series for the legend item depending on legendType
10462
			allItems = allItems.concat(
10463
					series.legendItems ||
10464
					(seriesOptions.legendType === 'point' ?
10465
							series.data :
10466
							series)
10467
			);
10468
		});
10469
		return allItems;
10470
	},
10471
 
10472
	/**
10473
	 * Adjust the chart margins by reserving space for the legend on only one side
10474
	 * of the chart. If the position is set to a corner, top or bottom is reserved
10475
	 * for horizontal legends and left or right for vertical ones.
10476
	 */
10477
	adjustMargins: function (margin, spacing) {
10478
		var chart = this.chart,
10479
			options = this.options,
10480
			// Use the first letter of each alignment option in order to detect the side
10481
			alignment = options.align.charAt(0) + options.verticalAlign.charAt(0) + options.layout.charAt(0); // #4189 - use charAt(x) notation instead of [x] for IE7
10482
 
10483
		if (this.display && !options.floating) {
10484
 
10485
			each([
10486
				/(lth|ct|rth)/,
10487
				/(rtv|rm|rbv)/,
10488
				/(rbh|cb|lbh)/,
10489
				/(lbv|lm|ltv)/
10490
			], function (alignments, side) {
10491
				if (alignments.test(alignment) && !defined(margin[side])) {
10492
					// Now we have detected on which side of the chart we should reserve space for the legend
10493
					chart[marginNames[side]] = mathMax(
10494
						chart[marginNames[side]],
10495
						chart.legend[(side + 1) % 2 ? 'legendHeight' : 'legendWidth'] +
10496
							[1, -1, -1, 1][side] * options[(side % 2) ? 'x' : 'y'] +
10497
							pick(options.margin, 12) +
10498
							spacing[side]
10499
					);
10500
				}
10501
			});
10502
		}
10503
	},
10504
 
10505
	/**
10506
	 * Render the legend. This method can be called both before and after
10507
	 * chart.render. If called after, it will only rearrange items instead
10508
	 * of creating new ones.
10509
	 */
10510
	render: function () {
10511
		var legend = this,
10512
			chart = legend.chart,
10513
			renderer = chart.renderer,
10514
			legendGroup = legend.group,
10515
			allItems,
10516
			display,
10517
			legendWidth,
10518
			legendHeight,
10519
			box = legend.box,
10520
			options = legend.options,
10521
			padding = legend.padding,
10522
			legendBorderWidth = options.borderWidth,
10523
			legendBackgroundColor = options.backgroundColor;
10524
 
10525
		legend.itemX = legend.initialItemX;
10526
		legend.itemY = legend.initialItemY;
10527
		legend.offsetWidth = 0;
10528
		legend.lastItemY = 0;
10529
 
10530
		if (!legendGroup) {
10531
			legend.group = legendGroup = renderer.g('legend')
10532
				.attr({ zIndex: 7 })
10533
				.add();
10534
			legend.contentGroup = renderer.g()
10535
				.attr({ zIndex: 1 }) // above background
10536
				.add(legendGroup);
10537
			legend.scrollGroup = renderer.g()
10538
				.add(legend.contentGroup);
10539
		}
10540
 
10541
		legend.renderTitle();
10542
 
10543
		// add each series or point
10544
		allItems = legend.getAllItems();
10545
 
10546
		// sort by legendIndex
10547
		stableSort(allItems, function (a, b) {
10548
			return ((a.options && a.options.legendIndex) || 0) - ((b.options && b.options.legendIndex) || 0);
10549
		});
10550
 
10551
		// reversed legend
10552
		if (options.reversed) {
10553
			allItems.reverse();
10554
		}
10555
 
10556
		legend.allItems = allItems;
10557
		legend.display = display = !!allItems.length;
10558
 
10559
		// render the items
10560
		legend.lastLineHeight = 0;
10561
		each(allItems, function (item) {
10562
			legend.renderItem(item);
10563
		});
10564
 
10565
		// Get the box
10566
		legendWidth = (options.width || legend.offsetWidth) + padding;
10567
		legendHeight = legend.lastItemY + legend.lastLineHeight + legend.titleHeight;
10568
		legendHeight = legend.handleOverflow(legendHeight);
10569
		legendHeight += padding;
10570
 
10571
		// Draw the border and/or background
10572
		if (legendBorderWidth || legendBackgroundColor) {
10573
 
10574
			if (!box) {
10575
				legend.box = box = renderer.rect(
10576
					0,
10577
					0,
10578
					legendWidth,
10579
					legendHeight,
10580
					options.borderRadius,
10581
					legendBorderWidth || 0
10582
				).attr({
10583
					stroke: options.borderColor,
10584
					'stroke-width': legendBorderWidth || 0,
10585
					fill: legendBackgroundColor || NONE
10586
				})
10587
				.add(legendGroup)
10588
				.shadow(options.shadow);
10589
				box.isNew = true;
10590
 
10591
			} else if (legendWidth > 0 && legendHeight > 0) {
10592
				box[box.isNew ? 'attr' : 'animate'](
10593
					box.crisp({ width: legendWidth, height: legendHeight })
10594
				);
10595
				box.isNew = false;
10596
			}
10597
 
10598
			// hide the border if no items
10599
			box[display ? 'show' : 'hide']();
10600
		}
10601
 
10602
		legend.legendWidth = legendWidth;
10603
		legend.legendHeight = legendHeight;
10604
 
10605
		// Now that the legend width and height are established, put the items in the
10606
		// final position
10607
		each(allItems, function (item) {
10608
			legend.positionItem(item);
10609
		});
10610
 
10611
		// 1.x compatibility: positioning based on style
10612
		/*var props = ['left', 'right', 'top', 'bottom'],
10613
			prop,
10614
			i = 4;
10615
		while (i--) {
10616
			prop = props[i];
10617
			if (options.style[prop] && options.style[prop] !== 'auto') {
10618
				options[i < 2 ? 'align' : 'verticalAlign'] = prop;
10619
				options[i < 2 ? 'x' : 'y'] = pInt(options.style[prop]) * (i % 2 ? -1 : 1);
10620
			}
10621
		}*/
10622
 
10623
		if (display) {
10624
			legendGroup.align(extend({
10625
				width: legendWidth,
10626
				height: legendHeight
10627
			}, options), true, 'spacingBox');
10628
		}
10629
 
10630
		if (!chart.isResizing) {
10631
			this.positionCheckboxes();
10632
		}
10633
	},
10634
 
10635
	/**
10636
	 * Set up the overflow handling by adding navigation with up and down arrows below the
10637
	 * legend.
10638
	 */
10639
	handleOverflow: function (legendHeight) {
10640
		var legend = this,
10641
			chart = this.chart,
10642
			renderer = chart.renderer,
10643
			options = this.options,
10644
			optionsY = options.y,
10645
			alignTop = options.verticalAlign === 'top',
10646
			spaceHeight = chart.spacingBox.height + (alignTop ? -optionsY : optionsY) - this.padding,
10647
			maxHeight = options.maxHeight,
10648
			clipHeight,
10649
			clipRect = this.clipRect,
10650
			navOptions = options.navigation,
10651
			animation = pick(navOptions.animation, true),
10652
			arrowSize = navOptions.arrowSize || 12,
10653
			nav = this.nav,
10654
			pages = this.pages,
10655
			padding = this.padding,
10656
			lastY,
10657
			allItems = this.allItems,
10658
			clipToHeight = function (height) {
10659
				clipRect.attr({
10660
					height: height
10661
				});
10662
 
10663
				// useHTML
10664
				if (legend.contentGroup.div) {
10665
					legend.contentGroup.div.style.clip = 'rect(' + padding + 'px,9999px,' + (padding + height) + 'px,0)';
10666
				}
10667
			};
10668
 
10669
 
10670
		// Adjust the height
10671
		if (options.layout === 'horizontal') {
10672
			spaceHeight /= 2;
10673
		}
10674
		if (maxHeight) {
10675
			spaceHeight = mathMin(spaceHeight, maxHeight);
10676
		}
10677
 
10678
		// Reset the legend height and adjust the clipping rectangle
10679
		pages.length = 0;
10680
		if (legendHeight > spaceHeight) {
10681
 
10682
			this.clipHeight = clipHeight = mathMax(spaceHeight - 20 - this.titleHeight - padding, 0);
10683
			this.currentPage = pick(this.currentPage, 1);
10684
			this.fullHeight = legendHeight;
10685
 
10686
			// Fill pages with Y positions so that the top of each a legend item defines
10687
			// the scroll top for each page (#2098)
10688
			each(allItems, function (item, i) {
10689
				var y = item._legendItemPos[1],
10690
					h = mathRound(item.legendItem.getBBox().height),
10691
					len = pages.length;
10692
 
10693
				if (!len || (y - pages[len - 1] > clipHeight && (lastY || y) !== pages[len - 1])) {
10694
					pages.push(lastY || y);
10695
					len++;
10696
				}
10697
 
10698
				if (i === allItems.length - 1 && y + h - pages[len - 1] > clipHeight) {
10699
					pages.push(y);
10700
				}
10701
				if (y !== lastY) {
10702
					lastY = y;
10703
				}
10704
			});
10705
 
10706
			// Only apply clipping if needed. Clipping causes blurred legend in PDF export (#1787)
10707
			if (!clipRect) {
10708
				clipRect = legend.clipRect = renderer.clipRect(0, padding, 9999, 0);
10709
				legend.contentGroup.clip(clipRect);
10710
			}
10711
 
10712
			clipToHeight(clipHeight);
10713
 
10714
			// Add navigation elements
10715
			if (!nav) {
10716
				this.nav = nav = renderer.g().attr({ zIndex: 1 }).add(this.group);
10717
				this.up = renderer.symbol('triangle', 0, 0, arrowSize, arrowSize)
10718
					.on('click', function () {
10719
						legend.scroll(-1, animation);
10720
					})
10721
					.add(nav);
10722
				this.pager = renderer.text('', 15, 10)
10723
					.css(navOptions.style)
10724
					.add(nav);
10725
				this.down = renderer.symbol('triangle-down', 0, 0, arrowSize, arrowSize)
10726
					.on('click', function () {
10727
						legend.scroll(1, animation);
10728
					})
10729
					.add(nav);
10730
			}
10731
 
10732
			// Set initial position
10733
			legend.scroll(0);
10734
 
10735
			legendHeight = spaceHeight;
10736
 
10737
		} else if (nav) {
10738
			clipToHeight(chart.chartHeight);
10739
			nav.hide();
10740
			this.scrollGroup.attr({
10741
				translateY: 1
10742
			});
10743
			this.clipHeight = 0; // #1379
10744
		}
10745
 
10746
		return legendHeight;
10747
	},
10748
 
10749
	/**
10750
	 * Scroll the legend by a number of pages
10751
	 * @param {Object} scrollBy
10752
	 * @param {Object} animation
10753
	 */
10754
	scroll: function (scrollBy, animation) {
10755
		var pages = this.pages,
10756
			pageCount = pages.length,
10757
			currentPage = this.currentPage + scrollBy,
10758
			clipHeight = this.clipHeight,
10759
			navOptions = this.options.navigation,
10760
			activeColor = navOptions.activeColor,
10761
			inactiveColor = navOptions.inactiveColor,
10762
			pager = this.pager,
10763
			padding = this.padding,
10764
			scrollOffset;
10765
 
10766
		// When resizing while looking at the last page
10767
		if (currentPage > pageCount) {
10768
			currentPage = pageCount;
10769
		}
10770
 
10771
		if (currentPage > 0) {
10772
 
10773
			if (animation !== UNDEFINED) {
10774
				setAnimation(animation, this.chart);
10775
			}
10776
 
10777
			this.nav.attr({
10778
				translateX: padding,
10779
				translateY: clipHeight + this.padding + 7 + this.titleHeight,
10780
				visibility: VISIBLE
10781
			});
10782
			this.up.attr({
10783
					fill: currentPage === 1 ? inactiveColor : activeColor
10784
				})
10785
				.css({
10786
					cursor: currentPage === 1 ? 'default' : 'pointer'
10787
				});
10788
			pager.attr({
10789
				text: currentPage + '/' + pageCount
10790
			});
10791
			this.down.attr({
10792
					x: 18 + this.pager.getBBox().width, // adjust to text width
10793
					fill: currentPage === pageCount ? inactiveColor : activeColor
10794
				})
10795
				.css({
10796
					cursor: currentPage === pageCount ? 'default' : 'pointer'
10797
				});
10798
 
10799
			scrollOffset = -pages[currentPage - 1] + this.initialItemY;
10800
 
10801
			this.scrollGroup.animate({
10802
				translateY: scrollOffset
10803
			});
10804
 
10805
			this.currentPage = currentPage;
10806
			this.positionCheckboxes(scrollOffset);
10807
		}
10808
 
10809
	}
10810
 
10811
};
10812
 
10813
/*
10814
 * LegendSymbolMixin
10815
 */
10816
 
10817
var LegendSymbolMixin = Highcharts.LegendSymbolMixin = {
10818
 
10819
	/**
10820
	 * Get the series' symbol in the legend
10821
	 *
10822
	 * @param {Object} legend The legend object
10823
	 * @param {Object} item The series (this) or point
10824
	 */
10825
	drawRectangle: function (legend, item) {
10826
		var symbolHeight = legend.options.symbolHeight || legend.fontMetrics.f;
10827
 
10828
		item.legendSymbol = this.chart.renderer.rect(
10829
			0,
10830
			legend.baseline - symbolHeight + 1, // #3988
10831
			legend.symbolWidth,
10832
			symbolHeight,
10833
			legend.options.symbolRadius || 0
10834
		).attr({
10835
			zIndex: 3
10836
		}).add(item.legendGroup);
10837
 
10838
	},
10839
 
10840
	/**
10841
	 * Get the series' symbol in the legend. This method should be overridable to create custom
10842
	 * symbols through Highcharts.seriesTypes[type].prototype.drawLegendSymbols.
10843
	 *
10844
	 * @param {Object} legend The legend object
10845
	 */
10846
	drawLineMarker: function (legend) {
10847
 
10848
		var options = this.options,
10849
			markerOptions = options.marker,
10850
			radius,
10851
			legendSymbol,
10852
			symbolWidth = legend.symbolWidth,
10853
			renderer = this.chart.renderer,
10854
			legendItemGroup = this.legendGroup,
10855
			verticalCenter = legend.baseline - mathRound(legend.fontMetrics.b * 0.3),
10856
			attr;
10857
 
10858
		// Draw the line
10859
		if (options.lineWidth) {
10860
			attr = {
10861
				'stroke-width': options.lineWidth
10862
			};
10863
			if (options.dashStyle) {
10864
				attr.dashstyle = options.dashStyle;
10865
			}
10866
			this.legendLine = renderer.path([
10867
				M,
10868
				0,
10869
				verticalCenter,
10870
				L,
10871
				symbolWidth,
10872
				verticalCenter
10873
			])
10874
			.attr(attr)
10875
			.add(legendItemGroup);
10876
		}
10877
 
10878
		// Draw the marker
10879
		if (markerOptions && markerOptions.enabled !== false) {
10880
			radius = markerOptions.radius;
10881
			this.legendSymbol = legendSymbol = renderer.symbol(
10882
				this.symbol,
10883
				(symbolWidth / 2) - radius,
10884
				verticalCenter - radius,
10885
				2 * radius,
10886
				2 * radius
10887
			)
10888
			.add(legendItemGroup);
10889
			legendSymbol.isMarker = true;
10890
		}
10891
	}
10892
};
10893
 
10894
// Workaround for #2030, horizontal legend items not displaying in IE11 Preview,
10895
// and for #2580, a similar drawing flaw in Firefox 26.
10896
// TODO: Explore if there's a general cause for this. The problem may be related
10897
// to nested group elements, as the legend item texts are within 4 group elements.
10898
if (/Trident\/7\.0/.test(userAgent) || isFirefox) {
10899
	wrap(Legend.prototype, 'positionItem', function (proceed, item) {
10900
		var legend = this,
10901
			runPositionItem = function () { // If chart destroyed in sync, this is undefined (#2030)
10902
				if (item._legendItemPos) {
10903
					proceed.call(legend, item);
10904
				}
10905
			};
10906
 
10907
		// Do it now, for export and to get checkbox placement
10908
		runPositionItem();
10909
 
10910
		// Do it after to work around the core issue
10911
		setTimeout(runPositionItem);
10912
	});
10913
}
10914
/**
10915
 * The chart class
10916
 * @param {Object} options
10917
 * @param {Function} callback Function to run when the chart has loaded
10918
 */
10919
var Chart = Highcharts.Chart = function () {
10920
	this.init.apply(this, arguments);
10921
};
10922
 
10923
Chart.prototype = {
10924
 
10925
	/**
10926
	 * Hook for modules
10927
	 */
10928
	callbacks: [],
10929
 
10930
	/**
10931
	 * Initialize the chart
10932
	 */
10933
	init: function (userOptions, callback) {
10934
 
10935
		// Handle regular options
10936
		var options,
10937
			seriesOptions = userOptions.series; // skip merging data points to increase performance
10938
 
10939
		userOptions.series = null;
10940
		options = merge(defaultOptions, userOptions); // do the merge
10941
		options.series = userOptions.series = seriesOptions; // set back the series data
10942
		this.userOptions = userOptions;
10943
 
10944
		var optionsChart = options.chart;
10945
 
10946
		// Create margin & spacing array
10947
		this.margin = this.splashArray('margin', optionsChart);
10948
		this.spacing = this.splashArray('spacing', optionsChart);
10949
 
10950
		var chartEvents = optionsChart.events;
10951
 
10952
		//this.runChartClick = chartEvents && !!chartEvents.click;
10953
		this.bounds = { h: {}, v: {} }; // Pixel data bounds for touch zoom
10954
 
10955
		this.callback = callback;
10956
		this.isResizing = 0;
10957
		this.options = options;
10958
		//chartTitleOptions = UNDEFINED;
10959
		//chartSubtitleOptions = UNDEFINED;
10960
 
10961
		this.axes = [];
10962
		this.series = [];
10963
		this.hasCartesianSeries = optionsChart.showAxes;
10964
		//this.axisOffset = UNDEFINED;
10965
		//this.maxTicks = UNDEFINED; // handle the greatest amount of ticks on grouped axes
10966
		//this.inverted = UNDEFINED;
10967
		//this.loadingShown = UNDEFINED;
10968
		//this.container = UNDEFINED;
10969
		//this.chartWidth = UNDEFINED;
10970
		//this.chartHeight = UNDEFINED;
10971
		//this.marginRight = UNDEFINED;
10972
		//this.marginBottom = UNDEFINED;
10973
		//this.containerWidth = UNDEFINED;
10974
		//this.containerHeight = UNDEFINED;
10975
		//this.oldChartWidth = UNDEFINED;
10976
		//this.oldChartHeight = UNDEFINED;
10977
 
10978
		//this.renderTo = UNDEFINED;
10979
		//this.renderToClone = UNDEFINED;
10980
 
10981
		//this.spacingBox = UNDEFINED
10982
 
10983
		//this.legend = UNDEFINED;
10984
 
10985
		// Elements
10986
		//this.chartBackground = UNDEFINED;
10987
		//this.plotBackground = UNDEFINED;
10988
		//this.plotBGImage = UNDEFINED;
10989
		//this.plotBorder = UNDEFINED;
10990
		//this.loadingDiv = UNDEFINED;
10991
		//this.loadingSpan = UNDEFINED;
10992
 
10993
		var chart = this,
10994
			eventType;
10995
 
10996
		// Add the chart to the global lookup
10997
		chart.index = charts.length;
10998
		charts.push(chart);
10999
		chartCount++;
11000
 
11001
		// Set up auto resize
11002
		if (optionsChart.reflow !== false) {
11003
			addEvent(chart, 'load', function () {
11004
				chart.initReflow();
11005
			});
11006
		}
11007
 
11008
		// Chart event handlers
11009
		if (chartEvents) {
11010
			for (eventType in chartEvents) {
11011
				addEvent(chart, eventType, chartEvents[eventType]);
11012
			}
11013
		}
11014
 
11015
		chart.xAxis = [];
11016
		chart.yAxis = [];
11017
 
11018
		// Expose methods and variables
11019
		chart.animation = useCanVG ? false : pick(optionsChart.animation, true);
11020
		chart.pointCount = chart.colorCounter = chart.symbolCounter = 0;
11021
 
11022
		chart.firstRender();
11023
	},
11024
 
11025
	/**
11026
	 * Initialize an individual series, called internally before render time
11027
	 */
11028
	initSeries: function (options) {
11029
		var chart = this,
11030
			optionsChart = chart.options.chart,
11031
			type = options.type || optionsChart.type || optionsChart.defaultSeriesType,
11032
			series,
11033
			constr = seriesTypes[type];
11034
 
11035
		// No such series type
11036
		if (!constr) {
11037
			error(17, true);
11038
		}
11039
 
11040
		series = new constr();
11041
		series.init(this, options);
11042
		return series;
11043
	},
11044
 
11045
	/**
11046
	 * Check whether a given point is within the plot area
11047
	 *
11048
	 * @param {Number} plotX Pixel x relative to the plot area
11049
	 * @param {Number} plotY Pixel y relative to the plot area
11050
	 * @param {Boolean} inverted Whether the chart is inverted
11051
	 */
11052
	isInsidePlot: function (plotX, plotY, inverted) {
11053
		var x = inverted ? plotY : plotX,
11054
			y = inverted ? plotX : plotY;
11055
 
11056
		return x >= 0 &&
11057
			x <= this.plotWidth &&
11058
			y >= 0 &&
11059
			y <= this.plotHeight;
11060
	},
11061
 
11062
	/**
11063
	 * Redraw legend, axes or series based on updated data
11064
	 *
11065
	 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
11066
	 *    configuration
11067
	 */
11068
	redraw: function (animation) {
11069
		var chart = this,
11070
			axes = chart.axes,
11071
			series = chart.series,
11072
			pointer = chart.pointer,
11073
			legend = chart.legend,
11074
			redrawLegend = chart.isDirtyLegend,
11075
			hasStackedSeries,
11076
			hasDirtyStacks,
11077
			hasCartesianSeries = chart.hasCartesianSeries,
11078
			isDirtyBox = chart.isDirtyBox, // todo: check if it has actually changed?
11079
			seriesLength = series.length,
11080
			i = seriesLength,
11081
			serie,
11082
			renderer = chart.renderer,
11083
			isHiddenChart = renderer.isHidden(),
11084
			afterRedraw = [];
11085
 
11086
		setAnimation(animation, chart);
11087
 
11088
		if (isHiddenChart) {
11089
			chart.cloneRenderTo();
11090
		}
11091
 
11092
		// Adjust title layout (reflow multiline text)
11093
		chart.layOutTitles();
11094
 
11095
		// link stacked series
11096
		while (i--) {
11097
			serie = series[i];
11098
 
11099
			if (serie.options.stacking) {
11100
				hasStackedSeries = true;
11101
 
11102
				if (serie.isDirty) {
11103
					hasDirtyStacks = true;
11104
					break;
11105
				}
11106
			}
11107
		}
11108
		if (hasDirtyStacks) { // mark others as dirty
11109
			i = seriesLength;
11110
			while (i--) {
11111
				serie = series[i];
11112
				if (serie.options.stacking) {
11113
					serie.isDirty = true;
11114
				}
11115
			}
11116
		}
11117
 
11118
		// Handle updated data in the series
11119
		each(series, function (serie) {
11120
			if (serie.isDirty) {
11121
				if (serie.options.legendType === 'point') {
11122
					if (serie.updateTotals) {
11123
						serie.updateTotals();
11124
					}
11125
					redrawLegend = true;
11126
				}
11127
			}
11128
		});
11129
 
11130
		// handle added or removed series
11131
		if (redrawLegend && legend.options.enabled) { // series or pie points are added or removed
11132
			// draw legend graphics
11133
			legend.render();
11134
 
11135
			chart.isDirtyLegend = false;
11136
		}
11137
 
11138
		// reset stacks
11139
		if (hasStackedSeries) {
11140
			chart.getStacks();
11141
		}
11142
 
11143
 
11144
		if (hasCartesianSeries) {
11145
			if (!chart.isResizing) {
11146
 
11147
				// reset maxTicks
11148
				chart.maxTicks = null;
11149
 
11150
				// set axes scales
11151
				each(axes, function (axis) {
11152
					axis.setScale();
11153
				});
11154
			}
11155
		}
11156
 
11157
		chart.getMargins(); // #3098
11158
 
11159
		if (hasCartesianSeries) {
11160
			// If one axis is dirty, all axes must be redrawn (#792, #2169)
11161
			each(axes, function (axis) {
11162
				if (axis.isDirty) {
11163
					isDirtyBox = true;
11164
				}
11165
			});
11166
 
11167
			// redraw axes
11168
			each(axes, function (axis) {
11169
 
11170
				// Fire 'afterSetExtremes' only if extremes are set
11171
				var key = axis.min + ',' + axis.max;
11172
				if (axis.extKey !== key) { // #821, #4452
11173
					axis.extKey = key;
11174
					afterRedraw.push(function () { // prevent a recursive call to chart.redraw() (#1119)
11175
						fireEvent(axis, 'afterSetExtremes', extend(axis.eventArgs, axis.getExtremes())); // #747, #751
11176
						delete axis.eventArgs;
11177
					});
11178
				}
11179
				if (isDirtyBox || hasStackedSeries) {
11180
					axis.redraw();
11181
				}
11182
			});
11183
		}
11184
 
11185
		// the plot areas size has changed
11186
		if (isDirtyBox) {
11187
			chart.drawChartBox();
11188
		}
11189
 
11190
 
11191
		// redraw affected series
11192
		each(series, function (serie) {
11193
			if (serie.isDirty && serie.visible &&
11194
					(!serie.isCartesian || serie.xAxis)) { // issue #153
11195
				serie.redraw();
11196
			}
11197
		});
11198
 
11199
		// move tooltip or reset
11200
		if (pointer) {
11201
			pointer.reset(true);
11202
		}
11203
 
11204
		// redraw if canvas
11205
		renderer.draw();
11206
 
11207
		// fire the event
11208
		fireEvent(chart, 'redraw'); // jQuery breaks this when calling it from addEvent. Overwrites chart.redraw
11209
 
11210
		if (isHiddenChart) {
11211
			chart.cloneRenderTo(true);
11212
		}
11213
 
11214
		// Fire callbacks that are put on hold until after the redraw
11215
		each(afterRedraw, function (callback) {
11216
			callback.call();
11217
		});
11218
	},
11219
 
11220
	/**
11221
	 * Get an axis, series or point object by id.
11222
	 * @param id {String} The id as given in the configuration options
11223
	 */
11224
	get: function (id) {
11225
		var chart = this,
11226
			axes = chart.axes,
11227
			series = chart.series;
11228
 
11229
		var i,
11230
			j,
11231
			points;
11232
 
11233
		// search axes
11234
		for (i = 0; i < axes.length; i++) {
11235
			if (axes[i].options.id === id) {
11236
				return axes[i];
11237
			}
11238
		}
11239
 
11240
		// search series
11241
		for (i = 0; i < series.length; i++) {
11242
			if (series[i].options.id === id) {
11243
				return series[i];
11244
			}
11245
		}
11246
 
11247
		// search points
11248
		for (i = 0; i < series.length; i++) {
11249
			points = series[i].points || [];
11250
			for (j = 0; j < points.length; j++) {
11251
				if (points[j].id === id) {
11252
					return points[j];
11253
				}
11254
			}
11255
		}
11256
		return null;
11257
	},
11258
 
11259
	/**
11260
	 * Create the Axis instances based on the config options
11261
	 */
11262
	getAxes: function () {
11263
		var chart = this,
11264
			options = this.options,
11265
			xAxisOptions = options.xAxis = splat(options.xAxis || {}),
11266
			yAxisOptions = options.yAxis = splat(options.yAxis || {}),
11267
			optionsArray,
11268
			axis;
11269
 
11270
		// make sure the options are arrays and add some members
11271
		each(xAxisOptions, function (axis, i) {
11272
			axis.index = i;
11273
			axis.isX = true;
11274
		});
11275
 
11276
		each(yAxisOptions, function (axis, i) {
11277
			axis.index = i;
11278
		});
11279
 
11280
		// concatenate all axis options into one array
11281
		optionsArray = xAxisOptions.concat(yAxisOptions);
11282
 
11283
		each(optionsArray, function (axisOptions) {
11284
			axis = new Axis(chart, axisOptions);
11285
		});
11286
	},
11287
 
11288
 
11289
	/**
11290
	 * Get the currently selected points from all series
11291
	 */
11292
	getSelectedPoints: function () {
11293
		var points = [];
11294
		each(this.series, function (serie) {
11295
			points = points.concat(grep(serie.points || [], function (point) {
11296
				return point.selected;
11297
			}));
11298
		});
11299
		return points;
11300
	},
11301
 
11302
	/**
11303
	 * Get the currently selected series
11304
	 */
11305
	getSelectedSeries: function () {
11306
		return grep(this.series, function (serie) {
11307
			return serie.selected;
11308
		});
11309
	},
11310
 
11311
	/**
11312
	 * Show the title and subtitle of the chart
11313
	 *
11314
	 * @param titleOptions {Object} New title options
11315
	 * @param subtitleOptions {Object} New subtitle options
11316
	 *
11317
	 */
11318
	setTitle: function (titleOptions, subtitleOptions, redraw) {
11319
		var chart = this,
11320
			options = chart.options,
11321
			chartTitleOptions,
11322
			chartSubtitleOptions;
11323
 
11324
		chartTitleOptions = options.title = merge(options.title, titleOptions);
11325
		chartSubtitleOptions = options.subtitle = merge(options.subtitle, subtitleOptions);
11326
 
11327
		// add title and subtitle
11328
		each([
11329
			['title', titleOptions, chartTitleOptions],
11330
			['subtitle', subtitleOptions, chartSubtitleOptions]
11331
		], function (arr) {
11332
			var name = arr[0],
11333
				title = chart[name],
11334
				titleOptions = arr[1],
11335
				chartTitleOptions = arr[2];
11336
 
11337
			if (title && titleOptions) {
11338
				chart[name] = title = title.destroy(); // remove old
11339
			}
11340
 
11341
			if (chartTitleOptions && chartTitleOptions.text && !title) {
11342
				chart[name] = chart.renderer.text(
11343
					chartTitleOptions.text,
11344
					0,
11345
					0,
11346
					chartTitleOptions.useHTML
11347
				)
11348
				.attr({
11349
					align: chartTitleOptions.align,
11350
					'class': PREFIX + name,
11351
					zIndex: chartTitleOptions.zIndex || 4
11352
				})
11353
				.css(chartTitleOptions.style)
11354
				.add();
11355
			}
11356
		});
11357
		chart.layOutTitles(redraw);
11358
	},
11359
 
11360
	/**
11361
	 * Lay out the chart titles and cache the full offset height for use in getMargins
11362
	 */
11363
	layOutTitles: function (redraw) {
11364
		var titleOffset = 0,
11365
			title = this.title,
11366
			subtitle = this.subtitle,
11367
			options = this.options,
11368
			titleOptions = options.title,
11369
			subtitleOptions = options.subtitle,
11370
			requiresDirtyBox,
11371
			renderer = this.renderer,
11372
			autoWidth = this.spacingBox.width - 44; // 44 makes room for default context button
11373
 
11374
		if (title) {
11375
			title
11376
				.css({ width: (titleOptions.width || autoWidth) + PX })
11377
				.align(extend({
11378
					y: renderer.fontMetrics(titleOptions.style.fontSize, title).b - 3
11379
				}, titleOptions), false, 'spacingBox');
11380
 
11381
			if (!titleOptions.floating && !titleOptions.verticalAlign) {
11382
				titleOffset = title.getBBox().height;
11383
			}
11384
		}
11385
		if (subtitle) {
11386
			subtitle
11387
				.css({ width: (subtitleOptions.width || autoWidth) + PX })
11388
				.align(extend({
11389
					y: titleOffset + (titleOptions.margin - 13) + renderer.fontMetrics(subtitleOptions.style.fontSize, title).b
11390
				}, subtitleOptions), false, 'spacingBox');
11391
 
11392
			if (!subtitleOptions.floating && !subtitleOptions.verticalAlign) {
11393
				titleOffset = mathCeil(titleOffset + subtitle.getBBox().height);
11394
			}
11395
		}
11396
 
11397
		requiresDirtyBox = this.titleOffset !== titleOffset;
11398
		this.titleOffset = titleOffset; // used in getMargins
11399
 
11400
		if (!this.isDirtyBox && requiresDirtyBox) {
11401
			this.isDirtyBox = requiresDirtyBox;
11402
			// Redraw if necessary (#2719, #2744)
11403
			if (this.hasRendered && pick(redraw, true) && this.isDirtyBox) {
11404
				this.redraw();
11405
			}
11406
		}
11407
	},
11408
 
11409
	/**
11410
	 * Get chart width and height according to options and container size
11411
	 */
11412
	getChartSize: function () {
11413
		var chart = this,
11414
			optionsChart = chart.options.chart,
11415
			widthOption = optionsChart.width,
11416
			heightOption = optionsChart.height,
11417
			renderTo = chart.renderToClone || chart.renderTo;
11418
 
11419
		// get inner width and height from jQuery (#824)
11420
		if (!defined(widthOption)) {
11421
			chart.containerWidth = adapterRun(renderTo, 'width');
11422
		}
11423
		if (!defined(heightOption)) {
11424
			chart.containerHeight = adapterRun(renderTo, 'height');
11425
		}
11426
 
11427
		chart.chartWidth = mathMax(0, widthOption || chart.containerWidth || 600); // #1393, 1460
11428
		chart.chartHeight = mathMax(0, pick(heightOption,
11429
			// the offsetHeight of an empty container is 0 in standard browsers, but 19 in IE7:
11430
			chart.containerHeight > 19 ? chart.containerHeight : 400));
11431
	},
11432
 
11433
	/**
11434
	 * Create a clone of the chart's renderTo div and place it outside the viewport to allow
11435
	 * size computation on chart.render and chart.redraw
11436
	 */
11437
	cloneRenderTo: function (revert) {
11438
		var clone = this.renderToClone,
11439
			container = this.container;
11440
 
11441
		// Destroy the clone and bring the container back to the real renderTo div
11442
		if (revert) {
11443
			if (clone) {
11444
				this.renderTo.appendChild(container);
11445
				discardElement(clone);
11446
				delete this.renderToClone;
11447
			}
11448
 
11449
		// Set up the clone
11450
		} else {
11451
			if (container && container.parentNode === this.renderTo) {
11452
				this.renderTo.removeChild(container); // do not clone this
11453
			}
11454
			this.renderToClone = clone = this.renderTo.cloneNode(0);
11455
			css(clone, {
11456
				position: ABSOLUTE,
11457
				top: '-9999px',
11458
				display: 'block' // #833
11459
			});
11460
			if (clone.style.setProperty) { // #2631
11461
				clone.style.setProperty('display', 'block', 'important');
11462
			}
11463
			doc.body.appendChild(clone);
11464
			if (container) {
11465
				clone.appendChild(container);
11466
			}
11467
		}
11468
	},
11469
 
11470
	/**
11471
	 * Get the containing element, determine the size and create the inner container
11472
	 * div to hold the chart
11473
	 */
11474
	getContainer: function () {
11475
		var chart = this,
11476
			container,
11477
			options = chart.options,
11478
			optionsChart = options.chart,
11479
			chartWidth,
11480
			chartHeight,
11481
			renderTo,
11482
			indexAttrName = 'data-highcharts-chart',
11483
			oldChartIndex,
11484
			Ren,
11485
			containerId;
11486
 
11487
		chart.renderTo = renderTo = optionsChart.renderTo;
11488
		containerId = PREFIX + idCounter++;
11489
 
11490
		if (isString(renderTo)) {
11491
			chart.renderTo = renderTo = doc.getElementById(renderTo);
11492
		}
11493
 
11494
		// Display an error if the renderTo is wrong
11495
		if (!renderTo) {
11496
			error(13, true);
11497
		}
11498
 
11499
		// If the container already holds a chart, destroy it. The check for hasRendered is there
11500
		// because web pages that are saved to disk from the browser, will preserve the data-highcharts-chart
11501
		// attribute and the SVG contents, but not an interactive chart. So in this case,
11502
		// charts[oldChartIndex] will point to the wrong chart if any (#2609).
11503
		oldChartIndex = pInt(attr(renderTo, indexAttrName));
11504
		if (!isNaN(oldChartIndex) && charts[oldChartIndex] && charts[oldChartIndex].hasRendered) {
11505
			charts[oldChartIndex].destroy();
11506
		}
11507
 
11508
		// Make a reference to the chart from the div
11509
		attr(renderTo, indexAttrName, chart.index);
11510
 
11511
		// remove previous chart
11512
		renderTo.innerHTML = '';
11513
 
11514
		// If the container doesn't have an offsetWidth, it has or is a child of a node
11515
		// that has display:none. We need to temporarily move it out to a visible
11516
		// state to determine the size, else the legend and tooltips won't render
11517
		// properly. The allowClone option is used in sparklines as a micro optimization,
11518
		// saving about 1-2 ms each chart.
11519
		if (!optionsChart.skipClone && !renderTo.offsetWidth) {
11520
			chart.cloneRenderTo();
11521
		}
11522
 
11523
		// get the width and height
11524
		chart.getChartSize();
11525
		chartWidth = chart.chartWidth;
11526
		chartHeight = chart.chartHeight;
11527
 
11528
		// create the inner container
11529
		chart.container = container = createElement(DIV, {
11530
				className: PREFIX + 'container' +
11531
					(optionsChart.className ? ' ' + optionsChart.className : ''),
11532
				id: containerId
11533
			}, extend({
11534
				position: RELATIVE,
11535
				overflow: HIDDEN, // needed for context menu (avoid scrollbars) and
11536
					// content overflow in IE
11537
				width: chartWidth + PX,
11538
				height: chartHeight + PX,
11539
				textAlign: 'left',
11540
				lineHeight: 'normal', // #427
11541
				zIndex: 0, // #1072
11542
				'-webkit-tap-highlight-color': 'rgba(0,0,0,0)'
11543
			}, optionsChart.style),
11544
			chart.renderToClone || renderTo
11545
		);
11546
 
11547
		// cache the cursor (#1650)
11548
		chart._cursor = container.style.cursor;
11549
 
11550
		// Initialize the renderer
11551
		Ren = Highcharts[optionsChart.renderer] || Renderer;
11552
		chart.renderer = new Ren(
11553
			container,
11554
			chartWidth,
11555
			chartHeight,
11556
			optionsChart.style,
11557
			optionsChart.forExport,
11558
			options.exporting && options.exporting.allowHTML
11559
		);
11560
 
11561
		if (useCanVG) {
11562
			// If we need canvg library, extend and configure the renderer
11563
			// to get the tracker for translating mouse events
11564
			chart.renderer.create(chart, container, chartWidth, chartHeight);
11565
		}
11566
		// Add a reference to the charts index
11567
		chart.renderer.chartIndex = chart.index;
11568
	},
11569
 
11570
	/**
11571
	 * Calculate margins by rendering axis labels in a preliminary position. Title,
11572
	 * subtitle and legend have already been rendered at this stage, but will be
11573
	 * moved into their final positions
11574
	 */
11575
	getMargins: function (skipAxes) {
11576
		var chart = this,
11577
			spacing = chart.spacing,
11578
			margin = chart.margin,
11579
			titleOffset = chart.titleOffset;
11580
 
11581
		chart.resetMargins();
11582
 
11583
		// Adjust for title and subtitle
11584
		if (titleOffset && !defined(margin[0])) {
11585
			chart.plotTop = mathMax(chart.plotTop, titleOffset + chart.options.title.margin + spacing[0]);
11586
		}
11587
 
11588
		// Adjust for legend
11589
		chart.legend.adjustMargins(margin, spacing);
11590
 
11591
		// adjust for scroller
11592
		if (chart.extraBottomMargin) {
11593
			chart.marginBottom += chart.extraBottomMargin;
11594
		}
11595
		if (chart.extraTopMargin) {
11596
			chart.plotTop += chart.extraTopMargin;
11597
		}
11598
		if (!skipAxes) {
11599
			this.getAxisMargins();
11600
		}
11601
	},
11602
 
11603
	getAxisMargins: function () {
11604
 
11605
		var chart = this,
11606
			axisOffset = chart.axisOffset = [0, 0, 0, 0], // top, right, bottom, left
11607
			margin = chart.margin;
11608
 
11609
		// pre-render axes to get labels offset width
11610
		if (chart.hasCartesianSeries) {
11611
			each(chart.axes, function (axis) {
11612
				if (axis.visible) {
11613
					axis.getOffset();
11614
				}
11615
			});
11616
		}
11617
 
11618
		// Add the axis offsets
11619
		each(marginNames, function (m, side) {
11620
			if (!defined(margin[side])) {
11621
				chart[m] += axisOffset[side];
11622
			}
11623
		});
11624
 
11625
		chart.setChartSize();
11626
 
11627
	},
11628
 
11629
	/**
11630
	 * Resize the chart to its container if size is not explicitly set
11631
	 */
11632
	reflow: function (e) {
11633
		var chart = this,
11634
			optionsChart = chart.options.chart,
11635
			renderTo = chart.renderTo,
11636
			width = optionsChart.width || adapterRun(renderTo, 'width'),
11637
			height = optionsChart.height || adapterRun(renderTo, 'height'),
11638
			target = e ? e.target : win, // #805 - MooTools doesn't supply e
11639
			doReflow = function () {
11640
				if (chart.container) { // It may have been destroyed in the meantime (#1257)
11641
					chart.setSize(width, height, false);
11642
					chart.hasUserSize = null;
11643
				}
11644
			};
11645
 
11646
		// Width and height checks for display:none. Target is doc in IE8 and Opera,
11647
		// win in Firefox, Chrome and IE9.
11648
		if (!chart.hasUserSize && !chart.isPrinting && width && height && (target === win || target === doc)) { // #1093
11649
			if (width !== chart.containerWidth || height !== chart.containerHeight) {
11650
				clearTimeout(chart.reflowTimeout);
11651
				if (e) { // Called from window.resize
11652
					chart.reflowTimeout = setTimeout(doReflow, 100);
11653
				} else { // Called directly (#2224)
11654
					doReflow();
11655
				}
11656
			}
11657
			chart.containerWidth = width;
11658
			chart.containerHeight = height;
11659
		}
11660
	},
11661
 
11662
	/**
11663
	 * Add the event handlers necessary for auto resizing
11664
	 */
11665
	initReflow: function () {
11666
		var chart = this,
11667
			reflow = function (e) {
11668
				chart.reflow(e);
11669
			};
11670
 
11671
 
11672
		addEvent(win, 'resize', reflow);
11673
		addEvent(chart, 'destroy', function () {
11674
			removeEvent(win, 'resize', reflow);
11675
		});
11676
	},
11677
 
11678
	/**
11679
	 * Resize the chart to a given width and height
11680
	 * @param {Number} width
11681
	 * @param {Number} height
11682
	 * @param {Object|Boolean} animation
11683
	 */
11684
	setSize: function (width, height, animation) {
11685
		var chart = this,
11686
			chartWidth,
11687
			chartHeight,
11688
			fireEndResize,
11689
			renderer = chart.renderer,
11690
			globalAnimation;
11691
 
11692
		// Handle the isResizing counter
11693
		chart.isResizing += 1;
11694
		fireEndResize = function () {
11695
			if (chart) {
11696
				fireEvent(chart, 'endResize', null, function () {
11697
					chart.isResizing -= 1;
11698
				});
11699
			}
11700
		};
11701
 
11702
		// set the animation for the current process
11703
		setAnimation(animation, chart);
11704
 
11705
		chart.oldChartHeight = chart.chartHeight;
11706
		chart.oldChartWidth = chart.chartWidth;
11707
		if (defined(width)) {
11708
			chart.chartWidth = chartWidth = mathMax(0, mathRound(width));
11709
			chart.hasUserSize = !!chartWidth;
11710
		}
11711
		if (defined(height)) {
11712
			chart.chartHeight = chartHeight = mathMax(0, mathRound(height));
11713
		}
11714
 
11715
		// Resize the container with the global animation applied if enabled (#2503)
11716
		globalAnimation = renderer.globalAnimation;
11717
		(globalAnimation ? animate : css)(chart.container, {
11718
			width: chartWidth + PX,
11719
			height: chartHeight + PX
11720
		}, globalAnimation);
11721
 
11722
		chart.setChartSize(true);
11723
		renderer.setSize(chartWidth, chartHeight, animation);
11724
 
11725
		// handle axes
11726
		chart.maxTicks = null;
11727
		each(chart.axes, function (axis) {
11728
			axis.isDirty = true;
11729
			axis.setScale();
11730
		});
11731
 
11732
		// make sure non-cartesian series are also handled
11733
		each(chart.series, function (serie) {
11734
			serie.isDirty = true;
11735
		});
11736
 
11737
		chart.isDirtyLegend = true; // force legend redraw
11738
		chart.isDirtyBox = true; // force redraw of plot and chart border
11739
 
11740
		chart.layOutTitles(); // #2857
11741
		chart.getMargins();
11742
 
11743
		chart.redraw(animation);
11744
 
11745
 
11746
		chart.oldChartHeight = null;
11747
		fireEvent(chart, 'resize');
11748
 
11749
		// Fire endResize and set isResizing back. If animation is disabled, fire without delay
11750
		globalAnimation = renderer.globalAnimation; // Reassign it before using it, it may have changed since the top of this function.
11751
		if (globalAnimation === false) {
11752
			fireEndResize();
11753
		} else { // else set a timeout with the animation duration
11754
			setTimeout(fireEndResize, (globalAnimation && globalAnimation.duration) || 500);
11755
		}
11756
	},
11757
 
11758
	/**
11759
	 * Set the public chart properties. This is done before and after the pre-render
11760
	 * to determine margin sizes
11761
	 */
11762
	setChartSize: function (skipAxes) {
11763
		var chart = this,
11764
			inverted = chart.inverted,
11765
			renderer = chart.renderer,
11766
			chartWidth = chart.chartWidth,
11767
			chartHeight = chart.chartHeight,
11768
			optionsChart = chart.options.chart,
11769
			spacing = chart.spacing,
11770
			clipOffset = chart.clipOffset,
11771
			clipX,
11772
			clipY,
11773
			plotLeft,
11774
			plotTop,
11775
			plotWidth,
11776
			plotHeight,
11777
			plotBorderWidth;
11778
 
11779
		chart.plotLeft = plotLeft = mathRound(chart.plotLeft);
11780
		chart.plotTop = plotTop = mathRound(chart.plotTop);
11781
		chart.plotWidth = plotWidth = mathMax(0, mathRound(chartWidth - plotLeft - chart.marginRight));
11782
		chart.plotHeight = plotHeight = mathMax(0, mathRound(chartHeight - plotTop - chart.marginBottom));
11783
 
11784
		chart.plotSizeX = inverted ? plotHeight : plotWidth;
11785
		chart.plotSizeY = inverted ? plotWidth : plotHeight;
11786
 
11787
		chart.plotBorderWidth = optionsChart.plotBorderWidth || 0;
11788
 
11789
		// Set boxes used for alignment
11790
		chart.spacingBox = renderer.spacingBox = {
11791
			x: spacing[3],
11792
			y: spacing[0],
11793
			width: chartWidth - spacing[3] - spacing[1],
11794
			height: chartHeight - spacing[0] - spacing[2]
11795
		};
11796
		chart.plotBox = renderer.plotBox = {
11797
			x: plotLeft,
11798
			y: plotTop,
11799
			width: plotWidth,
11800
			height: plotHeight
11801
		};
11802
 
11803
		plotBorderWidth = 2 * mathFloor(chart.plotBorderWidth / 2);
11804
		clipX = mathCeil(mathMax(plotBorderWidth, clipOffset[3]) / 2);
11805
		clipY = mathCeil(mathMax(plotBorderWidth, clipOffset[0]) / 2);
11806
		chart.clipBox = {
11807
			x: clipX,
11808
			y: clipY,
11809
			width: mathFloor(chart.plotSizeX - mathMax(plotBorderWidth, clipOffset[1]) / 2 - clipX),
11810
			height: mathMax(0, mathFloor(chart.plotSizeY - mathMax(plotBorderWidth, clipOffset[2]) / 2 - clipY))
11811
		};
11812
 
11813
		if (!skipAxes) {
11814
			each(chart.axes, function (axis) {
11815
				axis.setAxisSize();
11816
				axis.setAxisTranslation();
11817
			});
11818
		}
11819
	},
11820
 
11821
	/**
11822
	 * Initial margins before auto size margins are applied
11823
	 */
11824
	resetMargins: function () {
11825
		var chart = this;
11826
 
11827
		each(marginNames, function (m, side) {
11828
			chart[m] = pick(chart.margin[side], chart.spacing[side]);
11829
		});
11830
		chart.axisOffset = [0, 0, 0, 0]; // top, right, bottom, left
11831
		chart.clipOffset = [0, 0, 0, 0];
11832
	},
11833
 
11834
	/**
11835
	 * Draw the borders and backgrounds for chart and plot area
11836
	 */
11837
	drawChartBox: function () {
11838
		var chart = this,
11839
			optionsChart = chart.options.chart,
11840
			renderer = chart.renderer,
11841
			chartWidth = chart.chartWidth,
11842
			chartHeight = chart.chartHeight,
11843
			chartBackground = chart.chartBackground,
11844
			plotBackground = chart.plotBackground,
11845
			plotBorder = chart.plotBorder,
11846
			plotBGImage = chart.plotBGImage,
11847
			chartBorderWidth = optionsChart.borderWidth || 0,
11848
			chartBackgroundColor = optionsChart.backgroundColor,
11849
			plotBackgroundColor = optionsChart.plotBackgroundColor,
11850
			plotBackgroundImage = optionsChart.plotBackgroundImage,
11851
			plotBorderWidth = optionsChart.plotBorderWidth || 0,
11852
			mgn,
11853
			bgAttr,
11854
			plotLeft = chart.plotLeft,
11855
			plotTop = chart.plotTop,
11856
			plotWidth = chart.plotWidth,
11857
			plotHeight = chart.plotHeight,
11858
			plotBox = chart.plotBox,
11859
			clipRect = chart.clipRect,
11860
			clipBox = chart.clipBox;
11861
 
11862
		// Chart area
11863
		mgn = chartBorderWidth + (optionsChart.shadow ? 8 : 0);
11864
 
11865
		if (chartBorderWidth || chartBackgroundColor) {
11866
			if (!chartBackground) {
11867
 
11868
				bgAttr = {
11869
					fill: chartBackgroundColor || NONE
11870
				};
11871
				if (chartBorderWidth) { // #980
11872
					bgAttr.stroke = optionsChart.borderColor;
11873
					bgAttr['stroke-width'] = chartBorderWidth;
11874
				}
11875
				chart.chartBackground = renderer.rect(mgn / 2, mgn / 2, chartWidth - mgn, chartHeight - mgn,
11876
						optionsChart.borderRadius, chartBorderWidth)
11877
					.attr(bgAttr)
11878
					.addClass(PREFIX + 'background')
11879
					.add()
11880
					.shadow(optionsChart.shadow);
11881
 
11882
			} else { // resize
11883
				chartBackground.animate(
11884
					chartBackground.crisp({ width: chartWidth - mgn, height: chartHeight - mgn })
11885
				);
11886
			}
11887
		}
11888
 
11889
 
11890
		// Plot background
11891
		if (plotBackgroundColor) {
11892
			if (!plotBackground) {
11893
				chart.plotBackground = renderer.rect(plotLeft, plotTop, plotWidth, plotHeight, 0)
11894
					.attr({
11895
						fill: plotBackgroundColor
11896
					})
11897
					.add()
11898
					.shadow(optionsChart.plotShadow);
11899
			} else {
11900
				plotBackground.animate(plotBox);
11901
			}
11902
		}
11903
		if (plotBackgroundImage) {
11904
			if (!plotBGImage) {
11905
				chart.plotBGImage = renderer.image(plotBackgroundImage, plotLeft, plotTop, plotWidth, plotHeight)
11906
					.add();
11907
			} else {
11908
				plotBGImage.animate(plotBox);
11909
			}
11910
		}
11911
 
11912
		// Plot clip
11913
		if (!clipRect) {
11914
			chart.clipRect = renderer.clipRect(clipBox);
11915
		} else {
11916
			clipRect.animate({
11917
				width: clipBox.width,
11918
				height: clipBox.height
11919
			});
11920
		}
11921
 
11922
		// Plot area border
11923
		if (plotBorderWidth) {
11924
			if (!plotBorder) {
11925
				chart.plotBorder = renderer.rect(plotLeft, plotTop, plotWidth, plotHeight, 0, -plotBorderWidth)
11926
					.attr({
11927
						stroke: optionsChart.plotBorderColor,
11928
						'stroke-width': plotBorderWidth,
11929
						fill: NONE,
11930
						zIndex: 1
11931
					})
11932
					.add();
11933
			} else {
11934
				plotBorder.animate(
11935
					plotBorder.crisp({ x: plotLeft, y: plotTop, width: plotWidth, height: plotHeight, strokeWidth: -plotBorderWidth }) //#3282 plotBorder should be negative
11936
				);
11937
			}
11938
		}
11939
 
11940
		// reset
11941
		chart.isDirtyBox = false;
11942
	},
11943
 
11944
	/**
11945
	 * Detect whether a certain chart property is needed based on inspecting its options
11946
	 * and series. This mainly applies to the chart.invert property, and in extensions to
11947
	 * the chart.angular and chart.polar properties.
11948
	 */
11949
	propFromSeries: function () {
11950
		var chart = this,
11951
			optionsChart = chart.options.chart,
11952
			klass,
11953
			seriesOptions = chart.options.series,
11954
			i,
11955
			value;
11956
 
11957
 
11958
		each(['inverted', 'angular', 'polar'], function (key) {
11959
 
11960
			// The default series type's class
11961
			klass = seriesTypes[optionsChart.type || optionsChart.defaultSeriesType];
11962
 
11963
			// Get the value from available chart-wide properties
11964
			value = (
11965
				chart[key] || // 1. it is set before
11966
				optionsChart[key] || // 2. it is set in the options
11967
				(klass && klass.prototype[key]) // 3. it's default series class requires it
11968
			);
11969
 
11970
			// 4. Check if any the chart's series require it
11971
			i = seriesOptions && seriesOptions.length;
11972
			while (!value && i--) {
11973
				klass = seriesTypes[seriesOptions[i].type];
11974
				if (klass && klass.prototype[key]) {
11975
					value = true;
11976
				}
11977
			}
11978
 
11979
			// Set the chart property
11980
			chart[key] = value;
11981
		});
11982
 
11983
	},
11984
 
11985
	/**
11986
	 * Link two or more series together. This is done initially from Chart.render,
11987
	 * and after Chart.addSeries and Series.remove.
11988
	 */
11989
	linkSeries: function () {
11990
		var chart = this,
11991
			chartSeries = chart.series;
11992
 
11993
		// Reset links
11994
		each(chartSeries, function (series) {
11995
			series.linkedSeries.length = 0;
11996
		});
11997
 
11998
		// Apply new links
11999
		each(chartSeries, function (series) {
12000
			var linkedTo = series.options.linkedTo;
12001
			if (isString(linkedTo)) {
12002
				if (linkedTo === ':previous') {
12003
					linkedTo = chart.series[series.index - 1];
12004
				} else {
12005
					linkedTo = chart.get(linkedTo);
12006
				}
12007
				if (linkedTo) {
12008
					linkedTo.linkedSeries.push(series);
12009
					series.linkedParent = linkedTo;
12010
					series.visible = pick(series.options.visible, linkedTo.options.visible, series.visible); // #3879
12011
				}
12012
			}
12013
		});
12014
	},
12015
 
12016
	/**
12017
	 * Render series for the chart
12018
	 */
12019
	renderSeries: function () {
12020
		each(this.series, function (serie) {
12021
			serie.translate();
12022
			serie.render();
12023
		});
12024
	},
12025
 
12026
	/**
12027
	 * Render labels for the chart
12028
	 */
12029
	renderLabels: function () {
12030
		var chart = this,
12031
			labels = chart.options.labels;
12032
		if (labels.items) {
12033
			each(labels.items, function (label) {
12034
				var style = extend(labels.style, label.style),
12035
					x = pInt(style.left) + chart.plotLeft,
12036
					y = pInt(style.top) + chart.plotTop + 12;
12037
 
12038
				// delete to prevent rewriting in IE
12039
				delete style.left;
12040
				delete style.top;
12041
 
12042
				chart.renderer.text(
12043
					label.html,
12044
					x,
12045
					y
12046
				)
12047
				.attr({ zIndex: 2 })
12048
				.css(style)
12049
				.add();
12050
 
12051
			});
12052
		}
12053
	},
12054
 
12055
	/**
12056
	 * Render all graphics for the chart
12057
	 */
12058
	render: function () {
12059
		var chart = this,
12060
			axes = chart.axes,
12061
			renderer = chart.renderer,
12062
			options = chart.options,
12063
			tempWidth,
12064
			tempHeight,
12065
			redoHorizontal,
12066
			redoVertical;
12067
 
12068
		// Title
12069
		chart.setTitle();
12070
 
12071
 
12072
		// Legend
12073
		chart.legend = new Legend(chart, options.legend);
12074
 
12075
		// Get stacks
12076
		if (chart.getStacks) {
12077
			chart.getStacks();
12078
		}
12079
 
12080
		// Get chart margins
12081
		chart.getMargins(true);
12082
		chart.setChartSize();
12083
 
12084
		// Record preliminary dimensions for later comparison
12085
		tempWidth = chart.plotWidth;
12086
		tempHeight = chart.plotHeight = chart.plotHeight - 13; // 13 is the most common height of X axis labels
12087
 
12088
		// Get margins by pre-rendering axes
12089
		each(axes, function (axis) {
12090
			axis.setScale();
12091
		});
12092
		chart.getAxisMargins();
12093
 
12094
		// If the plot area size has changed significantly, calculate tick positions again
12095
		redoHorizontal = tempWidth / chart.plotWidth > 1.1;
12096
		redoVertical = tempHeight / chart.plotHeight > 1.1;
12097
 
12098
		if (redoHorizontal || redoVertical) {
12099
 
12100
			chart.maxTicks = null; // reset for second pass
12101
			each(axes, function (axis) {
12102
				if ((axis.horiz && redoHorizontal) || (!axis.horiz && redoVertical)) {
12103
					axis.setTickInterval(true); // update to reflect the new margins
12104
				}
12105
			});
12106
			chart.getMargins(); // second pass to check for new labels
12107
		}
12108
 
12109
		// Draw the borders and backgrounds
12110
		chart.drawChartBox();
12111
 
12112
 
12113
		// Axes
12114
		if (chart.hasCartesianSeries) {
12115
			each(axes, function (axis) {
12116
				if (axis.visible) {
12117
					axis.render();
12118
				}
12119
			});
12120
		}
12121
 
12122
		// The series
12123
		if (!chart.seriesGroup) {
12124
			chart.seriesGroup = renderer.g('series-group')
12125
				.attr({ zIndex: 3 })
12126
				.add();
12127
		}
12128
		chart.renderSeries();
12129
 
12130
		// Labels
12131
		chart.renderLabels();
12132
 
12133
		// Credits
12134
		chart.showCredits(options.credits);
12135
 
12136
		// Set flag
12137
		chart.hasRendered = true;
12138
 
12139
	},
12140
 
12141
	/**
12142
	 * Show chart credits based on config options
12143
	 */
12144
	showCredits: function (credits) {
12145
		if (credits.enabled && !this.credits) {
12146
			this.credits = this.renderer.text(
12147
				credits.text,
12148
				0,
12149
 
12150
			)
12151
			.on('click', function () {
12152
				if (credits.href) {
12153
					location.href = credits.href;
12154
				}
12155
			})
12156
			.attr({
12157
				align: credits.position.align,
12158
				zIndex: 8
12159
			})
12160
			.css(credits.style)
12161
			.add()
12162
			.align(credits.position);
12163
		}
12164
	},
12165
 
12166
	/**
12167
	 * Clean up memory usage
12168
	 */
12169
	destroy: function () {
12170
		var chart = this,
12171
			axes = chart.axes,
12172
			series = chart.series,
12173
			container = chart.container,
12174
			i,
12175
			parentNode = container && container.parentNode;
12176
 
12177
		// fire the chart.destoy event
12178
		fireEvent(chart, 'destroy');
12179
 
12180
		// Delete the chart from charts lookup array
12181
		charts[chart.index] = UNDEFINED;
12182
		chartCount--;
12183
		chart.renderTo.removeAttribute('data-highcharts-chart');
12184
 
12185
		// remove events
12186
		removeEvent(chart);
12187
 
12188
		// ==== Destroy collections:
12189
		// Destroy axes
12190
		i = axes.length;
12191
		while (i--) {
12192
			axes[i] = axes[i].destroy();
12193
		}
12194
 
12195
		// Destroy each series
12196
		i = series.length;
12197
		while (i--) {
12198
			series[i] = series[i].destroy();
12199
		}
12200
 
12201
		// ==== Destroy chart properties:
12202
		each(['title', 'subtitle', 'chartBackground', 'plotBackground', 'plotBGImage',
12203
				'plotBorder', 'seriesGroup', 'clipRect', 'credits', 'pointer', 'scroller',
12204
				'rangeSelector', 'legend', 'resetZoomButton', 'tooltip', 'renderer'], function (name) {
12205
			var prop = chart[name];
12206
 
12207
			if (prop && prop.destroy) {
12208
				chart[name] = prop.destroy();
12209
			}
12210
		});
12211
 
12212
		// remove container and all SVG
12213
		if (container) { // can break in IE when destroyed before finished loading
12214
			container.innerHTML = '';
12215
			removeEvent(container);
12216
			if (parentNode) {
12217
				discardElement(container);
12218
			}
12219
 
12220
		}
12221
 
12222
		// clean it all up
12223
		for (i in chart) {
12224
			delete chart[i];
12225
		}
12226
 
12227
	},
12228
 
12229
 
12230
	/**
12231
	 * VML namespaces can't be added until after complete. Listening
12232
	 * for Perini's doScroll hack is not enough.
12233
	 */
12234
	isReadyToRender: function () {
12235
		var chart = this;
12236
 
12237
		// Note: in spite of JSLint's complaints, win == win.top is required
12238
		/*jslint eqeq: true*/
12239
		if ((!hasSVG && (win == win.top && doc.readyState !== 'complete')) || (useCanVG && !win.canvg)) {
12240
		/*jslint eqeq: false*/
12241
			if (useCanVG) {
12242
				// Delay rendering until canvg library is downloaded and ready
12243
				CanVGController.push(function () { chart.firstRender(); }, chart.options.global.canvasToolsURL);
12244
			} else {
12245
				doc.attachEvent('onreadystatechange', function () {
12246
					doc.detachEvent('onreadystatechange', chart.firstRender);
12247
					if (doc.readyState === 'complete') {
12248
						chart.firstRender();
12249
					}
12250
				});
12251
			}
12252
			return false;
12253
		}
12254
		return true;
12255
	},
12256
 
12257
	/**
12258
	 * Prepare for first rendering after all data are loaded
12259
	 */
12260
	firstRender: function () {
12261
		var chart = this,
12262
			options = chart.options,
12263
			callback = chart.callback;
12264
 
12265
		// Check whether the chart is ready to render
12266
		if (!chart.isReadyToRender()) {
12267
			return;
12268
		}
12269
 
12270
		// Create the container
12271
		chart.getContainer();
12272
 
12273
		// Run an early event after the container and renderer are established
12274
		fireEvent(chart, 'init');
12275
 
12276
 
12277
		chart.resetMargins();
12278
		chart.setChartSize();
12279
 
12280
		// Set the common chart properties (mainly invert) from the given series
12281
		chart.propFromSeries();
12282
 
12283
		// get axes
12284
		chart.getAxes();
12285
 
12286
		// Initialize the series
12287
		each(options.series || [], function (serieOptions) {
12288
			chart.initSeries(serieOptions);
12289
		});
12290
 
12291
		chart.linkSeries();
12292
 
12293
		// Run an event after axes and series are initialized, but before render. At this stage,
12294
		// the series data is indexed and cached in the xData and yData arrays, so we can access
12295
		// those before rendering. Used in Highstock.
12296
		fireEvent(chart, 'beforeRender');
12297
 
12298
		// depends on inverted and on margins being set
12299
		if (Highcharts.Pointer) {
12300
			chart.pointer = new Pointer(chart, options);
12301
		}
12302
 
12303
		chart.render();
12304
 
12305
		// add canvas
12306
		chart.renderer.draw();
12307
		// run callbacks
12308
		if (callback) {
12309
			callback.apply(chart, [chart]);
12310
		}
12311
		each(chart.callbacks, function (fn) {
12312
			if (chart.index !== UNDEFINED) { // Chart destroyed in its own callback (#3600)
12313
				fn.apply(chart, [chart]);
12314
			}
12315
		});
12316
 
12317
		// Fire the load event
12318
		fireEvent(chart, 'load');
12319
 
12320
		// If the chart was rendered outside the top container, put it back in (#3679)
12321
		chart.cloneRenderTo(true);
12322
 
12323
	},
12324
 
12325
	/**
12326
	* Creates arrays for spacing and margin from given options.
12327
	*/
12328
	splashArray: function (target, options) {
12329
		var oVar = options[target],
12330
			tArray = isObject(oVar) ? oVar : [oVar, oVar, oVar, oVar];
12331
 
12332
		return [pick(options[target + 'Top'], tArray[0]),
12333
				pick(options[target + 'Right'], tArray[1]),
12334
				pick(options[target + 'Bottom'], tArray[2]),
12335
				pick(options[target + 'Left'], tArray[3])];
12336
	}
12337
}; // end Chart
12338
/**
12339
 * The Point object and prototype. Inheritable and used as base for PiePoint
12340
 */
12341
var Point = function () {};
12342
Point.prototype = {
12343
 
12344
	/**
12345
	 * Initialize the point
12346
	 * @param {Object} series The series object containing this point
12347
	 * @param {Object} options The data in either number, array or object format
12348
	 */
12349
	init: function (series, options, x) {
12350
 
12351
		var point = this,
12352
			colors;
12353
		point.series = series;
12354
		point.color = series.color; // #3445
12355
		point.applyOptions(options, x);
12356
		point.pointAttr = {};
12357
 
12358
		if (series.options.colorByPoint) {
12359
			colors = series.options.colors || series.chart.options.colors;
12360
			point.color = point.color || colors[series.colorCounter++];
12361
			// loop back to zero
12362
			if (series.colorCounter === colors.length) {
12363
				series.colorCounter = 0;
12364
			}
12365
		}
12366
 
12367
		series.chart.pointCount++;
12368
		return point;
12369
	},
12370
	/**
12371
	 * Apply the options containing the x and y data and possible some extra properties.
12372
	 * This is called on point init or from point.update.
12373
	 *
12374
	 * @param {Object} options
12375
	 */
12376
	applyOptions: function (options, x) {
12377
		var point = this,
12378
			series = point.series,
12379
			pointValKey = series.options.pointValKey || series.pointValKey;
12380
 
12381
		options = Point.prototype.optionsToObject.call(this, options);
12382
 
12383
		// copy options directly to point
12384
		extend(point, options);
12385
		point.options = point.options ? extend(point.options, options) : options;
12386
 
12387
		// For higher dimension series types. For instance, for ranges, point.y is mapped to point.low.
12388
		if (pointValKey) {
12389
			point.y = point[pointValKey];
12390
		}
12391
 
12392
		// If no x is set by now, get auto incremented value. All points must have an
12393
		// x value, however the y value can be null to create a gap in the series
12394
		if (point.x === UNDEFINED && series) {
12395
			point.x = x === UNDEFINED ? series.autoIncrement() : x;
12396
		}
12397
 
12398
		return point;
12399
	},
12400
 
12401
	/**
12402
	 * Transform number or array configs into objects
12403
	 */
12404
	optionsToObject: function (options) {
12405
		var ret = {},
12406
			series = this.series,
12407
			keys = series.options.keys,
12408
			pointArrayMap = keys || series.pointArrayMap || ['y'],
12409
			valueCount = pointArrayMap.length,
12410
			firstItemType,
12411
			i = 0,
12412
			j = 0;
12413
 
12414
		if (typeof options === 'number' || options === null) {
12415
			ret[pointArrayMap[0]] = options;
12416
 
12417
		} else if (isArray(options)) {
12418
			// with leading x value
12419
			if (!keys && options.length > valueCount) {
12420
				firstItemType = typeof options[0];
12421
				if (firstItemType === 'string') {
12422
					ret.name = options[0];
12423
				} else if (firstItemType === 'number') {
12424
					ret.x = options[0];
12425
				}
12426
				i++;
12427
			}
12428
			while (j < valueCount) {
12429
				if (!keys || options[i] !== undefined) { // Skip undefined positions for keys
12430
					ret[pointArrayMap[j]] = options[i];
12431
				}
12432
				i++;
12433
				j++;
12434
			}
12435
		} else if (typeof options === 'object') {
12436
			ret = options;
12437
 
12438
			// This is the fastest way to detect if there are individual point dataLabels that need
12439
			// to be considered in drawDataLabels. These can only occur in object configs.
12440
			if (options.dataLabels) {
12441
				series._hasPointLabels = true;
12442
			}
12443
 
12444
			// Same approach as above for markers
12445
			if (options.marker) {
12446
				series._hasPointMarkers = true;
12447
			}
12448
		}
12449
		return ret;
12450
	},
12451
 
12452
	/**
12453
	 * Destroy a point to clear memory. Its reference still stays in series.data.
12454
	 */
12455
	destroy: function () {
12456
		var point = this,
12457
			series = point.series,
12458
			chart = series.chart,
12459
			hoverPoints = chart.hoverPoints,
12460
			prop;
12461
 
12462
		chart.pointCount--;
12463
 
12464
		if (hoverPoints) {
12465
			point.setState();
12466
			erase(hoverPoints, point);
12467
			if (!hoverPoints.length) {
12468
				chart.hoverPoints = null;
12469
			}
12470
 
12471
		}
12472
		if (point === chart.hoverPoint) {
12473
			point.onMouseOut();
12474
		}
12475
 
12476
		// remove all events
12477
		if (point.graphic || point.dataLabel) { // removeEvent and destroyElements are performance expensive
12478
			removeEvent(point);
12479
			point.destroyElements();
12480
		}
12481
 
12482
		if (point.legendItem) { // pies have legend items
12483
			chart.legend.destroyItem(point);
12484
		}
12485
 
12486
		for (prop in point) {
12487
			point[prop] = null;
12488
		}
12489
 
12490
 
12491
	},
12492
 
12493
	/**
12494
	 * Destroy SVG elements associated with the point
12495
	 */
12496
	destroyElements: function () {
12497
		var point = this,
12498
			props = ['graphic', 'dataLabel', 'dataLabelUpper', 'connector', 'shadowGroup'],
12499
			prop,
12500
			i = 6;
12501
		while (i--) {
12502
			prop = props[i];
12503
			if (point[prop]) {
12504
				point[prop] = point[prop].destroy();
12505
			}
12506
		}
12507
	},
12508
 
12509
	/**
12510
	 * Return the configuration hash needed for the data label and tooltip formatters
12511
	 */
12512
	getLabelConfig: function () {
12513
		return {
12514
			x: this.category,
12515
			y: this.y,
12516
			color: this.color,
12517
			key: this.name || this.category,
12518
			series: this.series,
12519
			point: this,
12520
			percentage: this.percentage,
12521
			total: this.total || this.stackTotal
12522
		};
12523
	},
12524
 
12525
	/**
12526
	 * Extendable method for formatting each point's tooltip line
12527
	 *
12528
	 * @return {String} A string to be concatenated in to the common tooltip text
12529
	 */
12530
	tooltipFormatter: function (pointFormat) {
12531
 
12532
		// Insert options for valueDecimals, valuePrefix, and valueSuffix
12533
		var series = this.series,
12534
			seriesTooltipOptions = series.tooltipOptions,
12535
			valueDecimals = pick(seriesTooltipOptions.valueDecimals, ''),
12536
			valuePrefix = seriesTooltipOptions.valuePrefix || '',
12537
			valueSuffix = seriesTooltipOptions.valueSuffix || '';
12538
 
12539
		// Loop over the point array map and replace unformatted values with sprintf formatting markup
12540
		each(series.pointArrayMap || ['y'], function (key) {
12541
			key = '{point.' + key; // without the closing bracket
12542
			if (valuePrefix || valueSuffix) {
12543
				pointFormat = pointFormat.replace(key + '}', valuePrefix + key + '}' + valueSuffix);
12544
			}
12545
			pointFormat = pointFormat.replace(key + '}', key + ':,.' + valueDecimals + 'f}');
12546
		});
12547
 
12548
		return format(pointFormat, {
12549
			point: this,
12550
			series: this.series
12551
		});
12552
	},
12553
 
12554
	/**
12555
	 * Fire an event on the Point object. Must not be renamed to fireEvent, as this
12556
	 * causes a name clash in MooTools
12557
	 * @param {String} eventType
12558
	 * @param {Object} eventArgs Additional event arguments
12559
	 * @param {Function} defaultFunction Default event handler
12560
	 */
12561
	firePointEvent: function (eventType, eventArgs, defaultFunction) {
12562
		var point = this,
12563
			series = this.series,
12564
			seriesOptions = series.options;
12565
 
12566
		// load event handlers on demand to save time on mouseover/out
12567
		if (seriesOptions.point.events[eventType] || (point.options && point.options.events && point.options.events[eventType])) {
12568
			this.importEvents();
12569
		}
12570
 
12571
		// add default handler if in selection mode
12572
		if (eventType === 'click' && seriesOptions.allowPointSelect) {
12573
			defaultFunction = function (event) {
12574
				// Control key is for Windows, meta (= Cmd key) for Mac, Shift for Opera
12575
				if (point.select) { // Could be destroyed by prior event handlers (#2911)
12576
					point.select(null, event.ctrlKey || event.metaKey || event.shiftKey);
12577
				}
12578
			};
12579
		}
12580
 
12581
		fireEvent(this, eventType, eventArgs, defaultFunction);
12582
	},
12583
	visible: true
12584
};/**
12585
 * @classDescription The base function which all other series types inherit from. The data in the series is stored
12586
 * in various arrays.
12587
 *
12588
 * - First, series.options.data contains all the original config options for
12589
 * each point whether added by options or methods like series.addPoint.
12590
 * - Next, series.data contains those values converted to points, but in case the series data length
12591
 * exceeds the cropThreshold, or if the data is grouped, series.data doesn't contain all the points. It
12592
 * only contains the points that have been created on demand.
12593
 * - Then there's series.points that contains all currently visible point objects. In case of cropping,
12594
 * the cropped-away points are not part of this array. The series.points array starts at series.cropStart
12595
 * compared to series.data and series.options.data. If however the series data is grouped, these can't
12596
 * be correlated one to one.
12597
 * - series.xData and series.processedXData contain clean x values, equivalent to series.data and series.points.
12598
 * - series.yData and series.processedYData contain clean x values, equivalent to series.data and series.points.
12599
 *
12600
 * @param {Object} chart
12601
 * @param {Object} options
12602
 */
12603
var Series = Highcharts.Series = function () {};
12604
 
12605
Series.prototype = {
12606
 
12607
	isCartesian: true,
12608
	type: 'line',
12609
	pointClass: Point,
12610
	sorted: true, // requires the data to be sorted
12611
	requireSorting: true,
12612
	pointAttrToOptions: { // mapping between SVG attributes and the corresponding options
12613
		stroke: 'lineColor',
12614
		'stroke-width': 'lineWidth',
12615
		fill: 'fillColor',
12616
		r: 'radius'
12617
	},
12618
	directTouch: false,
12619
	axisTypes: ['xAxis', 'yAxis'],
12620
	colorCounter: 0,
12621
	parallelArrays: ['x', 'y'], // each point's x and y values are stored in this.xData and this.yData
12622
	init: function (chart, options) {
12623
		var series = this,
12624
			eventType,
12625
			events,
12626
			chartSeries = chart.series,
12627
			sortByIndex = function (a, b) {
12628
				return pick(a.options.index, a._i) - pick(b.options.index, b._i);
12629
			};
12630
 
12631
		series.chart = chart;
12632
		series.options = options = series.setOptions(options); // merge with plotOptions
12633
		series.linkedSeries = [];
12634
 
12635
		// bind the axes
12636
		series.bindAxes();
12637
 
12638
		// set some variables
12639
		extend(series, {
12640
			name: options.name,
12641
			state: NORMAL_STATE,
12642
			pointAttr: {},
12643
			visible: options.visible !== false, // true by default
12644
			selected: options.selected === true // false by default
12645
		});
12646
 
12647
		// special
12648
		if (useCanVG) {
12649
			options.animation = false;
12650
		}
12651
 
12652
		// register event listeners
12653
		events = options.events;
12654
		for (eventType in events) {
12655
			addEvent(series, eventType, events[eventType]);
12656
		}
12657
		if (
12658
			(events && events.click) ||
12659
			(options.point && options.point.events && options.point.events.click) ||
12660
			options.allowPointSelect
12661
		) {
12662
			chart.runTrackerClick = true;
12663
		}
12664
 
12665
		series.getColor();
12666
		series.getSymbol();
12667
 
12668
		// Set the data
12669
		each(series.parallelArrays, function (key) {
12670
			series[key + 'Data'] = [];
12671
		});
12672
		series.setData(options.data, false);
12673
 
12674
		// Mark cartesian
12675
		if (series.isCartesian) {
12676
			chart.hasCartesianSeries = true;
12677
		}
12678
 
12679
		// Register it in the chart
12680
		chartSeries.push(series);
12681
		series._i = chartSeries.length - 1;
12682
 
12683
		// Sort series according to index option (#248, #1123, #2456)
12684
		stableSort(chartSeries, sortByIndex);
12685
		if (this.yAxis) {
12686
			stableSort(this.yAxis.series, sortByIndex);
12687
		}
12688
 
12689
		each(chartSeries, function (series, i) {
12690
			series.index = i;
12691
			series.name = series.name || 'Series ' + (i + 1);
12692
		});
12693
 
12694
	},
12695
 
12696
	/**
12697
	 * Set the xAxis and yAxis properties of cartesian series, and register the series
12698
	 * in the axis.series array
12699
	 */
12700
	bindAxes: function () {
12701
		var series = this,
12702
			seriesOptions = series.options,
12703
			chart = series.chart,
12704
			axisOptions;
12705
 
12706
		each(series.axisTypes || [], function (AXIS) { // repeat for xAxis and yAxis
12707
 
12708
			each(chart[AXIS], function (axis) { // loop through the chart's axis objects
12709
				axisOptions = axis.options;
12710
 
12711
				// apply if the series xAxis or yAxis option mathches the number of the
12712
				// axis, or if undefined, use the first axis
12713
				if ((seriesOptions[AXIS] === axisOptions.index) ||
12714
						(seriesOptions[AXIS] !== UNDEFINED && seriesOptions[AXIS] === axisOptions.id) ||
12715
						(seriesOptions[AXIS] === UNDEFINED && axisOptions.index === 0)) {
12716
 
12717
					// register this series in the axis.series lookup
12718
					axis.series.push(series);
12719
 
12720
					// set this series.xAxis or series.yAxis reference
12721
					series[AXIS] = axis;
12722
 
12723
					// mark dirty for redraw
12724
					axis.isDirty = true;
12725
				}
12726
			});
12727
 
12728
			// The series needs an X and an Y axis
12729
			if (!series[AXIS] && series.optionalAxis !== AXIS) {
12730
				error(18, true);
12731
			}
12732
 
12733
		});
12734
	},
12735
 
12736
	/**
12737
	 * For simple series types like line and column, the data values are held in arrays like
12738
	 * xData and yData for quick lookup to find extremes and more. For multidimensional series
12739
	 * like bubble and map, this can be extended with arrays like zData and valueData by
12740
	 * adding to the series.parallelArrays array.
12741
	 */
12742
	updateParallelArrays: function (point, i) {
12743
		var series = point.series,
12744
			args = arguments,
12745
			fn = typeof i === 'number' ?
12746
				 // Insert the value in the given position
12747
				function (key) {
12748
					var val = key === 'y' && series.toYData ? series.toYData(point) : point[key];
12749
					series[key + 'Data'][i] = val;
12750
				} :
12751
				// Apply the method specified in i with the following arguments as arguments
12752
				function (key) {
12753
					Array.prototype[i].apply(series[key + 'Data'], Array.prototype.slice.call(args, 2));
12754
				};
12755
 
12756
		each(series.parallelArrays, fn);
12757
	},
12758
 
12759
	/**
12760
	 * Return an auto incremented x value based on the pointStart and pointInterval options.
12761
	 * This is only used if an x value is not given for the point that calls autoIncrement.
12762
	 */
12763
	autoIncrement: function () {
12764
 
12765
		var options = this.options,
12766
			xIncrement = this.xIncrement,
12767
			date,
12768
			pointInterval,
12769
			pointIntervalUnit = options.pointIntervalUnit;
12770
 
12771
		xIncrement = pick(xIncrement, options.pointStart, 0);
12772
 
12773
		this.pointInterval = pointInterval = pick(this.pointInterval, options.pointInterval, 1);
12774
 
12775
		// Added code for pointInterval strings
12776
		if (pointIntervalUnit === 'month' || pointIntervalUnit === 'year') {
12777
			date = new Date(xIncrement);
12778
			date = (pointIntervalUnit === 'month') ?
12779
				+date[setMonth](date[getMonth]() + pointInterval) :
12780
				+date[setFullYear](date[getFullYear]() + pointInterval);
12781
			pointInterval = date - xIncrement;
12782
		}
12783
 
12784
		this.xIncrement = xIncrement + pointInterval;
12785
		return xIncrement;
12786
	},
12787
 
12788
	/**
12789
	 * Divide the series data into segments divided by null values.
12790
	 */
12791
	getSegments: function () {
12792
		var series = this,
12793
			lastNull = -1,
12794
			segments = [],
12795
			i,
12796
			points = series.points,
12797
			pointsLength = points.length;
12798
 
12799
		if (pointsLength) { // no action required for []
12800
 
12801
			// if connect nulls, just remove null points
12802
			if (series.options.connectNulls) {
12803
				i = pointsLength;
12804
				while (i--) {
12805
					if (points[i].y === null) {
12806
						points.splice(i, 1);
12807
					}
12808
				}
12809
				if (points.length) {
12810
					segments = [points];
12811
				}
12812
 
12813
			// else, split on null points
12814
			} else {
12815
				each(points, function (point, i) {
12816
					if (point.y === null) {
12817
						if (i > lastNull + 1) {
12818
							segments.push(points.slice(lastNull + 1, i));
12819
						}
12820
						lastNull = i;
12821
					} else if (i === pointsLength - 1) { // last value
12822
						segments.push(points.slice(lastNull + 1, i + 1));
12823
					}
12824
				});
12825
			}
12826
		}
12827
 
12828
		// register it
12829
		series.segments = segments;
12830
	},
12831
 
12832
	/**
12833
	 * Set the series options by merging from the options tree
12834
	 * @param {Object} itemOptions
12835
	 */
12836
	setOptions: function (itemOptions) {
12837
		var chart = this.chart,
12838
			chartOptions = chart.options,
12839
			plotOptions = chartOptions.plotOptions,
12840
			userOptions = chart.userOptions || {},
12841
			userPlotOptions = userOptions.plotOptions || {},
12842
			typeOptions = plotOptions[this.type],
12843
			options,
12844
			zones;
12845
 
12846
		this.userOptions = itemOptions;
12847
 
12848
		// General series options take precedence over type options because otherwise, default
12849
		// type options like column.animation would be overwritten by the general option.
12850
		// But issues have been raised here (#3881), and the solution may be to distinguish
12851
		// between default option and userOptions like in the tooltip below.
12852
		options = merge(
12853
			typeOptions,
12854
			plotOptions.series,
12855
			itemOptions
12856
		);
12857
 
12858
		// The tooltip options are merged between global and series specific options
12859
		this.tooltipOptions = merge(
12860
			defaultOptions.tooltip,
12861
			defaultOptions.plotOptions[this.type].tooltip,
12862
			userOptions.tooltip,
12863
			userPlotOptions.series && userPlotOptions.series.tooltip,
12864
			userPlotOptions[this.type] && userPlotOptions[this.type].tooltip,
12865
			itemOptions.tooltip
12866
		);
12867
 
12868
		// Delete marker object if not allowed (#1125)
12869
		if (typeOptions.marker === null) {
12870
			delete options.marker;
12871
		}
12872
 
12873
		// Handle color zones
12874
		this.zoneAxis = options.zoneAxis;
12875
		zones = this.zones = (options.zones || []).slice();
12876
		if ((options.negativeColor || options.negativeFillColor) && !options.zones) {
12877
			zones.push({
12878
				value: options[this.zoneAxis + 'Threshold'] || options.threshold || 0,
12879
				color: options.negativeColor,
12880
				fillColor: options.negativeFillColor
12881
			});
12882
		}
12883
		if (zones.length) { // Push one extra zone for the rest
12884
			if (defined(zones[zones.length - 1].value)) {
12885
				zones.push({
12886
					color: this.color,
12887
					fillColor: this.fillColor
12888
				});
12889
			}
12890
		}
12891
		return options;
12892
	},
12893
 
12894
	getCyclic: function (prop, value, defaults) {
12895
		var i,
12896
			userOptions = this.userOptions,
12897
			indexName = '_' + prop + 'Index',
12898
			counterName = prop + 'Counter';
12899
 
12900
		if (!value) {
12901
			if (defined(userOptions[indexName])) { // after Series.update()
12902
				i = userOptions[indexName];
12903
			} else {
12904
				userOptions[indexName] = i = this.chart[counterName] % defaults.length;
12905
				this.chart[counterName] += 1;
12906
			}
12907
			value = defaults[i];
12908
		}
12909
		this[prop] = value;
12910
	},
12911
 
12912
	/**
12913
	 * Get the series' color
12914
	 */
12915
	getColor: function () {
12916
		if (this.options.colorByPoint) {
12917
			this.options.color = null; // #4359, selected slice got series.color even when colorByPoint was set.
12918
		} else {
12919
			this.getCyclic('color', this.options.color || defaultPlotOptions[this.type].color, this.chart.options.colors);
12920
		}
12921
	},
12922
	/**
12923
	 * Get the series' symbol
12924
	 */
12925
	getSymbol: function () {
12926
		var seriesMarkerOption = this.options.marker;
12927
 
12928
		this.getCyclic('symbol', seriesMarkerOption.symbol, this.chart.options.symbols);
12929
 
12930
		// don't substract radius in image symbols (#604)
12931
		if (/^url/.test(this.symbol)) {
12932
			seriesMarkerOption.radius = 0;
12933
		}
12934
	},
12935
 
12936
	drawLegendSymbol: LegendSymbolMixin.drawLineMarker,
12937
 
12938
	/**
12939
	 * Replace the series data with a new set of data
12940
	 * @param {Object} data
12941
	 * @param {Object} redraw
12942
	 */
12943
	setData: function (data, redraw, animation, updatePoints) {
12944
		var series = this,
12945
			oldData = series.points,
12946
			oldDataLength = (oldData && oldData.length) || 0,
12947
			dataLength,
12948
			options = series.options,
12949
			chart = series.chart,
12950
			firstPoint = null,
12951
			xAxis = series.xAxis,
12952
			hasCategories = xAxis && !!xAxis.categories,
12953
			i,
12954
			turboThreshold = options.turboThreshold,
12955
			pt,
12956
			xData = this.xData,
12957
			yData = this.yData,
12958
			pointArrayMap = series.pointArrayMap,
12959
			valueCount = pointArrayMap && pointArrayMap.length;
12960
 
12961
		data = data || [];
12962
		dataLength = data.length;
12963
		redraw = pick(redraw, true);
12964
 
12965
		// If the point count is the same as is was, just run Point.update which is
12966
		// cheaper, allows animation, and keeps references to points.
12967
		if (updatePoints !== false && dataLength && oldDataLength === dataLength && !series.cropped && !series.hasGroupedData && series.visible) {
12968
			each(data, function (point, i) {
12969
				if (oldData[i].update) { // Linked, previously hidden series (#3709)
12970
					oldData[i].update(point, false, null, false);
12971
				}
12972
			});
12973
 
12974
		} else {
12975
 
12976
			// Reset properties
12977
			series.xIncrement = null;
12978
			series.pointRange = hasCategories ? 1 : options.pointRange;
12979
 
12980
			series.colorCounter = 0; // for series with colorByPoint (#1547)
12981
 
12982
			// Update parallel arrays
12983
			each(this.parallelArrays, function (key) {
12984
				series[key + 'Data'].length = 0;
12985
			});
12986
 
12987
			// In turbo mode, only one- or twodimensional arrays of numbers are allowed. The
12988
			// first value is tested, and we assume that all the rest are defined the same
12989
			// way. Although the 'for' loops are similar, they are repeated inside each
12990
			// if-else conditional for max performance.
12991
			if (turboThreshold && dataLength > turboThreshold) {
12992
 
12993
				// find the first non-null point
12994
				i = 0;
12995
				while (firstPoint === null && i < dataLength) {
12996
					firstPoint = data[i];
12997
					i++;
12998
				}
12999
 
13000
 
13001
				if (isNumber(firstPoint)) { // assume all points are numbers
13002
					var x = pick(options.pointStart, 0),
13003
						pointInterval = pick(options.pointInterval, 1);
13004
 
13005
					for (i = 0; i < dataLength; i++) {
13006
						xData[i] = x;
13007
						yData[i] = data[i];
13008
						x += pointInterval;
13009
					}
13010
					series.xIncrement = x;
13011
				} else if (isArray(firstPoint)) { // assume all points are arrays
13012
					if (valueCount) { // [x, low, high] or [x, o, h, l, c]
13013
						for (i = 0; i < dataLength; i++) {
13014
							pt = data[i];
13015
							xData[i] = pt[0];
13016
							yData[i] = pt.slice(1, valueCount + 1);
13017
						}
13018
					} else { // [x, y]
13019
						for (i = 0; i < dataLength; i++) {
13020
							pt = data[i];
13021
							xData[i] = pt[0];
13022
							yData[i] = pt[1];
13023
						}
13024
					}
13025
				} else {
13026
					error(12); // Highcharts expects configs to be numbers or arrays in turbo mode
13027
				}
13028
			} else {
13029
				for (i = 0; i < dataLength; i++) {
13030
					if (data[i] !== UNDEFINED) { // stray commas in oldIE
13031
						pt = { series: series };
13032
						series.pointClass.prototype.applyOptions.apply(pt, [data[i]]);
13033
						series.updateParallelArrays(pt, i);
13034
						if (hasCategories && defined(pt.name)) { // #4401
13035
							xAxis.names[pt.x] = pt.name; // #2046
13036
						}
13037
					}
13038
				}
13039
			}
13040
 
13041
			// Forgetting to cast strings to numbers is a common caveat when handling CSV or JSON
13042
			if (isString(yData[0])) {
13043
				error(14, true);
13044
			}
13045
 
13046
			series.data = [];
13047
			series.options.data = data;
13048
			//series.zData = zData;
13049
 
13050
			// destroy old points
13051
			i = oldDataLength;
13052
			while (i--) {
13053
				if (oldData[i] && oldData[i].destroy) {
13054
					oldData[i].destroy();
13055
				}
13056
			}
13057
 
13058
			// reset minRange (#878)
13059
			if (xAxis) {
13060
				xAxis.minRange = xAxis.userMinRange;
13061
			}
13062
 
13063
			// redraw
13064
			series.isDirty = series.isDirtyData = chart.isDirtyBox = true;
13065
			animation = false;
13066
		}
13067
 
13068
		// Typically for pie series, points need to be processed and generated
13069
		// prior to rendering the legend
13070
		if (options.legendType === 'point') { // docs: legendType now supported on more series types (at least column and pie)
13071
			this.processData();
13072
			this.generatePoints();
13073
		}
13074
 
13075
		if (redraw) {
13076
			chart.redraw(animation);
13077
		}
13078
	},
13079
 
13080
	/**
13081
	 * Process the data by cropping away unused data points if the series is longer
13082
	 * than the crop threshold. This saves computing time for lage series.
13083
	 */
13084
	processData: function (force) {
13085
		var series = this,
13086
			processedXData = series.xData, // copied during slice operation below
13087
			processedYData = series.yData,
13088
			dataLength = processedXData.length,
13089
			croppedData,
13090
			cropStart = 0,
13091
			cropped,
13092
			distance,
13093
			closestPointRange,
13094
			xAxis = series.xAxis,
13095
			i, // loop variable
13096
			options = series.options,
13097
			cropThreshold = options.cropThreshold,
13098
			getExtremesFromAll = series.getExtremesFromAll || options.getExtremesFromAll, // #4599
13099
			isCartesian = series.isCartesian,
13100
			xExtremes,
13101
			min,
13102
			max;
13103
 
13104
		// If the series data or axes haven't changed, don't go through this. Return false to pass
13105
		// the message on to override methods like in data grouping.
13106
		if (isCartesian && !series.isDirty && !xAxis.isDirty && !series.yAxis.isDirty && !force) {
13107
			return false;
13108
		}
13109
 
13110
		if (xAxis) {
13111
			xExtremes = xAxis.getExtremes(); // corrected for log axis (#3053)
13112
			min = xExtremes.min;
13113
			max = xExtremes.max;
13114
		}
13115
 
13116
		// optionally filter out points outside the plot area
13117
		if (isCartesian && series.sorted && !getExtremesFromAll && (!cropThreshold || dataLength > cropThreshold || series.forceCrop)) {
13118
 
13119
			// it's outside current extremes
13120
			if (processedXData[dataLength - 1] < min || processedXData[0] > max) {
13121
				processedXData = [];
13122
				processedYData = [];
13123
 
13124
			// only crop if it's actually spilling out
13125
			} else if (processedXData[0] < min || processedXData[dataLength - 1] > max) {
13126
				croppedData = this.cropData(series.xData, series.yData, min, max);
13127
				processedXData = croppedData.xData;
13128
				processedYData = croppedData.yData;
13129
				cropStart = croppedData.start;
13130
				cropped = true;
13131
			}
13132
		}
13133
 
13134
 
13135
		// Find the closest distance between processed points
13136
		for (i = processedXData.length - 1; i >= 0; i--) {
13137
			distance = processedXData[i] - processedXData[i - 1];
13138
 
13139
			if (distance > 0 && (closestPointRange === UNDEFINED || distance < closestPointRange)) {
13140
				closestPointRange = distance;
13141
 
13142
			// Unsorted data is not supported by the line tooltip, as well as data grouping and
13143
			// navigation in Stock charts (#725) and width calculation of columns (#1900)
13144
			} else if (distance < 0 && series.requireSorting) {
13145
				error(15);
13146
			}
13147
		}
13148
 
13149
		// Record the properties
13150
		series.cropped = cropped; // undefined or true
13151
		series.cropStart = cropStart;
13152
		series.processedXData = processedXData;
13153
		series.processedYData = processedYData;
13154
 
13155
		if (options.pointRange === null) { // null means auto, as for columns, candlesticks and OHLC
13156
			series.pointRange = closestPointRange || 1;
13157
		}
13158
		series.closestPointRange = closestPointRange;
13159
 
13160
	},
13161
 
13162
	/**
13163
	 * Iterate over xData and crop values between min and max. Returns object containing crop start/end
13164
	 * cropped xData with corresponding part of yData, dataMin and dataMax within the cropped range
13165
	 */
13166
	cropData: function (xData, yData, min, max) {
13167
		var dataLength = xData.length,
13168
			cropStart = 0,
13169
			cropEnd = dataLength,
13170
			cropShoulder = pick(this.cropShoulder, 1), // line-type series need one point outside
13171
			i;
13172
 
13173
		// iterate up to find slice start
13174
		for (i = 0; i < dataLength; i++) {
13175
			if (xData[i] >= min) {
13176
				cropStart = mathMax(0, i - cropShoulder);
13177
				break;
13178
			}
13179
		}
13180
 
13181
		// proceed to find slice end
13182
		for (; i < dataLength; i++) {
13183
			if (xData[i] > max) {
13184
				cropEnd = i + cropShoulder;
13185
				break;
13186
			}
13187
		}
13188
 
13189
		return {
13190
			xData: xData.slice(cropStart, cropEnd),
13191
			yData: yData.slice(cropStart, cropEnd),
13192
			start: cropStart,
13193
			end: cropEnd
13194
		};
13195
	},
13196
 
13197
 
13198
	/**
13199
	 * Generate the data point after the data has been processed by cropping away
13200
	 * unused points and optionally grouped in Highcharts Stock.
13201
	 */
13202
	generatePoints: function () {
13203
		var series = this,
13204
			options = series.options,
13205
			dataOptions = options.data,
13206
			data = series.data,
13207
			dataLength,
13208
			processedXData = series.processedXData,
13209
			processedYData = series.processedYData,
13210
			pointClass = series.pointClass,
13211
			processedDataLength = processedXData.length,
13212
			cropStart = series.cropStart || 0,
13213
			cursor,
13214
			hasGroupedData = series.hasGroupedData,
13215
			point,
13216
			points = [],
13217
			i;
13218
 
13219
		if (!data && !hasGroupedData) {
13220
			var arr = [];
13221
			arr.length = dataOptions.length;
13222
			data = series.data = arr;
13223
		}
13224
 
13225
		for (i = 0; i < processedDataLength; i++) {
13226
			cursor = cropStart + i;
13227
			if (!hasGroupedData) {
13228
				if (data[cursor]) {
13229
					point = data[cursor];
13230
				} else if (dataOptions[cursor] !== UNDEFINED) { // #970
13231
					data[cursor] = point = (new pointClass()).init(series, dataOptions[cursor], processedXData[i]);
13232
				}
13233
				points[i] = point;
13234
			} else {
13235
				// splat the y data in case of ohlc data array
13236
				points[i] = (new pointClass()).init(series, [processedXData[i]].concat(splat(processedYData[i])));
13237
			}
13238
			points[i].index = cursor; // For faster access in Point.update
13239
		}
13240
 
13241
		// Hide cropped-away points - this only runs when the number of points is above cropThreshold, or when
13242
		// swithching view from non-grouped data to grouped data (#637)
13243
		if (data && (processedDataLength !== (dataLength = data.length) || hasGroupedData)) {
13244
			for (i = 0; i < dataLength; i++) {
13245
				if (i === cropStart && !hasGroupedData) { // when has grouped data, clear all points
13246
					i += processedDataLength;
13247
				}
13248
				if (data[i]) {
13249
					data[i].destroyElements();
13250
					data[i].plotX = UNDEFINED; // #1003
13251
				}
13252
			}
13253
		}
13254
 
13255
		series.data = data;
13256
		series.points = points;
13257
	},
13258
 
13259
	/**
13260
	 * Calculate Y extremes for visible data
13261
	 */
13262
	getExtremes: function (yData) {
13263
		var xAxis = this.xAxis,
13264
			yAxis = this.yAxis,
13265
			xData = this.processedXData,
13266
			yDataLength,
13267
			activeYData = [],
13268
			activeCounter = 0,
13269
			xExtremes = xAxis.getExtremes(), // #2117, need to compensate for log X axis
13270
			xMin = xExtremes.min,
13271
			xMax = xExtremes.max,
13272
			validValue,
13273
			withinRange,
13274
			x,
13275
			y,
13276
			i,
13277
			j;
13278
 
13279
		yData = yData || this.stackedYData || this.processedYData;
13280
		yDataLength = yData.length;
13281
 
13282
		for (i = 0; i < yDataLength; i++) {
13283
 
13284
			x = xData[i];
13285
			y = yData[i];
13286
 
13287
			// For points within the visible range, including the first point outside the
13288
			// visible range, consider y extremes
13289
			validValue = y !== null && y !== UNDEFINED && (!yAxis.isLog || (y.length || y > 0));
13290
			withinRange = this.getExtremesFromAll || this.options.getExtremesFromAll || this.cropped ||
13291
				((xData[i + 1] || x) >= xMin &&	(xData[i - 1] || x) <= xMax);
13292
 
13293
			if (validValue && withinRange) {
13294
 
13295
				j = y.length;
13296
				if (j) { // array, like ohlc or range data
13297
					while (j--) {
13298
						if (y[j] !== null) {
13299
							activeYData[activeCounter++] = y[j];
13300
						}
13301
					}
13302
				} else {
13303
					activeYData[activeCounter++] = y;
13304
				}
13305
			}
13306
		}
13307
		this.dataMin = arrayMin(activeYData);
13308
		this.dataMax = arrayMax(activeYData);
13309
	},
13310
 
13311
	/**
13312
	 * Translate data points from raw data values to chart specific positioning data
13313
	 * needed later in drawPoints, drawGraph and drawTracker.
13314
	 */
13315
	translate: function () {
13316
		if (!this.processedXData) { // hidden series
13317
			this.processData();
13318
		}
13319
		this.generatePoints();
13320
		var series = this,
13321
			options = series.options,
13322
			stacking = options.stacking,
13323
			xAxis = series.xAxis,
13324
			categories = xAxis.categories,
13325
			yAxis = series.yAxis,
13326
			points = series.points,
13327
			dataLength = points.length,
13328
			hasModifyValue = !!series.modifyValue,
13329
			i,
13330
			pointPlacement = options.pointPlacement,
13331
			dynamicallyPlaced = pointPlacement === 'between' || isNumber(pointPlacement),
13332
			threshold = options.threshold,
13333
			stackThreshold = options.startFromThreshold ? threshold : 0,
13334
			plotX,
13335
			plotY,
13336
			lastPlotX,
13337
			stackIndicator,
13338
			closestPointRangePx = Number.MAX_VALUE;
13339
 
13340
		// Translate each point
13341
		for (i = 0; i < dataLength; i++) {
13342
			var point = points[i],
13343
				xValue = point.x,
13344
				yValue = point.y,
13345
				yBottom = point.low,
13346
				stack = stacking && yAxis.stacks[(series.negStacks && yValue < (stackThreshold ? 0 : threshold) ? '-' : '') + series.stackKey],
13347
				pointStack,
13348
				stackValues;
13349
 
13350
			// Discard disallowed y values for log axes (#3434)
13351
			if (yAxis.isLog && yValue !== null && yValue <= 0) {
13352
				point.y = yValue = null;
13353
				error(10);
13354
			}
13355
 
13356
			// Get the plotX translation
13357
			point.plotX = plotX = mathMin(mathMax(-1e5, xAxis.translate(xValue, 0, 0, 0, 1, pointPlacement, this.type === 'flags')), 1e5); // #3923
13358
 
13359
 
13360
			// Calculate the bottom y value for stacked series
13361
			if (stacking && series.visible && stack && stack[xValue]) {
13362
				stackIndicator = series.getStackIndicator(stackIndicator, xValue, series.index);
13363
				pointStack = stack[xValue];
13364
				stackValues = pointStack.points[stackIndicator.key];
13365
				yBottom = stackValues[0];
13366
				yValue = stackValues[1];
13367
 
13368
				if (yBottom === stackThreshold) {
13369
					yBottom = pick(threshold, yAxis.min);
13370
				}
13371
				if (yAxis.isLog && yBottom <= 0) { // #1200, #1232
13372
					yBottom = null;
13373
				}
13374
 
13375
				point.total = point.stackTotal = pointStack.total;
13376
				point.percentage = pointStack.total && (point.y / pointStack.total * 100);
13377
				point.stackY = yValue;
13378
 
13379
				// Place the stack label
13380
				pointStack.setOffset(series.pointXOffset || 0, series.barW || 0);
13381
 
13382
			}
13383
 
13384
			// Set translated yBottom or remove it
13385
			point.yBottom = defined(yBottom) ?
13386
				yAxis.translate(yBottom, 0, 1, 0, 1) :
13387
				null;
13388
 
13389
			// general hook, used for Highstock compare mode
13390
			if (hasModifyValue) {
13391
				yValue = series.modifyValue(yValue, point);
13392
			}
13393
 
13394
			// Set the the plotY value, reset it for redraws
13395
			point.plotY = plotY = (typeof yValue === 'number' && yValue !== Infinity) ?
13396
				mathMin(mathMax(-1e5, yAxis.translate(yValue, 0, 1, 0, 1)), 1e5) : // #3201
13397
				UNDEFINED;
13398
			point.isInside = plotY !== UNDEFINED && plotY >= 0 && plotY <= yAxis.len && // #3519
13399
				plotX >= 0 && plotX <= xAxis.len;
13400
 
13401
 
13402
			// Set client related positions for mouse tracking
13403
			point.clientX = dynamicallyPlaced ? xAxis.translate(xValue, 0, 0, 0, 1) : plotX; // #1514
13404
 
13405
			point.negative = point.y < (threshold || 0);
13406
 
13407
			// some API data
13408
			point.category = categories && categories[point.x] !== UNDEFINED ?
13409
				categories[point.x] : point.x;
13410
 
13411
			// Determine auto enabling of markers (#3635)
13412
			if (i) {
13413
				closestPointRangePx = mathMin(closestPointRangePx, mathAbs(plotX - lastPlotX));
13414
			}
13415
			lastPlotX = plotX;
13416
 
13417
		}
13418
 
13419
		series.closestPointRangePx = closestPointRangePx;
13420
 
13421
		// now that we have the cropped data, build the segments
13422
		series.getSegments();
13423
	},
13424
 
13425
	/**
13426
	 * Set the clipping for the series. For animated series it is called twice, first to initiate
13427
	 * animating the clip then the second time without the animation to set the final clip.
13428
	 */
13429
	setClip: function (animation) {
13430
		var chart = this.chart,
13431
			options = this.options,
13432
			renderer = chart.renderer,
13433
			inverted = chart.inverted,
13434
			seriesClipBox = this.clipBox,
13435
			clipBox = seriesClipBox || chart.clipBox,
13436
			sharedClipKey = this.sharedClipKey || ['_sharedClip', animation && animation.duration, animation && animation.easing, clipBox.height, options.xAxis, options.yAxis].join(','), // #4526
13437
			clipRect = chart[sharedClipKey],
13438
			markerClipRect = chart[sharedClipKey + 'm'];
13439
 
13440
		// If a clipping rectangle with the same properties is currently present in the chart, use that.
13441
		if (!clipRect) {
13442
 
13443
			// When animation is set, prepare the initial positions
13444
			if (animation) {
13445
				clipBox.width = 0;
13446
 
13447
				chart[sharedClipKey + 'm'] = markerClipRect = renderer.clipRect(
13448
					-99, // include the width of the first marker
13449
					inverted ? -chart.plotLeft : -chart.plotTop,
13450
					99,
13451
					inverted ? chart.chartWidth : chart.chartHeight
13452
				);
13453
			}
13454
			chart[sharedClipKey] = clipRect = renderer.clipRect(clipBox);
13455
 
13456
		}
13457
		if (animation) {
13458
			clipRect.count += 1;
13459
		}
13460
 
13461
		if (options.clip !== false) {
13462
			this.group.clip(animation || seriesClipBox ? clipRect : chart.clipRect);
13463
			this.markerGroup.clip(markerClipRect);
13464
			this.sharedClipKey = sharedClipKey;
13465
		}
13466
 
13467
		// Remove the shared clipping rectangle when all series are shown
13468
		if (!animation) {
13469
			clipRect.count -= 1;
13470
			if (clipRect.count <= 0 && sharedClipKey && chart[sharedClipKey]) {
13471
				if (!seriesClipBox) {
13472
					chart[sharedClipKey] = chart[sharedClipKey].destroy();
13473
				}
13474
				if (chart[sharedClipKey + 'm']) {
13475
					chart[sharedClipKey + 'm'] = chart[sharedClipKey + 'm'].destroy();
13476
				}
13477
			}
13478
		}
13479
	},
13480
 
13481
	/**
13482
	 * Animate in the series
13483
	 */
13484
	animate: function (init) {
13485
		var series = this,
13486
			chart = series.chart,
13487
			clipRect,
13488
			animation = series.options.animation,
13489
			sharedClipKey;
13490
 
13491
		// Animation option is set to true
13492
		if (animation && !isObject(animation)) {
13493
			animation = defaultPlotOptions[series.type].animation;
13494
		}
13495
 
13496
		// Initialize the animation. Set up the clipping rectangle.
13497
		if (init) {
13498
 
13499
			series.setClip(animation);
13500
 
13501
		// Run the animation
13502
		} else {
13503
			sharedClipKey = this.sharedClipKey;
13504
			clipRect = chart[sharedClipKey];
13505
			if (clipRect) {
13506
				clipRect.animate({
13507
					width: chart.plotSizeX
13508
				}, animation);
13509
			}
13510
			if (chart[sharedClipKey + 'm']) {
13511
				chart[sharedClipKey + 'm'].animate({
13512
					width: chart.plotSizeX + 99
13513
				}, animation);
13514
			}
13515
 
13516
			// Delete this function to allow it only once
13517
			series.animate = null;
13518
 
13519
		}
13520
	},
13521
 
13522
	/**
13523
	 * This runs after animation to land on the final plot clipping
13524
	 */
13525
	afterAnimate: function () {
13526
		this.setClip();
13527
		fireEvent(this, 'afterAnimate');
13528
	},
13529
 
13530
	/**
13531
	 * Draw the markers
13532
	 */
13533
	drawPoints: function () {
13534
		var series = this,
13535
			pointAttr,
13536
			points = series.points,
13537
			chart = series.chart,
13538
			plotX,
13539
			plotY,
13540
			i,
13541
			point,
13542
			radius,
13543
			symbol,
13544
			isImage,
13545
			graphic,
13546
			options = series.options,
13547
			seriesMarkerOptions = options.marker,
13548
			seriesPointAttr = series.pointAttr[''],
13549
			pointMarkerOptions,
13550
			hasPointMarker,
13551
			enabled,
13552
			isInside,
13553
			markerGroup = series.markerGroup,
13554
			xAxis = series.xAxis,
13555
			globallyEnabled = pick(
13556
				seriesMarkerOptions.enabled,
13557
				xAxis.isRadial,
13558
				series.closestPointRangePx > 2 * seriesMarkerOptions.radius
13559
			);
13560
 
13561
		if (seriesMarkerOptions.enabled !== false || series._hasPointMarkers) {
13562
 
13563
			i = points.length;
13564
			while (i--) {
13565
				point = points[i];
13566
				plotX = mathFloor(point.plotX); // #1843
13567
				plotY = point.plotY;
13568
				graphic = point.graphic;
13569
				pointMarkerOptions = point.marker || {};
13570
				hasPointMarker = !!point.marker;
13571
				enabled = (globallyEnabled && pointMarkerOptions.enabled === UNDEFINED) || pointMarkerOptions.enabled;
13572
				isInside = point.isInside;
13573
 
13574
				// only draw the point if y is defined
13575
				if (enabled && plotY !== UNDEFINED && !isNaN(plotY) && point.y !== null) {
13576
 
13577
					// shortcuts
13578
					pointAttr = point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE] || seriesPointAttr;
13579
					radius = pointAttr.r;
13580
					symbol = pick(pointMarkerOptions.symbol, series.symbol);
13581
					isImage = symbol.indexOf('url') === 0;
13582
 
13583
					if (graphic) { // update
13584
						graphic[isInside ? 'show' : 'hide'](true) // Since the marker group isn't clipped, each individual marker must be toggled
13585
							.animate(extend({
13586
								x: plotX - radius,
13587
								y: plotY - radius
13588
							}, graphic.symbolName ? { // don't apply to image symbols #507
13589
								width: 2 * radius,
13590
								height: 2 * radius
13591
							} : {}));
13592
					} else if (isInside && (radius > 0 || isImage)) {
13593
						point.graphic = graphic = chart.renderer.symbol(
13594
							symbol,
13595
							plotX - radius,
13596
							plotY - radius,
13597
							2 * radius,
13598
							2 * radius,
13599
							hasPointMarker ? pointMarkerOptions : seriesMarkerOptions
13600
						)
13601
						.attr(pointAttr)
13602
						.add(markerGroup);
13603
					}
13604
 
13605
				} else if (graphic) {
13606
					point.graphic = graphic.destroy(); // #1269
13607
				}
13608
			}
13609
		}
13610
 
13611
	},
13612
 
13613
	/**
13614
	 * Convert state properties from API naming conventions to SVG attributes
13615
	 *
13616
	 * @param {Object} options API options object
13617
	 * @param {Object} base1 SVG attribute object to inherit from
13618
	 * @param {Object} base2 Second level SVG attribute object to inherit from
13619
	 */
13620
	convertAttribs: function (options, base1, base2, base3) {
13621
		var conversion = this.pointAttrToOptions,
13622
			attr,
13623
			option,
13624
			obj = {};
13625
 
13626
		options = options || {};
13627
		base1 = base1 || {};
13628
		base2 = base2 || {};
13629
		base3 = base3 || {};
13630
 
13631
		for (attr in conversion) {
13632
			option = conversion[attr];
13633
			obj[attr] = pick(options[option], base1[attr], base2[attr], base3[attr]);
13634
		}
13635
		return obj;
13636
	},
13637
 
13638
	/**
13639
	 * Get the state attributes. Each series type has its own set of attributes
13640
	 * that are allowed to change on a point's state change. Series wide attributes are stored for
13641
	 * all series, and additionally point specific attributes are stored for all
13642
	 * points with individual marker options. If such options are not defined for the point,
13643
	 * a reference to the series wide attributes is stored in point.pointAttr.
13644
	 */
13645
	getAttribs: function () {
13646
		var series = this,
13647
			seriesOptions = series.options,
13648
			normalOptions = defaultPlotOptions[series.type].marker ? seriesOptions.marker : seriesOptions,
13649
			stateOptions = normalOptions.states,
13650
			stateOptionsHover = stateOptions[HOVER_STATE],
13651
			pointStateOptionsHover,
13652
			seriesColor = series.color,
13653
			seriesNegativeColor = series.options.negativeColor,
13654
			normalDefaults = {
13655
				stroke: seriesColor,
13656
				fill: seriesColor
13657
			},
13658
			points = series.points || [], // #927
13659
			i,
13660
			j,
13661
			threshold,
13662
			point,
13663
			seriesPointAttr = [],
13664
			pointAttr,
13665
			pointAttrToOptions = series.pointAttrToOptions,
13666
			hasPointSpecificOptions = series.hasPointSpecificOptions,
13667
			defaultLineColor = normalOptions.lineColor,
13668
			defaultFillColor = normalOptions.fillColor,
13669
			turboThreshold = seriesOptions.turboThreshold,
13670
			zones = series.zones,
13671
			zoneAxis = series.zoneAxis || 'y',
13672
			attr,
13673
			key;
13674
 
13675
		// series type specific modifications
13676
		if (seriesOptions.marker) { // line, spline, area, areaspline, scatter
13677
 
13678
			// if no hover radius is given, default to normal radius + 2
13679
			stateOptionsHover.radius = stateOptionsHover.radius || normalOptions.radius + stateOptionsHover.radiusPlus;
13680
			stateOptionsHover.lineWidth = stateOptionsHover.lineWidth || normalOptions.lineWidth + stateOptionsHover.lineWidthPlus;
13681
 
13682
		} else { // column, bar, pie
13683
 
13684
			// if no hover color is given, brighten the normal color
13685
			stateOptionsHover.color = stateOptionsHover.color ||
13686
				Color(stateOptionsHover.color || seriesColor)
13687
					.brighten(stateOptionsHover.brightness).get();
13688
 
13689
			// if no hover negativeColor is given, brighten the normal negativeColor
13690
			stateOptionsHover.negativeColor = stateOptionsHover.negativeColor ||
13691
				Color(stateOptionsHover.negativeColor || seriesNegativeColor)
13692
					.brighten(stateOptionsHover.brightness).get();
13693
		}
13694
 
13695
		// general point attributes for the series normal state
13696
		seriesPointAttr[NORMAL_STATE] = series.convertAttribs(normalOptions, normalDefaults);
13697
 
13698
		// HOVER_STATE and SELECT_STATE states inherit from normal state except the default radius
13699
		each([HOVER_STATE, SELECT_STATE], function (state) {
13700
			seriesPointAttr[state] =
13701
					series.convertAttribs(stateOptions[state], seriesPointAttr[NORMAL_STATE]);
13702
		});
13703
 
13704
		// set it
13705
		series.pointAttr = seriesPointAttr;
13706
 
13707
 
13708
		// Generate the point-specific attribute collections if specific point
13709
		// options are given. If not, create a referance to the series wide point
13710
		// attributes
13711
		i = points.length;
13712
		if (!turboThreshold || i < turboThreshold || hasPointSpecificOptions) {
13713
			while (i--) {
13714
				point = points[i];
13715
				normalOptions = (point.options && point.options.marker) || point.options;
13716
				if (normalOptions && normalOptions.enabled === false) {
13717
					normalOptions.radius = 0;
13718
				}
13719
 
13720
				if (zones.length) {
13721
					j = 0;
13722
					threshold = zones[j];
13723
					while (point[zoneAxis] >= threshold.value) {
13724
						threshold = zones[++j];
13725
					}
13726
 
13727
					point.color = point.fillColor = pick(threshold.color, series.color); // #3636, #4267, #4430 - inherit color from series, when color is undefined
13728
 
13729
				}
13730
 
13731
				hasPointSpecificOptions = seriesOptions.colorByPoint || point.color; // #868
13732
 
13733
				// check if the point has specific visual options
13734
				if (point.options) {
13735
					for (key in pointAttrToOptions) {
13736
						if (defined(normalOptions[pointAttrToOptions[key]])) {
13737
							hasPointSpecificOptions = true;
13738
						}
13739
					}
13740
				}
13741
 
13742
				// a specific marker config object is defined for the individual point:
13743
				// create it's own attribute collection
13744
				if (hasPointSpecificOptions) {
13745
					normalOptions = normalOptions || {};
13746
					pointAttr = [];
13747
					stateOptions = normalOptions.states || {}; // reassign for individual point
13748
					pointStateOptionsHover = stateOptions[HOVER_STATE] = stateOptions[HOVER_STATE] || {};
13749
 
13750
					// Handle colors for column and pies
13751
					if (!seriesOptions.marker || (point.negative && !pointStateOptionsHover.fillColor && !stateOptionsHover.fillColor)) { // column, bar, point or negative threshold for series with markers (#3636)
13752
						// If no hover color is given, brighten the normal color. #1619, #2579
13753
						pointStateOptionsHover[series.pointAttrToOptions.fill] = pointStateOptionsHover.color || (!point.options.color && stateOptionsHover[(point.negative && seriesNegativeColor ? 'negativeColor' : 'color')]) ||
13754
							Color(point.color)
13755
								.brighten(pointStateOptionsHover.brightness || stateOptionsHover.brightness)
13756
								.get();
13757
					}
13758
 
13759
					// normal point state inherits series wide normal state
13760
					attr = { color: point.color }; // #868
13761
					if (!defaultFillColor) { // Individual point color or negative color markers (#2219)
13762
						attr.fillColor = point.color;
13763
					}
13764
					if (!defaultLineColor) {
13765
						attr.lineColor = point.color; // Bubbles take point color, line markers use white
13766
					}
13767
					// Color is explicitly set to null or undefined (#1288, #4068)
13768
					if (normalOptions.hasOwnProperty('color') && !normalOptions.color) {
13769
						delete normalOptions.color;
13770
					}
13771
					pointAttr[NORMAL_STATE] = series.convertAttribs(extend(attr, normalOptions), seriesPointAttr[NORMAL_STATE]);
13772
 
13773
					// inherit from point normal and series hover
13774
					pointAttr[HOVER_STATE] = series.convertAttribs(
13775
						stateOptions[HOVER_STATE],
13776
						seriesPointAttr[HOVER_STATE],
13777
						pointAttr[NORMAL_STATE]
13778
					);
13779
 
13780
					// inherit from point normal and series hover
13781
					pointAttr[SELECT_STATE] = series.convertAttribs(
13782
						stateOptions[SELECT_STATE],
13783
						seriesPointAttr[SELECT_STATE],
13784
						pointAttr[NORMAL_STATE]
13785
					);
13786
 
13787
 
13788
				// no marker config object is created: copy a reference to the series-wide
13789
				// attribute collection
13790
				} else {
13791
					pointAttr = seriesPointAttr;
13792
				}
13793
 
13794
				point.pointAttr = pointAttr;
13795
			}
13796
		}
13797
	},
13798
 
13799
	/**
13800
	 * Clear DOM objects and free up memory
13801
	 */
13802
	destroy: function () {
13803
		var series = this,
13804
			chart = series.chart,
13805
			issue134 = /AppleWebKit\/533/.test(userAgent),
13806
			destroy,
13807
			i,
13808
			data = series.data || [],
13809
			point,
13810
			prop,
13811
			axis;
13812
 
13813
		// add event hook
13814
		fireEvent(series, 'destroy');
13815
 
13816
		// remove all events
13817
		removeEvent(series);
13818
 
13819
		// erase from axes
13820
		each(series.axisTypes || [], function (AXIS) {
13821
			axis = series[AXIS];
13822
			if (axis) {
13823
				erase(axis.series, series);
13824
				axis.isDirty = axis.forceRedraw = true;
13825
			}
13826
		});
13827
 
13828
		// remove legend items
13829
		if (series.legendItem) {
13830
			series.chart.legend.destroyItem(series);
13831
		}
13832
 
13833
		// destroy all points with their elements
13834
		i = data.length;
13835
		while (i--) {
13836
			point = data[i];
13837
			if (point && point.destroy) {
13838
				point.destroy();
13839
			}
13840
		}
13841
		series.points = null;
13842
 
13843
		// Clear the animation timeout if we are destroying the series during initial animation
13844
		clearTimeout(series.animationTimeout);
13845
 
13846
		// Destroy all SVGElements associated to the series
13847
		for (prop in series) {
13848
			if (series[prop] instanceof SVGElement && !series[prop].survive) { // Survive provides a hook for not destroying
13849
 
13850
				// issue 134 workaround
13851
				destroy = issue134 && prop === 'group' ?
13852
					'hide' :
13853
					'destroy';
13854
 
13855
				series[prop][destroy]();
13856
			}
13857
		}
13858
 
13859
		// remove from hoverSeries
13860
		if (chart.hoverSeries === series) {
13861
			chart.hoverSeries = null;
13862
		}
13863
		erase(chart.series, series);
13864
 
13865
		// clear all members
13866
		for (prop in series) {
13867
			delete series[prop];
13868
		}
13869
	},
13870
 
13871
	/**
13872
	 * Return the graph path of a segment
13873
	 */
13874
	getSegmentPath: function (segment) {
13875
		var series = this,
13876
			segmentPath = [],
13877
			step = series.options.step;
13878
 
13879
		// build the segment line
13880
		each(segment, function (point, i) {
13881
 
13882
			var plotX = point.plotX,
13883
				plotY = point.plotY,
13884
				lastPoint;
13885
 
13886
			if (series.getPointSpline) { // generate the spline as defined in the SplineSeries object
13887
				segmentPath.push.apply(segmentPath, series.getPointSpline(segment, point, i));
13888
 
13889
			} else {
13890
 
13891
				// moveTo or lineTo
13892
				segmentPath.push(i ? L : M);
13893
 
13894
				// step line?
13895
				if (step && i) {
13896
					lastPoint = segment[i - 1];
13897
					if (step === 'right') {
13898
						segmentPath.push(
13899
							lastPoint.plotX,
13900
							plotY,
13901
							L
13902
						);
13903
 
13904
					} else if (step === 'center') {
13905
						segmentPath.push(
13906
							(lastPoint.plotX + plotX) / 2,
13907
							lastPoint.plotY,
13908
							L,
13909
							(lastPoint.plotX + plotX) / 2,
13910
							plotY,
13911
							L
13912
						);
13913
 
13914
					} else {
13915
						segmentPath.push(
13916
							plotX,
13917
							lastPoint.plotY,
13918
							L
13919
						);
13920
					}
13921
				}
13922
 
13923
				// normal line to next point
13924
				segmentPath.push(
13925
					point.plotX,
13926
					point.plotY
13927
				);
13928
			}
13929
		});
13930
 
13931
		return segmentPath;
13932
	},
13933
 
13934
	/**
13935
	 * Get the graph path
13936
	 */
13937
	getGraphPath: function () {
13938
		var series = this,
13939
			graphPath = [],
13940
			segmentPath,
13941
			singlePoints = []; // used in drawTracker
13942
 
13943
		// Divide into segments and build graph and area paths
13944
		each(series.segments, function (segment) {
13945
 
13946
			segmentPath = series.getSegmentPath(segment);
13947
 
13948
			// add the segment to the graph, or a single point for tracking
13949
			if (segment.length > 1) {
13950
				graphPath = graphPath.concat(segmentPath);
13951
			} else {
13952
				singlePoints.push(segment[0]);
13953
			}
13954
		});
13955
 
13956
		// Record it for use in drawGraph and drawTracker, and return graphPath
13957
		series.singlePoints = singlePoints;
13958
		series.graphPath = graphPath;
13959
 
13960
		return graphPath;
13961
 
13962
	},
13963
 
13964
	/**
13965
	 * Draw the actual graph
13966
	 */
13967
	drawGraph: function () {
13968
		var series = this,
13969
			options = this.options,
13970
			props = [['graph', options.lineColor || this.color, options.dashStyle]],
13971
			lineWidth = options.lineWidth,
13972
			roundCap = options.linecap !== 'square',
13973
			graphPath = this.getGraphPath(),
13974
			fillColor = (this.fillGraph && this.color) || NONE, // polygon series use filled graph
13975
			zones = this.zones;
13976
 
13977
		each(zones, function (threshold, i) {
13978
			props.push(['zoneGraph' + i, threshold.color || series.color, threshold.dashStyle || options.dashStyle]);
13979
		});
13980
 
13981
		// Draw the graph
13982
		each(props, function (prop, i) {
13983
			var graphKey = prop[0],
13984
				graph = series[graphKey],
13985
				attribs;
13986
 
13987
			if (graph) {
13988
				graph.animate({ d: graphPath });
13989
 
13990
			} else if ((lineWidth || fillColor) && graphPath.length) { // #1487
13991
				attribs = {
13992
					stroke: prop[1],
13993
					'stroke-width': lineWidth,
13994
					fill: fillColor,
13995
					zIndex: 1 // #1069
13996
				};
13997
				if (prop[2]) {
13998
					attribs.dashstyle = prop[2];
13999
				} else if (roundCap) {
14000
					attribs['stroke-linecap'] = attribs['stroke-linejoin'] = 'round';
14001
				}
14002
 
14003
				series[graphKey] = series.chart.renderer.path(graphPath)
14004
					.attr(attribs)
14005
					.add(series.group)
14006
					.shadow((i < 2) && options.shadow); // add shadow to normal series (0) or to first zone (1) #3932
14007
			}
14008
		});
14009
	},
14010
 
14011
	/**
14012
	 * Clip the graphs into the positive and negative coloured graphs
14013
	 */
14014
	applyZones: function () {
14015
		var series = this,
14016
			chart = this.chart,
14017
			renderer = chart.renderer,
14018
			zones = this.zones,
14019
			translatedFrom,
14020
			translatedTo,
14021
			clips = this.clips || [],
14022
			clipAttr,
14023
			graph = this.graph,
14024
			area = this.area,
14025
			chartSizeMax = mathMax(chart.chartWidth, chart.chartHeight),
14026
			axis = this[(this.zoneAxis || 'y') + 'Axis'],
14027
			extremes,
14028
			reversed = axis.reversed,
14029
			inverted = chart.inverted,
14030
			horiz = axis.horiz,
14031
			pxRange,
14032
			pxPosMin,
14033
			pxPosMax,
14034
			ignoreZones = false;
14035
 
14036
		if (zones.length && (graph || area) && axis.min !== UNDEFINED) {
14037
			// The use of the Color Threshold assumes there are no gaps
14038
			// so it is safe to hide the original graph and area
14039
			if (graph) {
14040
				graph.hide();
14041
			}
14042
			if (area) {
14043
				area.hide();
14044
			}
14045
 
14046
			// Create the clips
14047
			extremes = axis.getExtremes();
14048
			each(zones, function (threshold, i) {
14049
 
14050
				translatedFrom = reversed ?
14051
					(horiz ? chart.plotWidth : 0) :
14052
					(horiz ? 0 : axis.toPixels(extremes.min));
14053
				translatedFrom = mathMin(mathMax(pick(translatedTo, translatedFrom), 0), chartSizeMax);
14054
				translatedTo = mathMin(mathMax(mathRound(axis.toPixels(pick(threshold.value, extremes.max), true)), 0), chartSizeMax);
14055
 
14056
				if (ignoreZones) {
14057
					translatedFrom = translatedTo = axis.toPixels(extremes.max);
14058
				}
14059
 
14060
				pxRange = Math.abs(translatedFrom - translatedTo);
14061
				pxPosMin = mathMin(translatedFrom, translatedTo);
14062
				pxPosMax = mathMax(translatedFrom, translatedTo);
14063
				if (axis.isXAxis) {
14064
					clipAttr = {
14065
						x: inverted ? pxPosMax : pxPosMin,
14066
						y: 0,
14067
						width: pxRange,
14068
						height: chartSizeMax
14069
					};
14070
					if (!horiz) {
14071
						clipAttr.x = chart.plotHeight - clipAttr.x;
14072
					}
14073
				} else {
14074
					clipAttr = {
14075
						x: 0,
14076
						y: inverted ? pxPosMax : pxPosMin,
14077
						width: chartSizeMax,
14078
						height: pxRange
14079
					};
14080
					if (horiz) {
14081
						clipAttr.y = chart.plotWidth - clipAttr.y;
14082
					}
14083
				}
14084
 
14085
				/// VML SUPPPORT
14086
				if (chart.inverted && renderer.isVML) {
14087
					if (axis.isXAxis) {
14088
						clipAttr = {
14089
							x: 0,
14090
							y: reversed ? pxPosMin : pxPosMax,
14091
							height: clipAttr.width,
14092
							width: chart.chartWidth
14093
						};
14094
					} else {
14095
						clipAttr = {
14096
							x: clipAttr.y - chart.plotLeft - chart.spacingBox.x,
14097
							y: 0,
14098
							width: clipAttr.height,
14099
							height: chart.chartHeight
14100
						};
14101
					}
14102
				}
14103
				/// END OF VML SUPPORT
14104
 
14105
				if (clips[i]) {
14106
					clips[i].animate(clipAttr);
14107
				} else {
14108
					clips[i] = renderer.clipRect(clipAttr);
14109
 
14110
					if (graph) {
14111
						series['zoneGraph' + i].clip(clips[i]);
14112
					}
14113
 
14114
					if (area) {
14115
						series['zoneArea' + i].clip(clips[i]);
14116
					}
14117
				}
14118
				// if this zone extends out of the axis, ignore the others
14119
				ignoreZones = threshold.value > extremes.max;
14120
			});
14121
			this.clips = clips;
14122
		}
14123
	},
14124
 
14125
	/**
14126
	 * Initialize and perform group inversion on series.group and series.markerGroup
14127
	 */
14128
	invertGroups: function () {
14129
		var series = this,
14130
			chart = series.chart;
14131
 
14132
		// Pie, go away (#1736)
14133
		if (!series.xAxis) {
14134
			return;
14135
		}
14136
 
14137
		// A fixed size is needed for inversion to work
14138
		function setInvert() {
14139
			var size = {
14140
				width: series.yAxis.len,
14141
				height: series.xAxis.len
14142
			};
14143
 
14144
			each(['group', 'markerGroup'], function (groupName) {
14145
				if (series[groupName]) {
14146
					series[groupName].attr(size).invert();
14147
				}
14148
			});
14149
		}
14150
 
14151
		addEvent(chart, 'resize', setInvert); // do it on resize
14152
		addEvent(series, 'destroy', function () {
14153
			removeEvent(chart, 'resize', setInvert);
14154
		});
14155
 
14156
		// Do it now
14157
		setInvert(); // do it now
14158
 
14159
		// On subsequent render and redraw, just do setInvert without setting up events again
14160
		series.invertGroups = setInvert;
14161
	},
14162
 
14163
	/**
14164
	 * General abstraction for creating plot groups like series.group, series.dataLabelsGroup and
14165
	 * series.markerGroup. On subsequent calls, the group will only be adjusted to the updated plot size.
14166
	 */
14167
	plotGroup: function (prop, name, visibility, zIndex, parent) {
14168
		var group = this[prop],
14169
			isNew = !group;
14170
 
14171
		// Generate it on first call
14172
		if (isNew) {
14173
			this[prop] = group = this.chart.renderer.g(name)
14174
				.attr({
14175
					visibility: visibility,
14176
					zIndex: zIndex || 0.1 // IE8 needs this
14177
				})
14178
				.add(parent);
14179
 
14180
			group.addClass('highcharts-series-' + this.index);
14181
		}
14182
 
14183
		// Place it on first and subsequent (redraw) calls
14184
		group[isNew ? 'attr' : 'animate'](this.getPlotBox());
14185
		return group;
14186
	},
14187
 
14188
	/**
14189
	 * Get the translation and scale for the plot area of this series
14190
	 */
14191
	getPlotBox: function () {
14192
		var chart = this.chart,
14193
			xAxis = this.xAxis,
14194
			yAxis = this.yAxis;
14195
 
14196
		// Swap axes for inverted (#2339)
14197
		if (chart.inverted) {
14198
			xAxis = yAxis;
14199
			yAxis = this.xAxis;
14200
		}
14201
		return {
14202
			translateX: xAxis ? xAxis.left : chart.plotLeft,
14203
			translateY: yAxis ? yAxis.top : chart.plotTop,
14204
			scaleX: 1, // #1623
14205
			scaleY: 1
14206
		};
14207
	},
14208
 
14209
	/**
14210
	 * Render the graph and markers
14211
	 */
14212
	render: function () {
14213
		var series = this,
14214
			chart = series.chart,
14215
			group,
14216
			options = series.options,
14217
			animation = options.animation,
14218
			// Animation doesn't work in IE8 quirks when the group div is hidden,
14219
			// and looks bad in other oldIE
14220
			animDuration = (animation && !!series.animate && chart.renderer.isSVG && pick(animation.duration, 500)) || 0,
14221
			visibility = series.visible ? VISIBLE : HIDDEN,
14222
			zIndex = options.zIndex,
14223
			hasRendered = series.hasRendered,
14224
			chartSeriesGroup = chart.seriesGroup;
14225
 
14226
		// the group
14227
		group = series.plotGroup(
14228
			'group',
14229
			'series',
14230
			visibility,
14231
			zIndex,
14232
			chartSeriesGroup
14233
		);
14234
 
14235
		series.markerGroup = series.plotGroup(
14236
			'markerGroup',
14237
			'markers',
14238
			visibility,
14239
			zIndex,
14240
			chartSeriesGroup
14241
		);
14242
 
14243
		// initiate the animation
14244
		if (animDuration) {
14245
			series.animate(true);
14246
		}
14247
 
14248
		// cache attributes for shapes
14249
		series.getAttribs();
14250
 
14251
		// SVGRenderer needs to know this before drawing elements (#1089, #1795)
14252
		group.inverted = series.isCartesian ? chart.inverted : false;
14253
 
14254
		// draw the graph if any
14255
		if (series.drawGraph) {
14256
			series.drawGraph();
14257
			series.applyZones();
14258
		}
14259
 
14260
		each(series.points, function (point) {
14261
			if (point.redraw) {
14262
				point.redraw();
14263
			}
14264
		});
14265
 
14266
		// draw the data labels (inn pies they go before the points)
14267
		if (series.drawDataLabels) {
14268
			series.drawDataLabels();
14269
		}
14270
 
14271
		// draw the points
14272
		if (series.visible) {
14273
			series.drawPoints();
14274
		}
14275
 
14276
 
14277
		// draw the mouse tracking area
14278
		if (series.drawTracker && series.options.enableMouseTracking !== false) {
14279
			series.drawTracker();
14280
		}
14281
 
14282
		// Handle inverted series and tracker groups
14283
		if (chart.inverted) {
14284
			series.invertGroups();
14285
		}
14286
 
14287
		// Initial clipping, must be defined after inverting groups for VML. Applies to columns etc. (#3839).
14288
		if (options.clip !== false && !series.sharedClipKey && !hasRendered) {
14289
			group.clip(chart.clipRect);
14290
		}
14291
 
14292
		// Run the animation
14293
		if (animDuration) {
14294
			series.animate();
14295
		}
14296
 
14297
		// Call the afterAnimate function on animation complete (but don't overwrite the animation.complete option
14298
		// which should be available to the user).
14299
		if (!hasRendered) {
14300
			if (animDuration) {
14301
				series.animationTimeout = setTimeout(function () {
14302
					series.afterAnimate();
14303
				}, animDuration);
14304
			} else {
14305
				series.afterAnimate();
14306
			}
14307
		}
14308
 
14309
		series.isDirty = series.isDirtyData = false; // means data is in accordance with what you see
14310
		// (See #322) series.isDirty = series.isDirtyData = false; // means data is in accordance with what you see
14311
		series.hasRendered = true;
14312
	},
14313
 
14314
	/**
14315
	 * Redraw the series after an update in the axes.
14316
	 */
14317
	redraw: function () {
14318
		var series = this,
14319
			chart = series.chart,
14320
			wasDirtyData = series.isDirtyData, // cache it here as it is set to false in render, but used after
14321
			wasDirty = series.isDirty,
14322
			group = series.group,
14323
			xAxis = series.xAxis,
14324
			yAxis = series.yAxis;
14325
 
14326
		// reposition on resize
14327
		if (group) {
14328
			if (chart.inverted) {
14329
				group.attr({
14330
					width: chart.plotWidth,
14331
					height: chart.plotHeight
14332
				});
14333
			}
14334
 
14335
			group.animate({
14336
				translateX: pick(xAxis && xAxis.left, chart.plotLeft),
14337
				translateY: pick(yAxis && yAxis.top, chart.plotTop)
14338
			});
14339
		}
14340
 
14341
		series.translate();
14342
		series.render();
14343
		if (wasDirtyData) {
14344
			fireEvent(series, 'updatedData');
14345
		}
14346
		if (wasDirty || wasDirtyData) {			// #3945 recalculate the kdtree when dirty
14347
			delete this.kdTree; // #3868 recalculate the kdtree with dirty data
14348
		}
14349
	},
14350
 
14351
	/**
14352
	 * KD Tree && PointSearching Implementation
14353
	 */
14354
 
14355
	kdDimensions: 1,
14356
	kdAxisArray: ['clientX', 'plotY'],
14357
 
14358
	searchPoint: function (e, compareX) {
14359
		var series = this,
14360
			xAxis = series.xAxis,
14361
			yAxis = series.yAxis,
14362
			inverted = series.chart.inverted;
14363
 
14364
		return this.searchKDTree({
14365
			clientX: inverted ? xAxis.len - e.chartY + xAxis.pos : e.chartX - xAxis.pos,
14366
			plotY: inverted ? yAxis.len - e.chartX + yAxis.pos : e.chartY - yAxis.pos
14367
		}, compareX);
14368
	},
14369
 
14370
	buildKDTree: function () {
14371
		var series = this,
14372
			dimensions = series.kdDimensions;
14373
 
14374
		// Internal function
14375
		function _kdtree(points, depth, dimensions) {
14376
			var axis, median, length = points && points.length;
14377
 
14378
			if (length) {
14379
 
14380
				// alternate between the axis
14381
				axis = series.kdAxisArray[depth % dimensions];
14382
 
14383
				// sort point array
14384
				points.sort(function(a, b) {
14385
					return a[axis] - b[axis];
14386
				});
14387
 
14388
				median = Math.floor(length / 2);
14389
 
14390
				// build and return nod
14391
				return {
14392
					point: points[median],
14393
					left: _kdtree(points.slice(0, median), depth + 1, dimensions),
14394
					right: _kdtree(points.slice(median + 1), depth + 1, dimensions)
14395
				};
14396
 
14397
			}
14398
		}
14399
 
14400
		// Start the recursive build process with a clone of the points array and null points filtered out (#3873)
14401
		function startRecursive() {
14402
			var points = grep(series.points || [], function (point) { // #4390
14403
				return point.y !== null;
14404
			});
14405
 
14406
			series.kdTree = _kdtree(points, dimensions, dimensions);
14407
		}
14408
		delete series.kdTree;
14409
 
14410
		if (series.options.kdSync) {  // For testing tooltips, don't build async
14411
			startRecursive();
14412
		} else {
14413
			setTimeout(startRecursive);
14414
		}
14415
	},
14416
 
14417
	searchKDTree: function (point, compareX) {
14418
		var series = this,
14419
			kdX = this.kdAxisArray[0],
14420
			kdY = this.kdAxisArray[1],
14421
			kdComparer = compareX ? 'distX' : 'dist';
14422
 
14423
		// Set the one and two dimensional distance on the point object
14424
		function setDistance(p1, p2) {
14425
			var x = (defined(p1[kdX]) && defined(p2[kdX])) ? Math.pow(p1[kdX] - p2[kdX], 2) : null,
14426
				y = (defined(p1[kdY]) && defined(p2[kdY])) ? Math.pow(p1[kdY] - p2[kdY], 2) : null,
14427
				r = (x || 0) + (y || 0);
14428
 
14429
			p2.dist = defined(r) ? Math.sqrt(r) : Number.MAX_VALUE;
14430
			p2.distX = defined(x) ? Math.sqrt(x) : Number.MAX_VALUE;
14431
		}
14432
		function _search(search, tree, depth, dimensions) {
14433
			var point = tree.point,
14434
				axis = series.kdAxisArray[depth % dimensions],
14435
				tdist,
14436
				sideA,
14437
				sideB,
14438
				ret = point,
14439
				nPoint1,
14440
				nPoint2;
14441
 
14442
			setDistance(search, point);
14443
 
14444
			// Pick side based on distance to splitting point
14445
			tdist = search[axis] - point[axis];
14446
			sideA = tdist < 0 ? 'left' : 'right';
14447
			sideB = tdist < 0 ? 'right' : 'left';
14448
 
14449
			// End of tree
14450
			if (tree[sideA]) {
14451
				nPoint1 =_search(search, tree[sideA], depth + 1, dimensions);
14452
 
14453
				ret = (nPoint1[kdComparer] < ret[kdComparer] ? nPoint1 : point);
14454
			}
14455
			if (tree[sideB]) {
14456
				// compare distance to current best to splitting point to decide wether to check side B or not
14457
				if (Math.sqrt(tdist * tdist) < ret[kdComparer]) {
14458
					nPoint2 = _search(search, tree[sideB], depth + 1, dimensions);
14459
					ret = (nPoint2[kdComparer] < ret[kdComparer] ? nPoint2 : ret);
14460
				}
14461
			}
14462
 
14463
			return ret;
14464
		}
14465
 
14466
		if (!this.kdTree) {
14467
			this.buildKDTree();
14468
		}
14469
 
14470
		if (this.kdTree) {
14471
			return _search(point,
14472
				this.kdTree, this.kdDimensions, this.kdDimensions);
14473
		}
14474
	}
14475
 
14476
}; // end Series prototype
14477
 
14478
// Extend the Chart prototype for dynamic methods
14479
extend(Chart.prototype, {
14480
 
14481
	/**
14482
	 * Add a series dynamically after  time
14483
	 *
14484
	 * @param {Object} options The config options
14485
	 * @param {Boolean} redraw Whether to redraw the chart after adding. Defaults to true.
14486
	 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
14487
	 *    configuration
14488
	 *
14489
	 * @return {Object} series The newly created series object
14490
	 */
14491
	addSeries: function (options, redraw, animation) {
14492
		var series,
14493
			chart = this;
14494
 
14495
		if (options) {
14496
			redraw = pick(redraw, true); // defaults to true
14497
 
14498
			fireEvent(chart, 'addSeries', { options: options }, function () {
14499
				series = chart.initSeries(options);
14500
 
14501
				chart.isDirtyLegend = true; // the series array is out of sync with the display
14502
				chart.linkSeries();
14503
				if (redraw) {
14504
					chart.redraw(animation);
14505
				}
14506
			});
14507
		}
14508
 
14509
		return series;
14510
	},
14511
 
14512
	/**
14513
     * Add an axis to the chart
14514
     * @param {Object} options The axis option
14515
     * @param {Boolean} isX Whether it is an X axis or a value axis
14516
     */
14517
	addAxis: function (options, isX, redraw, animation) {
14518
		var key = isX ? 'xAxis' : 'yAxis',
14519
			chartOptions = this.options,
14520
			axis;
14521
 
14522
		/*jslint unused: false*/
14523
		axis = new Axis(this, merge(options, {
14524
			index: this[key].length,
14525
			isX: isX
14526
		}));
14527
		/*jslint unused: true*/
14528
 
14529
		// Push the new axis options to the chart options
14530
		chartOptions[key] = splat(chartOptions[key] || {});
14531
		chartOptions[key].push(options);
14532
 
14533
		if (pick(redraw, true)) {
14534
			this.redraw(animation);
14535
		}
14536
	},
14537
 
14538
	/**
14539
	 * Dim the chart and show a loading text or symbol
14540
	 * @param {String} str An optional text to show in the loading label instead of the default one
14541
	 */
14542
	showLoading: function (str) {
14543
		var chart = this,
14544
			options = chart.options,
14545
			loadingDiv = chart.loadingDiv,
14546
			loadingOptions = options.loading,
14547
			setLoadingSize = function () {
14548
				if (loadingDiv) {
14549
					css(loadingDiv, {
14550
						left: chart.plotLeft + PX,
14551
						top: chart.plotTop + PX,
14552
						width: chart.plotWidth + PX,
14553
						height: chart.plotHeight + PX
14554
					});
14555
				}
14556
			};
14557
 
14558
		// create the layer at the first call
14559
		if (!loadingDiv) {
14560
			chart.loadingDiv = loadingDiv = createElement(DIV, {
14561
				className: PREFIX + 'loading'
14562
			}, extend(loadingOptions.style, {
14563
				zIndex: 10,
14564
				display: NONE
14565
			}), chart.container);
14566
 
14567
			chart.loadingSpan = createElement(
14568
				'span',
14569
				null,
14570
				loadingOptions.labelStyle,
14571
				loadingDiv
14572
			);
14573
			addEvent(chart, 'redraw', setLoadingSize); // #1080
14574
		}
14575
 
14576
		// update text
14577
		chart.loadingSpan.innerHTML = str || options.lang.loading;
14578
 
14579
		// show it
14580
		if (!chart.loadingShown) {
14581
			css(loadingDiv, {
14582
				opacity: 0,
14583
				display: ''
14584
			});
14585
			animate(loadingDiv, {
14586
				opacity: loadingOptions.style.opacity
14587
			}, {
14588
				duration: loadingOptions.showDuration || 0
14589
			});
14590
			chart.loadingShown = true;
14591
		}
14592
		setLoadingSize();
14593
	},
14594
 
14595
	/**
14596
	 * Hide the loading layer
14597
	 */
14598
	hideLoading: function () {
14599
		var options = this.options,
14600
			loadingDiv = this.loadingDiv;
14601
 
14602
		if (loadingDiv) {
14603
			animate(loadingDiv, {
14604
				opacity: 0
14605
			}, {
14606
				duration: options.loading.hideDuration || 100,
14607
				complete: function () {
14608
					css(loadingDiv, { display: NONE });
14609
				}
14610
			});
14611
		}
14612
		this.loadingShown = false;
14613
	}
14614
});
14615
 
14616
// extend the Point prototype for dynamic methods
14617
extend(Point.prototype, {
14618
	/**
14619
	 * Update the point with new options (typically x/y data) and optionally redraw the series.
14620
	 *
14621
	 * @param {Object} options Point options as defined in the series.data array
14622
	 * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call
14623
	 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
14624
	 *    configuration
14625
	 *
14626
	 */
14627
	update: function (options, redraw, animation, runEvent) {
14628
		var point = this,
14629
			series = point.series,
14630
			graphic = point.graphic,
14631
			i,
14632
			chart = series.chart,
14633
			seriesOptions = series.options,
14634
			names = series.xAxis && series.xAxis.names;
14635
 
14636
		redraw = pick(redraw, true);
14637
 
14638
		function update() {
14639
 
14640
			point.applyOptions(options);
14641
 
14642
			// Update visuals
14643
			if (point.y === null && graphic) { // #4146
14644
				point.graphic = graphic.destroy();
14645
			}
14646
			if (isObject(options) && !isArray(options)) {
14647
				// Defer the actual redraw until getAttribs has been called (#3260)
14648
				point.redraw = function () {
14649
					if (graphic && graphic.element) {
14650
						if (options && options.marker && options.marker.symbol) {
14651
							point.graphic = graphic.destroy();
14652
						}
14653
					}
14654
					if (options && options.dataLabels && point.dataLabel) { // #2468
14655
						point.dataLabel = point.dataLabel.destroy();
14656
					}
14657
					point.redraw = null;
14658
				};
14659
			}
14660
 
14661
			// record changes in the parallel arrays
14662
			i = point.index;
14663
			series.updateParallelArrays(point, i);
14664
			if (names && point.name) {
14665
				names[point.x] = point.name;
14666
			}
14667
 
14668
			seriesOptions.data[i] = point.options;
14669
 
14670
			// redraw
14671
			series.isDirty = series.isDirtyData = true;
14672
			if (!series.fixedBox && series.hasCartesianSeries) { // #1906, #2320
14673
				chart.isDirtyBox = true;
14674
			}
14675
 
14676
			if (seriesOptions.legendType === 'point') { // #1831, #1885
14677
				chart.isDirtyLegend = true;
14678
			}
14679
			if (redraw) {
14680
				chart.redraw(animation);
14681
			}
14682
		}
14683
 
14684
		// Fire the event with a default handler of doing the update
14685
		if (runEvent === false) { // When called from setData
14686
			update();
14687
		} else {
14688
			point.firePointEvent('update', { options: options }, update);
14689
		}
14690
	},
14691
 
14692
	/**
14693
	 * Remove a point and optionally redraw the series and if necessary the axes
14694
	 * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call
14695
	 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
14696
	 *    configuration
14697
	 */
14698
	remove: function (redraw, animation) {
14699
		this.series.removePoint(inArray(this, this.series.data), redraw, animation);
14700
	}
14701
});
14702
 
14703
// Extend the series prototype for dynamic methods
14704
extend(Series.prototype, {
14705
	/**
14706
	 * Add a point dynamically after chart load time
14707
	 * @param {Object} options Point options as given in series.data
14708
	 * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call
14709
	 * @param {Boolean} shift If shift is true, a point is shifted off the start
14710
	 *    of the series as one is appended to the end.
14711
	 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
14712
	 *    configuration
14713
	 */
14714
	addPoint: function (options, redraw, shift, animation) {
14715
		var series = this,
14716
			seriesOptions = series.options,
14717
			data = series.data,
14718
			graph = series.graph,
14719
			area = series.area,
14720
			chart = series.chart,
14721
			names = series.xAxis && series.xAxis.names,
14722
			currentShift = (graph && graph.shift) || 0,
14723
			shiftShapes = ['graph', 'area'],
14724
			dataOptions = seriesOptions.data,
14725
			point,
14726
			isInTheMiddle,
14727
			xData = series.xData,
14728
			i,
14729
			x;
14730
 
14731
		setAnimation(animation, chart);
14732
 
14733
		// Make graph animate sideways
14734
		if (shift) {
14735
			i = series.zones.length;
14736
			while (i--) {
14737
				shiftShapes.push('zoneGraph' + i, 'zoneArea' + i);
14738
			}
14739
			each(shiftShapes, function (shape) {
14740
				if (series[shape]) {
14741
					series[shape].shift = currentShift + (seriesOptions.step ? 2 : 1);
14742
				}
14743
			});
14744
		}
14745
		if (area) {
14746
			area.isArea = true; // needed in animation, both with and without shift
14747
		}
14748
 
14749
		// Optional redraw, defaults to true
14750
		redraw = pick(redraw, true);
14751
 
14752
		// Get options and push the point to xData, yData and series.options. In series.generatePoints
14753
		// the Point instance will be created on demand and pushed to the series.data array.
14754
		point = { series: series };
14755
		series.pointClass.prototype.applyOptions.apply(point, [options]);
14756
		x = point.x;
14757
 
14758
		// Get the insertion point
14759
		i = xData.length;
14760
		if (series.requireSorting && x < xData[i - 1]) {
14761
			isInTheMiddle = true;
14762
			while (i && xData[i - 1] > x) {
14763
				i--;
14764
			}
14765
		}
14766
 
14767
		series.updateParallelArrays(point, 'splice', i, 0, 0); // insert undefined item
14768
		series.updateParallelArrays(point, i); // update it
14769
 
14770
		if (names && point.name) {
14771
			names[x] = point.name;
14772
		}
14773
		dataOptions.splice(i, 0, options);
14774
 
14775
		if (isInTheMiddle) {
14776
			series.data.splice(i, 0, null);
14777
			series.processData();
14778
		}
14779
 
14780
		// Generate points to be added to the legend (#1329)
14781
		if (seriesOptions.legendType === 'point') {
14782
			series.generatePoints();
14783
		}
14784
 
14785
		// Shift the first point off the parallel arrays
14786
		// todo: consider series.removePoint(i) method
14787
		if (shift) {
14788
			if (data[0] && data[0].remove) {
14789
				data[0].remove(false);
14790
			} else {
14791
				data.shift();
14792
				series.updateParallelArrays(point, 'shift');
14793
 
14794
				dataOptions.shift();
14795
			}
14796
		}
14797
 
14798
		// redraw
14799
		series.isDirty = true;
14800
		series.isDirtyData = true;
14801
		if (redraw) {
14802
			series.getAttribs(); // #1937
14803
			chart.redraw();
14804
		}
14805
	},
14806
 
14807
	/**
14808
	 * Remove a point (rendered or not), by index
14809
	 */
14810
	removePoint: function (i, redraw, animation) {
14811
 
14812
		var series = this,
14813
			data = series.data,
14814
			point = data[i],
14815
			points = series.points,
14816
			chart = series.chart,
14817
			remove = function () {
14818
 
14819
				if (data.length === points.length) {
14820
					points.splice(i, 1);
14821
				}
14822
				data.splice(i, 1);
14823
				series.options.data.splice(i, 1);
14824
				series.updateParallelArrays(point || { series: series }, 'splice', i, 1);
14825
 
14826
				if (point) {
14827
					point.destroy();
14828
				}
14829
 
14830
				// redraw
14831
				series.isDirty = true;
14832
				series.isDirtyData = true;
14833
				if (redraw) {
14834
					chart.redraw();
14835
				}
14836
			};
14837
 
14838
		setAnimation(animation, chart);
14839
		redraw = pick(redraw, true);
14840
 
14841
		// Fire the event with a default handler of removing the point
14842
		if (point) {
14843
			point.firePointEvent('remove', null, remove);
14844
		} else {
14845
			remove();
14846
		}
14847
	},
14848
 
14849
	/**
14850
	 * Remove a series and optionally redraw the chart
14851
	 *
14852
	 * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call
14853
	 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
14854
	 *    configuration
14855
	 */
14856
 
14857
	remove: function (redraw, animation) {
14858
		var series = this,
14859
			chart = series.chart;
14860
		redraw = pick(redraw, true);
14861
 
14862
		if (!series.isRemoving) {  /* prevent triggering native event in jQuery
14863
				(calling the remove function from the remove event) */
14864
			series.isRemoving = true;
14865
 
14866
			// fire the event with a default handler of removing the point
14867
			fireEvent(series, 'remove', null, function () {
14868
 
14869
 
14870
				// destroy elements
14871
				series.destroy();
14872
 
14873
 
14874
				// redraw
14875
				chart.isDirtyLegend = chart.isDirtyBox = true;
14876
				chart.linkSeries();
14877
 
14878
				if (redraw) {
14879
					chart.redraw(animation);
14880
				}
14881
			});
14882
 
14883
		}
14884
		series.isRemoving = false;
14885
	},
14886
 
14887
	/**
14888
	 * Update the series with a new set of options
14889
	 */
14890
	update: function (newOptions, redraw) {
14891
		var series = this,
14892
			chart = this.chart,
14893
			// must use user options when changing type because this.options is merged
14894
			// in with type specific plotOptions
14895
			oldOptions = this.userOptions,
14896
			oldType = this.type,
14897
			proto = seriesTypes[oldType].prototype,
14898
			preserve = ['group', 'markerGroup', 'dataLabelsGroup'],
14899
			n;
14900
 
14901
		// If we're changing type or zIndex, create new groups (#3380, #3404)
14902
		if ((newOptions.type && newOptions.type !== oldType) || newOptions.zIndex !== undefined) {
14903
			preserve.length = 0;
14904
		}
14905
 
14906
		// Make sure groups are not destroyed (#3094)
14907
		each(preserve, function (prop) {
14908
			preserve[prop] = series[prop];
14909
			delete series[prop];
14910
		});
14911
 
14912
		// Do the merge, with some forced options
14913
		newOptions = merge(oldOptions, {
14914
			animation: false,
14915
			index: this.index,
14916
			pointStart: this.xData[0] // when updating after addPoint
14917
		}, { data: this.options.data }, newOptions);
14918
 
14919
		// Destroy the series and delete all properties. Reinsert all methods
14920
		// and properties from the new type prototype (#2270, #3719)
14921
		this.remove(false);
14922
		for (n in proto) {
14923
			this[n] = UNDEFINED;
14924
		}
14925
		extend(this, seriesTypes[newOptions.type || oldType].prototype);
14926
 
14927
		// Re-register groups (#3094)
14928
		each(preserve, function (prop) {
14929
			series[prop] = preserve[prop];
14930
		});
14931
 
14932
		this.init(chart, newOptions);
14933
		chart.linkSeries(); // Links are lost in this.remove (#3028)
14934
		if (pick(redraw, true)) {
14935
			chart.redraw(false);
14936
		}
14937
	}
14938
});
14939
 
14940
// Extend the Axis.prototype for dynamic methods
14941
extend(Axis.prototype, {
14942
 
14943
	/**
14944
	 * Update the axis with a new options structure
14945
	 */
14946
	update: function (newOptions, redraw) {
14947
		var chart = this.chart;
14948
 
14949
		newOptions = chart.options[this.coll][this.options.index] = merge(this.userOptions, newOptions);
14950
 
14951
		this.destroy(true);
14952
		this._addedPlotLB = this.chart._labelPanes = UNDEFINED; // #1611, #2887, #4314
14953
 
14954
		this.init(chart, extend(newOptions, { events: UNDEFINED }));
14955
 
14956
		chart.isDirtyBox = true;
14957
		if (pick(redraw, true)) {
14958
			chart.redraw();
14959
		}
14960
	},
14961
 
14962
	/**
14963
     * Remove the axis from the chart
14964
     */
14965
	remove: function (redraw) {
14966
		var chart = this.chart,
14967
			key = this.coll, // xAxis or yAxis
14968
			axisSeries = this.series,
14969
			i = axisSeries.length;
14970
 
14971
		// Remove associated series (#2687)
14972
		while (i--) {
14973
			if (axisSeries[i]) {
14974
				axisSeries[i].remove(false);
14975
			}
14976
		}
14977
 
14978
		// Remove the axis
14979
		erase(chart.axes, this);
14980
		erase(chart[key], this);
14981
		chart.options[key].splice(this.options.index, 1);
14982
		each(chart[key], function (axis, i) { // Re-index, #1706
14983
			axis.options.index = i;
14984
		});
14985
		this.destroy();
14986
		chart.isDirtyBox = true;
14987
 
14988
		if (pick(redraw, true)) {
14989
			chart.redraw();
14990
		}
14991
	},
14992
 
14993
	/**
14994
	 * Update the axis title by options
14995
	 */
14996
	setTitle: function (newTitleOptions, redraw) {
14997
		this.update({ title: newTitleOptions }, redraw);
14998
	},
14999
 
15000
	/**
15001
	 * Set new axis categories and optionally redraw
15002
	 * @param {Array} categories
15003
	 * @param {Boolean} redraw
15004
	 */
15005
	setCategories: function (categories, redraw) {
15006
		this.update({ categories: categories }, redraw);
15007
	}
15008
 
15009
});
15010
 
15011
 
15012
/**
15013
 * LineSeries object
15014
 */
15015
var LineSeries = extendClass(Series);
15016
seriesTypes.line = LineSeries;
15017
 
15018
/**
15019
 * Set the default options for column
15020
 */
15021
defaultPlotOptions.column = merge(defaultSeriesOptions, {
15022
	borderColor: '#FFFFFF',
15023
	//borderWidth: 1,
15024
	borderRadius: 0,
15025
	//colorByPoint: undefined,
15026
	groupPadding: 0.2,
15027
	//grouping: true,
15028
	marker: null, // point options are specified in the base options
15029
	pointPadding: 0.1,
15030
	//pointWidth: null,
15031
	minPointLength: 0,
15032
	cropThreshold: 50, // when there are more points, they will not animate out of the chart on xAxis.setExtremes
15033
	pointRange: null, // null means auto, meaning 1 in a categorized axis and least distance between points if not categories
15034
	states: {
15035
		hover: {
15036
			brightness: 0.1,
15037
			shadow: false,
15038
			halo: false
15039
		},
15040
		select: {
15041
			color: '#C0C0C0',
15042
			borderColor: '#000000',
15043
			shadow: false
15044
		}
15045
	},
15046
	dataLabels: {
15047
		align: null, // auto
15048
		verticalAlign: null, // auto
15049
		y: null
15050
	},
15051
	softThreshold: false,
15052
	startFromThreshold: true, // docs (but false doesn't work well): http://jsfiddle.net/highcharts/hz8fopan/14/
15053
	stickyTracking: false,
15054
	tooltip: {
15055
		distance: 6
15056
	},
15057
	threshold: 0
15058
});
15059
 
15060
/**
15061
 * ColumnSeries object
15062
 */
15063
var ColumnSeries = extendClass(Series, {
15064
	type: 'column',
15065
	pointAttrToOptions: { // mapping between SVG attributes and the corresponding options
15066
		stroke: 'borderColor',
15067
		fill: 'color',
15068
		r: 'borderRadius'
15069
	},
15070
	cropShoulder: 0,
15071
	directTouch: true, // When tooltip is not shared, this series (and derivatives) requires direct touch/hover. KD-tree does not apply.
15072
	trackerGroups: ['group', 'dataLabelsGroup'],
15073
	negStacks: true, // use separate negative stacks, unlike area stacks where a negative
15074
		// point is substracted from previous (#1910)
15075
 
15076
	/**
15077
	 * Initialize the series
15078
	 */
15079
	init: function () {
15080
		Series.prototype.init.apply(this, arguments);
15081
 
15082
		var series = this,
15083
			chart = series.chart;
15084
 
15085
		// if the series is added dynamically, force redraw of other
15086
		// series affected by a new column
15087
		if (chart.hasRendered) {
15088
			each(chart.series, function (otherSeries) {
15089
				if (otherSeries.type === series.type) {
15090
					otherSeries.isDirty = true;
15091
				}
15092
			});
15093
		}
15094
	},
15095
 
15096
	/**
15097
	 * Return the width and x offset of the columns adjusted for grouping, groupPadding, pointPadding,
15098
	 * pointWidth etc.
15099
	 */
15100
	getColumnMetrics: function () {
15101
 
15102
		var series = this,
15103
			options = series.options,
15104
			xAxis = series.xAxis,
15105
			yAxis = series.yAxis,
15106
			reversedXAxis = xAxis.reversed,
15107
			stackKey,
15108
			stackGroups = {},
15109
			columnIndex,
15110
			columnCount = 0;
15111
 
15112
		// Get the total number of column type series.
15113
		// This is called on every series. Consider moving this logic to a
15114
		// chart.orderStacks() function and call it on init, addSeries and removeSeries
15115
		if (options.grouping === false) {
15116
			columnCount = 1;
15117
		} else {
15118
			each(series.chart.series, function (otherSeries) {
15119
				var otherOptions = otherSeries.options,
15120
					otherYAxis = otherSeries.yAxis;
15121
				if (otherSeries.type === series.type && otherSeries.visible &&
15122
						yAxis.len === otherYAxis.len && yAxis.pos === otherYAxis.pos) {  // #642, #2086
15123
					if (otherOptions.stacking) {
15124
						stackKey = otherSeries.stackKey;
15125
						if (stackGroups[stackKey] === UNDEFINED) {
15126
							stackGroups[stackKey] = columnCount++;
15127
						}
15128
						columnIndex = stackGroups[stackKey];
15129
					} else if (otherOptions.grouping !== false) { // #1162
15130
						columnIndex = columnCount++;
15131
					}
15132
					otherSeries.columnIndex = columnIndex;
15133
				}
15134
			});
15135
		}
15136
 
15137
		var categoryWidth = mathMin(
15138
				mathAbs(xAxis.transA) * (xAxis.ordinalSlope || options.pointRange || xAxis.closestPointRange || xAxis.tickInterval || 1), // #2610
15139
				xAxis.len // #1535
15140
			),
15141
			groupPadding = categoryWidth * options.groupPadding,
15142
			groupWidth = categoryWidth - 2 * groupPadding,
15143
			pointOffsetWidth = groupWidth / columnCount,
15144
			pointWidth = mathMin(
15145
				options.maxPointWidth || xAxis.len,
15146
				pick(options.pointWidth, pointOffsetWidth * (1 - 2 * options.pointPadding))
15147
			),
15148
			pointPadding = (pointOffsetWidth - pointWidth) / 2,
15149
			colIndex = (reversedXAxis ?
15150
				columnCount - (series.columnIndex || 0) : // #1251
15151
				series.columnIndex) || 0,
15152
			pointXOffset = pointPadding + (groupPadding + colIndex *
15153
				pointOffsetWidth - (categoryWidth / 2)) *
15154
				(reversedXAxis ? -1 : 1);
15155
 
15156
		// Save it for reading in linked series (Error bars particularly)
15157
		return (series.columnMetrics = {
15158
			width: pointWidth,
15159
			offset: pointXOffset
15160
		});
15161
 
15162
	},
15163
 
15164
	/**
15165
	 * Make the columns crisp. The edges are rounded to the nearest full pixel.
15166
	 */
15167
	crispCol: function (x, y, w, h) {
15168
		var chart = this.chart,
15169
			borderWidth = this.borderWidth,
15170
			xCrisp = -(borderWidth % 2 ? 0.5 : 0),
15171
			yCrisp = borderWidth % 2 ? 0.5 : 1,
15172
			right,
15173
			bottom,
15174
			fromTop;
15175
 
15176
		if (chart.inverted && chart.renderer.isVML) {
15177
			yCrisp += 1;
15178
		}
15179
 
15180
		// Horizontal. We need to first compute the exact right edge, then round it
15181
		// and compute the width from there.
15182
		right = Math.round(x + w) + xCrisp;
15183
		x = Math.round(x) + xCrisp;
15184
		w = right - x;
15185
 
15186
		// Vertical
15187
		fromTop = mathAbs(y) <= 0.5; // #4504
15188
		bottom = Math.round(y + h) + yCrisp;
15189
		y = Math.round(y) + yCrisp;
15190
		h = bottom - y;
15191
 
15192
		// Top edges are exceptions
15193
		if (fromTop) {
15194
			y -= 1;
15195
			h += 1;
15196
		}
15197
 
15198
		return {
15199
			x: x,
15200
			y: y,
15201
			width: w,
15202
			height: h
15203
		};
15204
	},
15205
 
15206
	/**
15207
	 * Translate each point to the plot area coordinate system and find shape positions
15208
	 */
15209
	translate: function () {
15210
		var series = this,
15211
			chart = series.chart,
15212
			options = series.options,
15213
			borderWidth = series.borderWidth = pick(
15214
				options.borderWidth,
15215
				series.closestPointRange * series.xAxis.transA < 2 ? 0 : 1 // #3635
15216
			),
15217
			yAxis = series.yAxis,
15218
			threshold = options.threshold,
15219
			translatedThreshold = series.translatedThreshold = yAxis.getThreshold(threshold),
15220
			minPointLength = pick(options.minPointLength, 5),
15221
			metrics = series.getColumnMetrics(),
15222
			pointWidth = metrics.width,
15223
			seriesBarW = series.barW = mathMax(pointWidth, 1 + 2 * borderWidth), // postprocessed for border width
15224
			pointXOffset = series.pointXOffset = metrics.offset;
15225
 
15226
		if (chart.inverted) {
15227
			translatedThreshold -= 0.5; // #3355
15228
		}
15229
 
15230
		// When the pointPadding is 0, we want the columns to be packed tightly, so we allow individual
15231
		// columns to have individual sizes. When pointPadding is greater, we strive for equal-width
15232
		// columns (#2694).
15233
		if (options.pointPadding) {
15234
			seriesBarW = mathCeil(seriesBarW);
15235
		}
15236
 
15237
		Series.prototype.translate.apply(series);
15238
 
15239
		// Record the new values
15240
		each(series.points, function (point) {
15241
			var yBottom = mathMin(pick(point.yBottom, translatedThreshold), 9e4), // #3575
15242
				safeDistance = 999 + mathAbs(yBottom),
15243
				plotY = mathMin(mathMax(-safeDistance, point.plotY), yAxis.len + safeDistance), // Don't draw too far outside plot area (#1303, #2241, #4264)
15244
				barX = point.plotX + pointXOffset,
15245
				barW = seriesBarW,
15246
				barY = mathMin(plotY, yBottom),
15247
				up,
15248
				barH = mathMax(plotY, yBottom) - barY;
15249
 
15250
			// Handle options.minPointLength
15251
			if (mathAbs(barH) < minPointLength) {
15252
				if (minPointLength) {
15253
					barH = minPointLength;
15254
					up = (!yAxis.reversed && !point.negative) || (yAxis.reversed && point.negative);
15255
					barY = mathAbs(barY - translatedThreshold) > minPointLength ? // stacked
15256
							yBottom - minPointLength : // keep position
15257
							translatedThreshold - (up ? minPointLength : 0); // #1485, #4051
15258
				}
15259
			}
15260
 
15261
			// Cache for access in polar
15262
			point.barX = barX;
15263
			point.pointWidth = pointWidth;
15264
 
15265
			// Fix the tooltip on center of grouped columns (#1216, #424, #3648)
15266
			point.tooltipPos = chart.inverted ?
15267
				[yAxis.len + yAxis.pos - chart.plotLeft - plotY, series.xAxis.len - barX - barW / 2, barH] :
15268
				[barX + barW / 2, plotY + yAxis.pos - chart.plotTop, barH];
15269
 
15270
			// Register shape type and arguments to be used in drawPoints
15271
			point.shapeType = 'rect';
15272
			point.shapeArgs = series.crispCol(barX, barY, barW, barH);
15273
		});
15274
 
15275
	},
15276
 
15277
	getSymbol: noop,
15278
 
15279
	/**
15280
	 * Use a solid rectangle like the area series types
15281
	 */
15282
	drawLegendSymbol: LegendSymbolMixin.drawRectangle,
15283
 
15284
 
15285
	/**
15286
	 * Columns have no graph
15287
	 */
15288
	drawGraph: noop,
15289
 
15290
	/**
15291
	 * Draw the columns. For bars, the series.group is rotated, so the same coordinates
15292
	 * apply for columns and bars. This method is inherited by scatter series.
15293
	 *
15294
	 */
15295
	drawPoints: function () {
15296
		var series = this,
15297
			chart = this.chart,
15298
			options = series.options,
15299
			renderer = chart.renderer,
15300
			animationLimit = options.animationLimit || 250,
15301
			shapeArgs,
15302
			pointAttr;
15303
 
15304
		// draw the columns
15305
		each(series.points, function (point) {
15306
			var plotY = point.plotY,
15307
				graphic = point.graphic,
15308
				borderAttr;
15309
 
15310
			if (plotY !== UNDEFINED && !isNaN(plotY) && point.y !== null) {
15311
				shapeArgs = point.shapeArgs;
15312
 
15313
				borderAttr = defined(series.borderWidth) ? {
15314
					'stroke-width': series.borderWidth
15315
				} : {};
15316
 
15317
				pointAttr = point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE] || series.pointAttr[NORMAL_STATE];
15318
 
15319
				if (graphic) { // update
15320
					stop(graphic);
15321
					graphic.attr(borderAttr)[chart.pointCount < animationLimit ? 'animate' : 'attr'](merge(shapeArgs));
15322
 
15323
				} else {
15324
					point.graphic = graphic = renderer[point.shapeType](shapeArgs)
15325
						.attr(borderAttr)
15326
						.attr(pointAttr)
15327
						.add(point.group || series.group)
15328
						.shadow(options.shadow, null, options.stacking && !options.borderRadius);
15329
				}
15330
 
15331
			} else if (graphic) {
15332
				point.graphic = graphic.destroy(); // #1269
15333
			}
15334
		});
15335
	},
15336
 
15337
	/**
15338
	 * Animate the column heights one by one from zero
15339
	 * @param {Boolean} init Whether to initialize the animation or run it
15340
	 */
15341
	animate: function (init) {
15342
		var series = this,
15343
			yAxis = this.yAxis,
15344
			options = series.options,
15345
			inverted = this.chart.inverted,
15346
			attr = {},
15347
			translatedThreshold;
15348
 
15349
		if (hasSVG) { // VML is too slow anyway
15350
			if (init) {
15351
				attr.scaleY = 0.001;
15352
				translatedThreshold = mathMin(yAxis.pos + yAxis.len, mathMax(yAxis.pos, yAxis.toPixels(options.threshold)));
15353
				if (inverted) {
15354
					attr.translateX = translatedThreshold - yAxis.len;
15355
				} else {
15356
					attr.translateY = translatedThreshold;
15357
				}
15358
				series.group.attr(attr);
15359
 
15360
			} else { // run the animation
15361
 
15362
				attr.scaleY = 1;
15363
				attr[inverted ? 'translateX' : 'translateY'] = yAxis.pos;
15364
				series.group.animate(attr, series.options.animation);
15365
 
15366
				// delete this function to allow it only once
15367
				series.animate = null;
15368
			}
15369
		}
15370
	},
15371
 
15372
	/**
15373
	 * Remove this series from the chart
15374
	 */
15375
	remove: function () {
15376
		var series = this,
15377
			chart = series.chart;
15378
 
15379
		// column and bar series affects other series of the same type
15380
		// as they are either stacked or grouped
15381
		if (chart.hasRendered) {
15382
			each(chart.series, function (otherSeries) {
15383
				if (otherSeries.type === series.type) {
15384
					otherSeries.isDirty = true;
15385
				}
15386
			});
15387
		}
15388
 
15389
		Series.prototype.remove.apply(series, arguments);
15390
	}
15391
});
15392
seriesTypes.column = ColumnSeries;
15393
/**
15394
 * Set the default options for scatter
15395
 */
15396
defaultPlotOptions.scatter = merge(defaultSeriesOptions, {
15397
	lineWidth: 0,
15398
	marker: {
15399
		enabled: true // Overrides auto-enabling in line series (#3647)
15400
	},
15401
	tooltip: {
15402
		headerFormat: '<span style="color:{point.color}">\u25CF</span> <span style="font-size: 10px;"> {series.name}</span><br/>',
15403
		pointFormat: 'x: <b>{point.x}</b><br/>y: <b>{point.y}</b><br/>'
15404
	}
15405
});
15406
 
15407
/**
15408
 * The scatter series class
15409
 */
15410
var ScatterSeries = extendClass(Series, {
15411
	type: 'scatter',
15412
	sorted: false,
15413
	requireSorting: false,
15414
	noSharedTooltip: true,
15415
	trackerGroups: ['group', 'markerGroup', 'dataLabelsGroup'],
15416
	takeOrdinalPosition: false, // #2342
15417
	kdDimensions: 2,
15418
	drawGraph: function () {
15419
		if (this.options.lineWidth) {
15420
			Series.prototype.drawGraph.call(this);
15421
		}
15422
	}
15423
});
15424
 
15425
seriesTypes.scatter = ScatterSeries;
15426
 
15427
/**
15428
 * Draw the data labels
15429
 */
15430
Series.prototype.drawDataLabels = function () {
15431
 
15432
	var series = this,
15433
		seriesOptions = series.options,
15434
		cursor = seriesOptions.cursor,
15435
		options = seriesOptions.dataLabels,
15436
		points = series.points,
15437
		pointOptions,
15438
		generalOptions,
15439
		hasRendered = series.hasRendered || 0,
15440
		str,
15441
		dataLabelsGroup,
15442
		renderer = series.chart.renderer;
15443
 
15444
	if (options.enabled || series._hasPointLabels) {
15445
 
15446
		// Process default alignment of data labels for columns
15447
		if (series.dlProcessOptions) {
15448
			series.dlProcessOptions(options);
15449
		}
15450
 
15451
		// Create a separate group for the data labels to avoid rotation
15452
		dataLabelsGroup = series.plotGroup(
15453
			'dataLabelsGroup',
15454
			'data-labels',
15455
			options.defer ? HIDDEN : VISIBLE,
15456
			options.zIndex || 6
15457
		);
15458
 
15459
		if (pick(options.defer, true)) {
15460
			dataLabelsGroup.attr({ opacity: +hasRendered }); // #3300
15461
			if (!hasRendered) {
15462
				addEvent(series, 'afterAnimate', function () {
15463
					if (series.visible) { // #3023, #3024
15464
						dataLabelsGroup.show();
15465
					}
15466
					dataLabelsGroup[seriesOptions.animation ? 'animate' : 'attr']({ opacity: 1 }, { duration: 200 });
15467
				});
15468
			}
15469
		}
15470
 
15471
		// Make the labels for each point
15472
		generalOptions = options;
15473
		each(points, function (point) {
15474
 
15475
			var enabled,
15476
				dataLabel = point.dataLabel,
15477
				labelConfig,
15478
				attr,
15479
				name,
15480
				rotation,
15481
				connector = point.connector,
15482
				isNew = true,
15483
				style,
15484
				moreStyle = {};
15485
 
15486
			// Determine if each data label is enabled
15487
			pointOptions = point.dlOptions || (point.options && point.options.dataLabels); // dlOptions is used in treemaps
15488
			enabled = pick(pointOptions && pointOptions.enabled, generalOptions.enabled); // #2282
15489
 
15490
 
15491
			// If the point is outside the plot area, destroy it. #678, #820
15492
			if (dataLabel && !enabled) {
15493
				point.dataLabel = dataLabel.destroy();
15494
 
15495
			// Individual labels are disabled if the are explicitly disabled
15496
			// in the point options, or if they fall outside the plot area.
15497
			} else if (enabled) {
15498
 
15499
				// Create individual options structure that can be extended without
15500
				// affecting others
15501
				options = merge(generalOptions, pointOptions);
15502
				style = options.style;
15503
 
15504
				rotation = options.rotation;
15505
 
15506
				// Get the string
15507
				labelConfig = point.getLabelConfig();
15508
				str = options.format ?
15509
					format(options.format, labelConfig) :
15510
					options.formatter.call(labelConfig, options);
15511
 
15512
				// Determine the color
15513
				style.color = pick(options.color, style.color, series.color, 'black');
15514
 
15515
 
15516
				// update existing label
15517
				if (dataLabel) {
15518
 
15519
					if (defined(str)) {
15520
						dataLabel
15521
							.attr({
15522
								text: str
15523
							});
15524
						isNew = false;
15525
 
15526
					} else { // #1437 - the label is shown conditionally
15527
						point.dataLabel = dataLabel = dataLabel.destroy();
15528
						if (connector) {
15529
							point.connector = connector.destroy();
15530
						}
15531
					}
15532
 
15533
				// create new label
15534
				} else if (defined(str)) {
15535
					attr = {
15536
						//align: align,
15537
						fill: options.backgroundColor,
15538
						stroke: options.borderColor,
15539
						'stroke-width': options.borderWidth,
15540
						r: options.borderRadius || 0,
15541
						rotation: rotation,
15542
						padding: options.padding,
15543
						zIndex: 1
15544
					};
15545
 
15546
					// Get automated contrast color
15547
					if (style.color === 'contrast') {
15548
						moreStyle.color = options.inside || options.distance < 0 || !!seriesOptions.stacking ?
15549
							renderer.getContrast(point.color || series.color) :
15550
							'#000000';
15551
					}
15552
					if (cursor) {
15553
						moreStyle.cursor = cursor;
15554
					}
15555
 
15556
 
15557
					// Remove unused attributes (#947)
15558
					for (name in attr) {
15559
						if (attr[name] === UNDEFINED) {
15560
							delete attr[name];
15561
						}
15562
					}
15563
 
15564
					dataLabel = point.dataLabel = renderer[rotation ? 'text' : 'label']( // labels don't support rotation
15565
						str,
15566
						0,
15567
						-999,
15568
						options.shape,
15569
						null,
15570
						null,
15571
						options.useHTML
15572
					)
15573
					.attr(attr)
15574
					.css(extend(style, moreStyle))
15575
					.add(dataLabelsGroup)
15576
					.shadow(options.shadow);
15577
 
15578
				}
15579
 
15580
				if (dataLabel) {
15581
					// Now the data label is created and placed at 0,0, so we need to align it
15582
					series.alignDataLabel(point, dataLabel, options, null, isNew);
15583
				}
15584
			}
15585
		});
15586
	}
15587
};
15588
 
15589
/**
15590
 * Align each individual data label
15591
 */
15592
Series.prototype.alignDataLabel = function (point, dataLabel, options, alignTo, isNew) {
15593
	var chart = this.chart,
15594
		inverted = chart.inverted,
15595
		plotX = pick(point.plotX, -999),
15596
		plotY = pick(point.plotY, -999),
15597
		bBox = dataLabel.getBBox(),
15598
		baseline = chart.renderer.fontMetrics(options.style.fontSize).b,
15599
		rotCorr, // rotation correction
15600
		// Math.round for rounding errors (#2683), alignTo to allow column labels (#2700)
15601
		visible = this.visible && (point.series.forceDL || chart.isInsidePlot(plotX, mathRound(plotY), inverted) ||
15602
			(alignTo && chart.isInsidePlot(plotX, inverted ? alignTo.x + 1 : alignTo.y + alignTo.height - 1, inverted))),
15603
		alignAttr; // the final position;
15604
 
15605
	if (visible) {
15606
 
15607
		// The alignment box is a singular point
15608
		alignTo = extend({
15609
			x: inverted ? chart.plotWidth - plotY : plotX,
15610
			y: mathRound(inverted ? chart.plotHeight - plotX : plotY),
15611
			width: 0,
15612
			height: 0
15613
		}, alignTo);
15614
 
15615
		// Add the text size for alignment calculation
15616
		extend(options, {
15617
			width: bBox.width,
15618
			height: bBox.height
15619
		});
15620
 
15621
		// Allow a hook for changing alignment in the last moment, then do the alignment
15622
		if (options.rotation) { // Fancy box alignment isn't supported for rotated text
15623
			rotCorr = chart.renderer.rotCorr(baseline, options.rotation); // #3723
15624
			dataLabel[isNew ? 'attr' : 'animate']({
15625
					x: alignTo.x + options.x + alignTo.width / 2 + rotCorr.x,
15626
					y: alignTo.y + options.y + alignTo.height / 2
15627
				})
15628
				.attr({ // #3003
15629
					align: options.align
15630
				});
15631
		} else {
15632
			dataLabel.align(options, null, alignTo);
15633
			alignAttr = dataLabel.alignAttr;
15634
 
15635
			// Handle justify or crop
15636
			if (pick(options.overflow, 'justify') === 'justify') {
15637
				this.justifyDataLabel(dataLabel, options, alignAttr, bBox, alignTo, isNew);
15638
 
15639
			} else if (pick(options.crop, true)) {
15640
				// Now check that the data label is within the plot area
15641
				visible = chart.isInsidePlot(alignAttr.x, alignAttr.y) && chart.isInsidePlot(alignAttr.x + bBox.width, alignAttr.y + bBox.height);
15642
 
15643
			}
15644
 
15645
			// When we're using a shape, make it possible with a connector or an arrow pointing to thie point
15646
			if (options.shape) {
15647
				dataLabel.attr({
15648
					anchorX: point.plotX,
15649
					anchorY: point.plotY
15650
				});
15651
			}
15652
 
15653
		}
15654
	}
15655
 
15656
	// Show or hide based on the final aligned position
15657
	if (!visible) {
15658
		stop(dataLabel);
15659
		dataLabel.attr({ y: -999 });
15660
		dataLabel.placed = false; // don't animate back in
15661
	}
15662
 
15663
};
15664
 
15665
/**
15666
 * If data labels fall partly outside the plot area, align them back in, in a way that
15667
 * doesn't hide the point.
15668
 */
15669
Series.prototype.justifyDataLabel = function (dataLabel, options, alignAttr, bBox, alignTo, isNew) {
15670
	var chart = this.chart,
15671
		align = options.align,
15672
		verticalAlign = options.verticalAlign,
15673
		off,
15674
		justified,
15675
		padding = dataLabel.box ? 0 : (dataLabel.padding || 0);
15676
 
15677
	// Off left
15678
	off = alignAttr.x + padding;
15679
	if (off < 0) {
15680
		if (align === 'right') {
15681
			options.align = 'left';
15682
		} else {
15683
			options.x = -off;
15684
		}
15685
		justified = true;
15686
	}
15687
 
15688
	// Off right
15689
	off = alignAttr.x + bBox.width - padding;
15690
	if (off > chart.plotWidth) {
15691
		if (align === 'left') {
15692
			options.align = 'right';
15693
		} else {
15694
			options.x = chart.plotWidth - off;
15695
		}
15696
		justified = true;
15697
	}
15698
 
15699
	// Off top
15700
	off = alignAttr.y + padding;
15701
	if (off < 0) {
15702
		if (verticalAlign === 'bottom') {
15703
			options.verticalAlign = 'top';
15704
		} else {
15705
			options.y = -off;
15706
		}
15707
		justified = true;
15708
	}
15709
 
15710
	// Off bottom
15711
	off = alignAttr.y + bBox.height - padding;
15712
	if (off > chart.plotHeight) {
15713
		if (verticalAlign === 'top') {
15714
			options.verticalAlign = 'bottom';
15715
		} else {
15716
			options.y = chart.plotHeight - off;
15717
		}
15718
		justified = true;
15719
	}
15720
 
15721
	if (justified) {
15722
		dataLabel.placed = !isNew;
15723
		dataLabel.align(options, null, alignTo);
15724
	}
15725
};
15726
 
15727
/**
15728
 * Override the base drawDataLabels method by pie specific functionality
15729
 */
15730
if (seriesTypes.pie) {
15731
	seriesTypes.pie.prototype.drawDataLabels = function () {
15732
		var series = this,
15733
			data = series.data,
15734
			point,
15735
			chart = series.chart,
15736
			options = series.options.dataLabels,
15737
			connectorPadding = pick(options.connectorPadding, 10),
15738
			connectorWidth = pick(options.connectorWidth, 1),
15739
			plotWidth = chart.plotWidth,
15740
			plotHeight = chart.plotHeight,
15741
			connector,
15742
			connectorPath,
15743
			softConnector = pick(options.softConnector, true),
15744
			distanceOption = options.distance,
15745
			seriesCenter = series.center,
15746
			radius = seriesCenter[2] / 2,
15747
			centerY = seriesCenter[1],
15748
			outside = distanceOption > 0,
15749
			dataLabel,
15750
			dataLabelWidth,
15751
			labelPos,
15752
			labelHeight,
15753
			halves = [// divide the points into right and left halves for anti collision
15754
				[], // right
15755
				[]  // left
15756
			],
15757
			x,
15758
			y,
15759
			visibility,
15760
			rankArr,
15761
			i,
15762
			j,
15763
			overflow = [0, 0, 0, 0], // top, right, bottom, left
15764
			sort = function (a, b) {
15765
				return b.y - a.y;
15766
			};
15767
 
15768
		// get out if not enabled
15769
		if (!series.visible || (!options.enabled && !series._hasPointLabels)) {
15770
			return;
15771
		}
15772
 
15773
		// run parent method
15774
		Series.prototype.drawDataLabels.apply(series);
15775
 
15776
		// arrange points for detection collision
15777
		each(data, function (point) {
15778
			if (point.dataLabel && point.visible) { // #407, #2510
15779
				halves[point.half].push(point);
15780
			}
15781
		});
15782
 
15783
		/* Loop over the points in each half, starting from the top and bottom
15784
		 * of the pie to detect overlapping labels.
15785
		 */
15786
		i = 2;
15787
		while (i--) {
15788
 
15789
			var slots = [],
15790
				slotsLength,
15791
				usedSlots = [],
15792
				points = halves[i],
15793
				pos,
15794
				bottom,
15795
				length = points.length,
15796
				slotIndex;
15797
 
15798
			if (!length) {
15799
				continue;
15800
			}
15801
 
15802
			// Sort by angle
15803
			series.sortByAngle(points, i - 0.5);
15804
 
15805
			// Assume equal label heights on either hemisphere (#2630)
15806
			j = labelHeight = 0;
15807
			while (!labelHeight && points[j]) { // #1569
15808
				labelHeight = points[j] && points[j].dataLabel && (points[j].dataLabel.getBBox().height || 21); // 21 is for #968
15809
				j++;
15810
			}
15811
 
15812
			// Only do anti-collision when we are outside the pie and have connectors (#856)
15813
			if (distanceOption > 0) {
15814
 
15815
				// Build the slots
15816
				bottom = mathMin(centerY + radius + distanceOption, chart.plotHeight);
15817
				for (pos = mathMax(0, centerY - radius - distanceOption); pos <= bottom; pos += labelHeight) {
15818
					slots.push(pos);
15819
				}
15820
				slotsLength = slots.length;
15821
 
15822
 
15823
				/* Visualize the slots
15824
				if (!series.slotElements) {
15825
					series.slotElements = [];
15826
				}
15827
				if (i === 1) {
15828
					series.slotElements.forEach(function (elem) {
15829
						elem.destroy();
15830
					});
15831
					series.slotElements.length = 0;
15832
				}
15833
 
15834
				slots.forEach(function (pos, no) {
15835
					var slotX = series.getX(pos, i) + chart.plotLeft - (i ? 100 : 0),
15836
						slotY = pos + chart.plotTop;
15837
 
15838
					if (!isNaN(slotX)) {
15839
						series.slotElements.push(chart.renderer.rect(slotX, slotY - 7, 100, labelHeight, 1)
15840
							.attr({
15841
								'stroke-width': 1,
15842
								stroke: 'silver',
15843
								fill: 'rgba(0,0,255,0.1)'
15844
							})
15845
							.add());
15846
						series.slotElements.push(chart.renderer.text('Slot '+ no, slotX, slotY + 4)
15847
							.attr({
15848
								fill: 'silver'
15849
							}).add());
15850
					}
15851
				});
15852
				// */
15853
 
15854
				// if there are more values than available slots, remove lowest values
15855
				if (length > slotsLength) {
15856
					// create an array for sorting and ranking the points within each quarter
15857
					rankArr = [].concat(points);
15858
					rankArr.sort(sort);
15859
					j = length;
15860
					while (j--) {
15861
						rankArr[j].rank = j;
15862
					}
15863
					j = length;
15864
					while (j--) {
15865
						if (points[j].rank >= slotsLength) {
15866
							points.splice(j, 1);
15867
						}
15868
					}
15869
					length = points.length;
15870
				}
15871
 
15872
				// The label goes to the nearest open slot, but not closer to the edge than
15873
				// the label's index.
15874
				for (j = 0; j < length; j++) {
15875
 
15876
					point = points[j];
15877
					labelPos = point.labelPos;
15878
 
15879
					var closest = 9999,
15880
						distance,
15881
						slotI;
15882
 
15883
					// find the closest slot index
15884
					for (slotI = 0; slotI < slotsLength; slotI++) {
15885
						distance = mathAbs(slots[slotI] - labelPos[1]);
15886
						if (distance < closest) {
15887
							closest = distance;
15888
							slotIndex = slotI;
15889
						}
15890
					}
15891
 
15892
					// if that slot index is closer to the edges of the slots, move it
15893
					// to the closest appropriate slot
15894
					if (slotIndex < j && slots[j] !== null) { // cluster at the top
15895
						slotIndex = j;
15896
					} else if (slotsLength  < length - j + slotIndex && slots[j] !== null) { // cluster at the bottom
15897
						slotIndex = slotsLength - length + j;
15898
						while (slots[slotIndex] === null) { // make sure it is not taken
15899
							slotIndex++;
15900
						}
15901
					} else {
15902
						// Slot is taken, find next free slot below. In the next run, the next slice will find the
15903
						// slot above these, because it is the closest one
15904
						while (slots[slotIndex] === null) { // make sure it is not taken
15905
							slotIndex++;
15906
						}
15907
					}
15908
 
15909
					usedSlots.push({ i: slotIndex, y: slots[slotIndex] });
15910
					slots[slotIndex] = null; // mark as taken
15911
				}
15912
				// sort them in order to fill in from the top
15913
				usedSlots.sort(sort);
15914
			}
15915
 
15916
			// now the used slots are sorted, fill them up sequentially
15917
			for (j = 0; j < length; j++) {
15918
 
15919
				var slot, naturalY;
15920
 
15921
				point = points[j];
15922
				labelPos = point.labelPos;
15923
				dataLabel = point.dataLabel;
15924
				visibility = point.visible === false ? HIDDEN : 'inherit';
15925
				naturalY = labelPos[1];
15926
 
15927
				if (distanceOption > 0) {
15928
					slot = usedSlots.pop();
15929
					slotIndex = slot.i;
15930
 
15931
					// if the slot next to currrent slot is free, the y value is allowed
15932
					// to fall back to the natural position
15933
					y = slot.y;
15934
					if ((naturalY > y && slots[slotIndex + 1] !== null) ||
15935
							(naturalY < y &&  slots[slotIndex - 1] !== null)) {
15936
						y = mathMin(mathMax(0, naturalY), chart.plotHeight);
15937
					}
15938
 
15939
				} else {
15940
					y = naturalY;
15941
				}
15942
 
15943
				// get the x - use the natural x position for first and last slot, to prevent the top
15944
				// and botton slice connectors from touching each other on either side
15945
				x = options.justify ?
15946
					seriesCenter[0] + (i ? -1 : 1) * (radius + distanceOption) :
15947
					series.getX(y === centerY - radius - distanceOption || y === centerY + radius + distanceOption ? naturalY : y, i);
15948
 
15949
 
15950
				// Record the placement and visibility
15951
				dataLabel._attr = {
15952
					visibility: visibility,
15953
					align: labelPos[6]
15954
				};
15955
				dataLabel._pos = {
15956
					x: x + options.x +
15957
						({ left: connectorPadding, right: -connectorPadding }[labelPos[6]] || 0),
15958
					y: y + options.y - 10 // 10 is for the baseline (label vs text)
15959
				};
15960
				dataLabel.connX = x;
15961
				dataLabel.connY = y;
15962
 
15963
 
15964
				// Detect overflowing data labels
15965
				if (this.options.size === null) {
15966
					dataLabelWidth = dataLabel.width;
15967
					// Overflow left
15968
					if (x - dataLabelWidth < connectorPadding) {
15969
						overflow[3] = mathMax(mathRound(dataLabelWidth - x + connectorPadding), overflow[3]);
15970
 
15971
					// Overflow right
15972
					} else if (x + dataLabelWidth > plotWidth - connectorPadding) {
15973
						overflow[1] = mathMax(mathRound(x + dataLabelWidth - plotWidth + connectorPadding), overflow[1]);
15974
					}
15975
 
15976
					// Overflow top
15977
					if (y - labelHeight / 2 < 0) {
15978
						overflow[0] = mathMax(mathRound(-y + labelHeight / 2), overflow[0]);
15979
 
15980
					// Overflow left
15981
					} else if (y + labelHeight / 2 > plotHeight) {
15982
						overflow[2] = mathMax(mathRound(y + labelHeight / 2 - plotHeight), overflow[2]);
15983
					}
15984
				}
15985
			} // for each point
15986
		} // for each half
15987
 
15988
		// Do not apply the final placement and draw the connectors until we have verified
15989
		// that labels are not spilling over.
15990
		if (arrayMax(overflow) === 0 || this.verifyDataLabelOverflow(overflow)) {
15991
 
15992
			// Place the labels in the final position
15993
			this.placeDataLabels();
15994
 
15995
			// Draw the connectors
15996
			if (outside && connectorWidth) {
15997
				each(this.points, function (point) {
15998
					connector = point.connector;
15999
					labelPos = point.labelPos;
16000
					dataLabel = point.dataLabel;
16001
 
16002
					if (dataLabel && dataLabel._pos && point.visible) {
16003
						visibility = dataLabel._attr.visibility;
16004
						x = dataLabel.connX;
16005
						y = dataLabel.connY;
16006
						connectorPath = softConnector ? [
16007
							M,
16008
							x + (labelPos[6] === 'left' ? 5 : -5), y, // end of the string at the label
16009
							'C',
16010
							x, y, // first break, next to the label
16011
							2 * labelPos[2] - labelPos[4], 2 * labelPos[3] - labelPos[5],
16012
							labelPos[2], labelPos[3], // second break
16013
							L,
16014
							labelPos[4], labelPos[5] // base
16015
						] : [
16016
							M,
16017
							x + (labelPos[6] === 'left' ? 5 : -5), y, // end of the string at the label
16018
							L,
16019
							labelPos[2], labelPos[3], // second break
16020
							L,
16021
							labelPos[4], labelPos[5] // base
16022
						];
16023
 
16024
						if (connector) {
16025
							connector.animate({ d: connectorPath });
16026
							connector.attr('visibility', visibility);
16027
 
16028
						} else {
16029
							point.connector = connector = series.chart.renderer.path(connectorPath).attr({
16030
								'stroke-width': connectorWidth,
16031
								stroke: options.connectorColor || point.color || '#606060',
16032
								visibility: visibility
16033
								//zIndex: 0 // #2722 (reversed)
16034
							})
16035
							.add(series.dataLabelsGroup);
16036
						}
16037
					} else if (connector) {
16038
						point.connector = connector.destroy();
16039
					}
16040
				});
16041
			}
16042
		}
16043
	};
16044
	/**
16045
	 * Perform the final placement of the data labels after we have verified that they
16046
	 * fall within the plot area.
16047
	 */
16048
	seriesTypes.pie.prototype.placeDataLabels = function () {
16049
		each(this.points, function (point) {
16050
			var dataLabel = point.dataLabel,
16051
				_pos;
16052
 
16053
			if (dataLabel && point.visible) {
16054
				_pos = dataLabel._pos;
16055
				if (_pos) {
16056
					dataLabel.attr(dataLabel._attr);
16057
					dataLabel[dataLabel.moved ? 'animate' : 'attr'](_pos);
16058
					dataLabel.moved = true;
16059
				} else if (dataLabel) {
16060
					dataLabel.attr({ y: -999 });
16061
				}
16062
			}
16063
		});
16064
	};
16065
 
16066
	seriesTypes.pie.prototype.alignDataLabel =  noop;
16067
 
16068
	/**
16069
	 * Verify whether the data labels are allowed to draw, or we should run more translation and data
16070
	 * label positioning to keep them inside the plot area. Returns true when data labels are ready
16071
	 * to draw.
16072
	 */
16073
	seriesTypes.pie.prototype.verifyDataLabelOverflow = function (overflow) {
16074
 
16075
		var center = this.center,
16076
			options = this.options,
16077
			centerOption = options.center,
16078
			minSize = options.minSize || 80,
16079
			newSize = minSize,
16080
			ret;
16081
 
16082
		// Handle horizontal size and center
16083
		if (centerOption[0] !== null) { // Fixed center
16084
			newSize = mathMax(center[2] - mathMax(overflow[1], overflow[3]), minSize);
16085
 
16086
		} else { // Auto center
16087
			newSize = mathMax(
16088
				center[2] - overflow[1] - overflow[3], // horizontal overflow
16089
				minSize
16090
			);
16091
			center[0] += (overflow[3] - overflow[1]) / 2; // horizontal center
16092
		}
16093
 
16094
		// Handle vertical size and center
16095
		if (centerOption[1] !== null) { // Fixed center
16096
			newSize = mathMax(mathMin(newSize, center[2] - mathMax(overflow[0], overflow[2])), minSize);
16097
 
16098
		} else { // Auto center
16099
			newSize = mathMax(
16100
				mathMin(
16101
					newSize,
16102
					center[2] - overflow[0] - overflow[2] // vertical overflow
16103
				),
16104
				minSize
16105
			);
16106
			center[1] += (overflow[0] - overflow[2]) / 2; // vertical center
16107
		}
16108
 
16109
		// If the size must be decreased, we need to run translate and drawDataLabels again
16110
		if (newSize < center[2]) {
16111
			center[2] = newSize;
16112
			center[3] = Math.min(relativeLength(options.innerSize || 0, newSize), newSize); // #3632
16113
			this.translate(center);
16114
			each(this.points, function (point) {
16115
				if (point.dataLabel) {
16116
					point.dataLabel._pos = null; // reset
16117
				}
16118
			});
16119
 
16120
			if (this.drawDataLabels) {
16121
				this.drawDataLabels();
16122
			}
16123
		// Else, return true to indicate that the pie and its labels is within the plot area
16124
		} else {
16125
			ret = true;
16126
		}
16127
		return ret;
16128
	};
16129
}
16130
 
16131
if (seriesTypes.column) {
16132
 
16133
	/**
16134
	 * Override the basic data label alignment by adjusting for the position of the column
16135
	 */
16136
	seriesTypes.column.prototype.alignDataLabel = function (point, dataLabel, options,  alignTo, isNew) {
16137
		var inverted = this.chart.inverted,
16138
			series = point.series,
16139
			dlBox = point.dlBox || point.shapeArgs, // data label box for alignment
16140
			below = pick(point.below, point.plotY > pick(this.translatedThreshold, series.yAxis.len)), // point.below is used in range series
16141
			inside = pick(options.inside, !!this.options.stacking); // draw it inside the box?
16142
 
16143
		// Align to the column itself, or the top of it
16144
		if (dlBox) { // Area range uses this method but not alignTo
16145
			alignTo = merge(dlBox);
16146
 
16147
			if (inverted) {
16148
				alignTo = {
16149
					x: series.yAxis.len - alignTo.y - alignTo.height,
16150
					y: series.xAxis.len - alignTo.x - alignTo.width,
16151
					width: alignTo.height,
16152
					height: alignTo.width
16153
				};
16154
			}
16155
 
16156
			// Compute the alignment box
16157
			if (!inside) {
16158
				if (inverted) {
16159
					alignTo.x += below ? 0 : alignTo.width;
16160
					alignTo.width = 0;
16161
				} else {
16162
					alignTo.y += below ? alignTo.height : 0;
16163
					alignTo.height = 0;
16164
				}
16165
			}
16166
		}
16167
 
16168
 
16169
		// When alignment is undefined (typically columns and bars), display the individual
16170
		// point below or above the point depending on the threshold
16171
		options.align = pick(
16172
			options.align,
16173
			!inverted || inside ? 'center' : below ? 'right' : 'left'
16174
		);
16175
		options.verticalAlign = pick(
16176
			options.verticalAlign,
16177
			inverted || inside ? 'middle' : below ? 'top' : 'bottom'
16178
		);
16179
 
16180
		// Call the parent method
16181
		Series.prototype.alignDataLabel.call(this, point, dataLabel, options, alignTo, isNew);
16182
	};
16183
}
16184
 
16185
 
16186
 
16187
/**
16188
 * Highmaps JS v1.1.9 (2015-10-07)
16189
 * Highcharts module to hide overlapping data labels. This module is included by default in Highmaps.
16190
 *
16191
 * (c) 2010-2014 Torstein Honsi
16192
 *
16193
 * License: www.highcharts.com/license
16194
 */
16195
 
16196
/*global Highcharts, HighchartsAdapter */
16197
(function (H) {
16198
	var Chart = H.Chart,
16199
		each = H.each,
16200
		pick = H.pick,
16201
		addEvent = HighchartsAdapter.addEvent;
16202
 
16203
	// Collect potensial overlapping data labels. Stack labels probably don't need to be
16204
	// considered because they are usually accompanied by data labels that lie inside the columns.
16205
	Chart.prototype.callbacks.push(function (chart) {
16206
		function collectAndHide() {
16207
			var labels = [];
16208
 
16209
			each(chart.series, function (series) {
16210
				var dlOptions = series.options.dataLabels,
16211
					collections = series.dataLabelCollections || ['dataLabel']; // Range series have two collections
16212
				if ((dlOptions.enabled || series._hasPointLabels) && !dlOptions.allowOverlap && series.visible) { // #3866
16213
					each(collections, function (coll) {
16214
						each(series.points, function (point) {
16215
							if (point[coll]) {
16216
								point[coll].labelrank = pick(point.labelrank, point.shapeArgs && point.shapeArgs.height); // #4118
16217
								labels.push(point[coll]);
16218
							}
16219
						});
16220
					});
16221
				}
16222
			});
16223
			chart.hideOverlappingLabels(labels);
16224
		}
16225
 
16226
		// Do it now ...
16227
		collectAndHide();
16228
 
16229
		// ... and after each chart redraw
16230
		addEvent(chart, 'redraw', collectAndHide);
16231
 
16232
	});
16233
 
16234
	/**
16235
	 * Hide overlapping labels. Labels are moved and faded in and out on zoom to provide a smooth
16236
	 * visual imression.
16237
	 */
16238
	Chart.prototype.hideOverlappingLabels = function (labels) {
16239
 
16240
		var len = labels.length,
16241
			label,
16242
			i,
16243
			j,
16244
			label1,
16245
			label2,
16246
			isIntersecting,
16247
			pos1,
16248
			pos2,
16249
			padding,
16250
			intersectRect = function (x1, y1, w1, h1, x2, y2, w2, h2) {
16251
				return !(
16252
					x2 > x1 + w1 ||
16253
					x2 + w2 < x1 ||
16254
					y2 > y1 + h1 ||
16255
					y2 + h2 < y1
16256
				);
16257
			};
16258
 
16259
		// Mark with initial opacity
16260
		for (i = 0; i < len; i++) {
16261
			label = labels[i];
16262
			if (label) {
16263
				label.oldOpacity = label.opacity;
16264
				label.newOpacity = 1;
16265
			}
16266
		}
16267
 
16268
		// Prevent a situation in a gradually rising slope, that each label
16269
		// will hide the previous one because the previous one always has
16270
		// lower rank.
16271
		labels.sort(function (a, b) {
16272
			return (b.labelrank || 0) - (a.labelrank || 0);
16273
		});
16274
 
16275
		// Detect overlapping labels
16276
		for (i = 0; i < len; i++) {
16277
			label1 = labels[i];
16278
 
16279
			for (j = i + 1; j < len; ++j) {
16280
				label2 = labels[j];
16281
				if (label1 && label2 && label1.placed && label2.placed && label1.newOpacity !== 0 && label2.newOpacity !== 0) {
16282
					pos1 = label1.alignAttr;
16283
					pos2 = label2.alignAttr;
16284
					padding = 2 * (label1.box ? 0 : label1.padding); // Substract the padding if no background or border (#4333)
16285
					isIntersecting = intersectRect(
16286
						pos1.x,
16287
						pos1.y,
16288
						label1.width - padding,
16289
						label1.height - padding,
16290
						pos2.x,
16291
						pos2.y,
16292
						label2.width - padding,
16293
						label2.height - padding
16294
					);
16295
 
16296
					if (isIntersecting) {
16297
						(label1.labelrank < label2.labelrank ? label1 : label2).newOpacity = 0;
16298
					}
16299
				}
16300
			}
16301
		}
16302
 
16303
		// Hide or show
16304
		each(labels, function (label) {
16305
			var complete,
16306
				newOpacity;
16307
 
16308
			if (label) {
16309
				newOpacity = label.newOpacity;
16310
 
16311
				if (label.oldOpacity !== newOpacity && label.placed) {
16312
 
16313
					// Make sure the label is completely hidden to avoid catching clicks (#4362)
16314
					if (newOpacity) {
16315
						label.show(true);
16316
					} else {
16317
						complete = function () {
16318
							label.hide();
16319
						};
16320
					}
16321
 
16322
					// Animate or set the opacity
16323
					label.alignAttr.opacity = newOpacity;
16324
					label[label.isOld ? 'animate' : 'attr'](label.alignAttr, null, complete);
16325
 
16326
				}
16327
				label.isOld = true;
16328
			}
16329
		});
16330
	};
16331
 
16332
}(Highcharts));/**
16333
 * Override to use the extreme coordinates from the SVG shape, not the
16334
 * data values
16335
 */
16336
wrap(Axis.prototype, 'getSeriesExtremes', function (proceed) {
16337
	var isXAxis = this.isXAxis,
16338
		dataMin,
16339
		dataMax,
16340
		xData = [],
16341
		useMapGeometry;
16342
 
16343
	// Remove the xData array and cache it locally so that the proceed method doesn't use it
16344
	if (isXAxis) {
16345
		each(this.series, function (series, i) {
16346
			if (series.useMapGeometry) {
16347
				xData[i] = series.xData;
16348
				series.xData = [];
16349
			}
16350
		});
16351
	}
16352
 
16353
	// Call base to reach normal cartesian series (like mappoint)
16354
	proceed.call(this);
16355
 
16356
	// Run extremes logic for map and mapline
16357
	if (isXAxis) {
16358
		dataMin = pick(this.dataMin, Number.MAX_VALUE);
16359
		dataMax = pick(this.dataMax, -Number.MAX_VALUE);
16360
		each(this.series, function (series, i) {
16361
			if (series.useMapGeometry) {
16362
				dataMin = Math.min(dataMin, pick(series.minX, dataMin));
16363
				dataMax = Math.max(dataMax, pick(series.maxX, dataMin));
16364
				series.xData = xData[i]; // Reset xData array
16365
				useMapGeometry = true;
16366
			}
16367
		});
16368
		if (useMapGeometry) {
16369
			this.dataMin = dataMin;
16370
			this.dataMax = dataMax;
16371
		}
16372
	}
16373
});
16374
 
16375
/**
16376
 * Override axis translation to make sure the aspect ratio is always kept
16377
 */
16378
wrap(Axis.prototype, 'setAxisTranslation', function (proceed) {
16379
	var chart = this.chart,
16380
		mapRatio,
16381
		plotRatio = chart.plotWidth / chart.plotHeight,
16382
		adjustedAxisLength,
16383
		xAxis = chart.xAxis[0],
16384
		padAxis,
16385
		fixTo,
16386
		fixDiff,
16387
		preserveAspectRatio;
16388
 
16389
 
16390
	// Run the parent method
16391
	proceed.call(this);
16392
 
16393
	// Check for map-like series
16394
	if (this.coll === 'yAxis' && xAxis.transA !== UNDEFINED) {
16395
		each(this.series, function (series) {
16396
			if (series.preserveAspectRatio) {
16397
				preserveAspectRatio = true;
16398
			}
16399
		});
16400
	}
16401
 
16402
	// On Y axis, handle both
16403
	if (preserveAspectRatio) {
16404
 
16405
		// Use the same translation for both axes
16406
		this.transA = xAxis.transA = Math.min(this.transA, xAxis.transA);
16407
 
16408
		mapRatio = plotRatio / ((xAxis.max - xAxis.min) / (this.max - this.min));
16409
 
16410
		// What axis to pad to put the map in the middle
16411
		padAxis = mapRatio < 1 ? this : xAxis;
16412
 
16413
		// Pad it
16414
		adjustedAxisLength = (padAxis.max - padAxis.min) * padAxis.transA;
16415
		padAxis.pixelPadding = padAxis.len - adjustedAxisLength;
16416
		padAxis.minPixelPadding = padAxis.pixelPadding / 2;
16417
 
16418
		fixTo = padAxis.fixTo;
16419
		if (fixTo) {
16420
			fixDiff = fixTo[1] - padAxis.toValue(fixTo[0], true);
16421
			fixDiff *= padAxis.transA;
16422
			if (Math.abs(fixDiff) > padAxis.minPixelPadding || (padAxis.min === padAxis.dataMin && padAxis.max === padAxis.dataMax)) { // zooming out again, keep within restricted area
16423
				fixDiff = 0;
16424
			}
16425
			padAxis.minPixelPadding -= fixDiff;
16426
		}
16427
	}
16428
});
16429
 
16430
/**
16431
 * Override Axis.render in order to delete the fixTo prop
16432
 */
16433
wrap(Axis.prototype, 'render', function (proceed) {
16434
	proceed.call(this);
16435
	this.fixTo = null;
16436
});
16437
 
16438
 
16439
/**
16440
 * The ColorAxis object for inclusion in gradient legends
16441
 */
16442
var ColorAxis = Highcharts.ColorAxis = function () {
16443
	this.isColorAxis = true;
16444
	this.init.apply(this, arguments);
16445
};
16446
extend(ColorAxis.prototype, Axis.prototype);
16447
extend(ColorAxis.prototype, {
16448
	defaultColorAxisOptions: {
16449
		lineWidth: 0,
16450
		minPadding: 0,
16451
		maxPadding: 0,
16452
		gridLineWidth: 1,
16453
		tickPixelInterval: 72,
16454
		startOnTick: true,
16455
		endOnTick: true,
16456
		offset: 0,
16457
		marker: {
16458
			animation: {
16459
				duration: 50
16460
			},
16461
			color: 'gray',
16462
			width: 0.01
16463
		},
16464
		labels: {
16465
			overflow: 'justify'
16466
		},
16467
		minColor: '#EFEFFF',
16468
		maxColor: '#003875',
16469
		tickLength: 5
16470
	},
16471
	init: function (chart, userOptions) {
16472
		var horiz = chart.options.legend.layout !== 'vertical',
16473
			options;
16474
 
16475
		// Build the options
16476
		options = merge(this.defaultColorAxisOptions, {
16477
			side: horiz ? 2 : 1,
16478
			reversed: !horiz
16479
		}, userOptions, {
16480
			opposite: !horiz,
16481
			showEmpty: false,
16482
			title: null,
16483
			isColor: true
16484
		});
16485
 
16486
		Axis.prototype.init.call(this, chart, options);
16487
 
16488
		// Base init() pushes it to the xAxis array, now pop it again
16489
		//chart[this.isXAxis ? 'xAxis' : 'yAxis'].pop();
16490
 
16491
		// Prepare data classes
16492
		if (userOptions.dataClasses) {
16493
			this.initDataClasses(userOptions);
16494
		}
16495
		this.initStops(userOptions);
16496
 
16497
		// Override original axis properties
16498
		this.horiz = horiz;
16499
		this.zoomEnabled = false;
16500
	},
16501
 
16502
	/*
16503
	 * Return an intermediate color between two colors, according to pos where 0
16504
	 * is the from color and 1 is the to color.
16505
	 * NOTE: Changes here should be copied
16506
	 * to the same function in drilldown.src.js and solid-gauge-src.js.
16507
	 */
16508
	tweenColors: function (from, to, pos) {
16509
		// Check for has alpha, because rgba colors perform worse due to lack of
16510
		// support in WebKit.
16511
		var hasAlpha,
16512
			ret;
16513
 
16514
		// Unsupported color, return to-color (#3920)
16515
		if (!to.rgba.length || !from.rgba.length) {
16516
			ret = to.raw || 'none';
16517
 
16518
		// Interpolate
16519
		} else {
16520
			from = from.rgba;
16521
			to = to.rgba;
16522
			hasAlpha = (to[3] !== 1 || from[3] !== 1);
16523
			ret = (hasAlpha ? 'rgba(' : 'rgb(') +
16524
				Math.round(to[0] + (from[0] - to[0]) * (1 - pos)) + ',' +
16525
				Math.round(to[1] + (from[1] - to[1]) * (1 - pos)) + ',' +
16526
				Math.round(to[2] + (from[2] - to[2]) * (1 - pos)) +
16527
				(hasAlpha ? (',' + (to[3] + (from[3] - to[3]) * (1 - pos))) : '') + ')';
16528
		}
16529
		return ret;
16530
	},
16531
 
16532
	initDataClasses: function (userOptions) {
16533
		var axis = this,
16534
			chart = this.chart,
16535
			dataClasses,
16536
			colorCounter = 0,
16537
			options = this.options,
16538
			len = userOptions.dataClasses.length;
16539
		this.dataClasses = dataClasses = [];
16540
		this.legendItems = [];
16541
 
16542
		each(userOptions.dataClasses, function (dataClass, i) {
16543
			var colors;
16544
 
16545
			dataClass = merge(dataClass);
16546
			dataClasses.push(dataClass);
16547
			if (!dataClass.color) {
16548
				if (options.dataClassColor === 'category') {
16549
					colors = chart.options.colors;
16550
					dataClass.color = colors[colorCounter++];
16551
					// loop back to zero
16552
					if (colorCounter === colors.length) {
16553
						colorCounter = 0;
16554
					}
16555
				} else {
16556
					dataClass.color = axis.tweenColors(
16557
						Color(options.minColor),
16558
						Color(options.maxColor),
16559
						len < 2 ? 0.5 : i / (len - 1) // #3219
16560
					);
16561
				}
16562
			}
16563
		});
16564
	},
16565
 
16566
	initStops: function (userOptions) {
16567
		this.stops = userOptions.stops || [
16568
			[0, this.options.minColor],
16569
			[1, this.options.maxColor]
16570
		];
16571
		each(this.stops, function (stop) {
16572
			stop.color = Color(stop[1]);
16573
		});
16574
	},
16575
 
16576
	/**
16577
	 * Extend the setOptions method to process extreme colors and color
16578
	 * stops.
16579
	 */
16580
	setOptions: function (userOptions) {
16581
		Axis.prototype.setOptions.call(this, userOptions);
16582
 
16583
		this.options.crosshair = this.options.marker;
16584
		this.coll = 'colorAxis';
16585
	},
16586
 
16587
	setAxisSize: function () {
16588
		var symbol = this.legendSymbol,
16589
			chart = this.chart,
16590
			x,
16591
			y,
16592
			width,
16593
			height;
16594
 
16595
		if (symbol) {
16596
			this.left = x = symbol.attr('x');
16597
			this.top = y = symbol.attr('y');
16598
			this.width = width = symbol.attr('width');
16599
			this.height = height = symbol.attr('height');
16600
			this.right = chart.chartWidth - x - width;
16601
			this.bottom = chart.chartHeight - y - height;
16602
 
16603
			this.len = this.horiz ? width : height;
16604
			this.pos = this.horiz ? x : y;
16605
		}
16606
	},
16607
 
16608
	/**
16609
	 * Translate from a value to a color
16610
	 */
16611
	toColor: function (value, point) {
16612
		var pos,
16613
			stops = this.stops,
16614
			from,
16615
			to,
16616
			color,
16617
			dataClasses = this.dataClasses,
16618
			dataClass,
16619
			i;
16620
 
16621
		if (dataClasses) {
16622
			i = dataClasses.length;
16623
			while (i--) {
16624
				dataClass = dataClasses[i];
16625
				from = dataClass.from;
16626
				to = dataClass.to;
16627
				if ((from === UNDEFINED || value >= from) && (to === UNDEFINED || value <= to)) {
16628
					color = dataClass.color;
16629
					if (point) {
16630
						point.dataClass = i;
16631
					}
16632
					break;
16633
				}
16634
			}
16635
 
16636
		} else {
16637
 
16638
			if (this.isLog) {
16639
				value = this.val2lin(value);
16640
			}
16641
			pos = 1 - ((this.max - value) / ((this.max - this.min) || 1));
16642
			i = stops.length;
16643
			while (i--) {
16644
				if (pos > stops[i][0]) {
16645
					break;
16646
				}
16647
			}
16648
			from = stops[i] || stops[i + 1];
16649
			to = stops[i + 1] || from;
16650
 
16651
			// The position within the gradient
16652
			pos = 1 - (to[0] - pos) / ((to[0] - from[0]) || 1);
16653
 
16654
			color = this.tweenColors(
16655
				from.color,
16656
				to.color,
16657
				pos
16658
			);
16659
		}
16660
		return color;
16661
	},
16662
 
16663
	/**
16664
	 * Override the getOffset method to add the whole axis groups inside the legend.
16665
	 */
16666
	getOffset: function () {
16667
		var group = this.legendGroup,
16668
			sideOffset = this.chart.axisOffset[this.side];
16669
 
16670
		if (group) {
16671
 
16672
			// Hook for the getOffset method to add groups to this parent group
16673
			this.axisParent = group;
16674
 
16675
			// Call the base
16676
			Axis.prototype.getOffset.call(this);
16677
 
16678
			// First time only
16679
			if (!this.added) {
16680
 
16681
				this.added = true;
16682
 
16683
				this.labelLeft = 0;
16684
				this.labelRight = this.width;
16685
			}
16686
			// Reset it to avoid color axis reserving space
16687
			this.chart.axisOffset[this.side] = sideOffset;
16688
		}
16689
	},
16690
 
16691
	/**
16692
	 * Create the color gradient
16693
	 */
16694
	setLegendColor: function () {
16695
		var grad,
16696
			horiz = this.horiz,
16697
			options = this.options,
16698
			reversed = this.reversed;
16699
 
16700
		grad = horiz ? [+reversed, 0, +!reversed, 0] : [0, +!reversed, 0, +reversed]; // #3190
16701
		this.legendColor = {
16702
			linearGradient: { x1: grad[0], y1: grad[1], x2: grad[2], y2: grad[3] },
16703
			stops: options.stops || [
16704
				[0, options.minColor],
16705
				[1, options.maxColor]
16706
			]
16707
		};
16708
	},
16709
 
16710
	/**
16711
	 * The color axis appears inside the legend and has its own legend symbol
16712
	 */
16713
	drawLegendSymbol: function (legend, item) {
16714
		var padding = legend.padding,
16715
			legendOptions = legend.options,
16716
			horiz = this.horiz,
16717
			box,
16718
			width = pick(legendOptions.symbolWidth, horiz ? 200 : 12),
16719
			height = pick(legendOptions.symbolHeight, horiz ? 12 : 200),
16720
			labelPadding = pick(legendOptions.labelPadding, horiz ? 16 : 30),
16721
			itemDistance = pick(legendOptions.itemDistance, 10);
16722
 
16723
		this.setLegendColor();
16724
 
16725
		// Create the gradient
16726
		item.legendSymbol = this.chart.renderer.rect(
16727
			0,
16728
			legend.baseline - 11,
16729
			width,
16730
			height
16731
		).attr({
16732
			zIndex: 1
16733
		}).add(item.legendGroup);
16734
		box = item.legendSymbol.getBBox();
16735
 
16736
		// Set how much space this legend item takes up
16737
		this.legendItemWidth = width + padding + (horiz ? itemDistance : labelPadding);
16738
		this.legendItemHeight = height + padding + (horiz ? labelPadding : 0);
16739
	},
16740
	/**
16741
	 * Fool the legend
16742
	 */
16743
	setState: noop,
16744
	visible: true,
16745
	setVisible: noop,
16746
	getSeriesExtremes: function () {
16747
		var series;
16748
		if (this.series.length) {
16749
			series = this.series[0];
16750
			this.dataMin = series.valueMin;
16751
			this.dataMax = series.valueMax;
16752
		}
16753
	},
16754
	drawCrosshair: function (e, point) {
16755
		var plotX = point && point.plotX,
16756
			plotY = point && point.plotY,
16757
			crossPos,
16758
			axisPos = this.pos,
16759
			axisLen = this.len;
16760
 
16761
		if (point) {
16762
			crossPos = this.toPixels(point[point.series.colorKey]);
16763
			if (crossPos < axisPos) {
16764
				crossPos = axisPos - 2;
16765
			} else if (crossPos > axisPos + axisLen) {
16766
				crossPos = axisPos + axisLen + 2;
16767
			}
16768
 
16769
			point.plotX = crossPos;
16770
			point.plotY = this.len - crossPos;
16771
			Axis.prototype.drawCrosshair.call(this, e, point);
16772
			point.plotX = plotX;
16773
			point.plotY = plotY;
16774
 
16775
			if (this.cross) {
16776
				this.cross
16777
					.attr({
16778
						fill: this.crosshair.color
16779
					})
16780
					.add(this.legendGroup);
16781
			}
16782
		}
16783
	},
16784
	getPlotLinePath: function (a, b, c, d, pos) {
16785
		if (typeof pos === 'number') { // crosshairs only // #3969 pos can be 0 !!
16786
			return this.horiz ?
16787
				['M', pos - 4, this.top - 6, 'L', pos + 4, this.top - 6, pos, this.top, 'Z'] :
16788
				['M', this.left, pos, 'L', this.left - 6, pos + 6, this.left - 6, pos - 6, 'Z'];
16789
		} else {
16790
			return Axis.prototype.getPlotLinePath.call(this, a, b, c, d);
16791
		}
16792
	},
16793
 
16794
	update: function (newOptions, redraw) {
16795
		var chart = this.chart,
16796
			legend = chart.legend;
16797
 
16798
		each(this.series, function (series) {
16799
			series.isDirtyData = true; // Needed for Axis.update when choropleth colors change
16800
		});
16801
 
16802
		// When updating data classes, destroy old items and make sure new ones are created (#3207)
16803
		if (newOptions.dataClasses && legend.allItems) {
16804
			each(legend.allItems, function (item) {
16805
				if (item.isDataClass) {
16806
					item.legendGroup.destroy();
16807
				}
16808
			});
16809
			chart.isDirtyLegend = true;
16810
		}
16811
 
16812
		// Keep the options structure updated for export. Unlike xAxis and yAxis, the colorAxis is
16813
		// not an array. (#3207)
16814
		chart.options[this.coll] = merge(this.userOptions, newOptions);
16815
 
16816
		Axis.prototype.update.call(this, newOptions, redraw);
16817
		if (this.legendItem) {
16818
			this.setLegendColor();
16819
			legend.colorizeItem(this, true);
16820
		}
16821
	},
16822
 
16823
	/**
16824
	 * Get the legend item symbols for data classes
16825
	 */
16826
	getDataClassLegendSymbols: function () {
16827
		var axis = this,
16828
			chart = this.chart,
16829
			legendItems = this.legendItems,
16830
			legendOptions = chart.options.legend,
16831
			valueDecimals = legendOptions.valueDecimals,
16832
			valueSuffix = legendOptions.valueSuffix || '',
16833
			name;
16834
 
16835
		if (!legendItems.length) {
16836
			each(this.dataClasses, function (dataClass, i) {
16837
				var vis = true,
16838
					from = dataClass.from,
16839
					to = dataClass.to;
16840
 
16841
				// Assemble the default name. This can be overridden by legend.options.labelFormatter
16842
				name = '';
16843
				if (from === UNDEFINED) {
16844
					name = '< ';
16845
				} else if (to === UNDEFINED) {
16846
					name = '> ';
16847
				}
16848
				if (from !== UNDEFINED) {
16849
					name += Highcharts.numberFormat(from, valueDecimals) + valueSuffix;
16850
				}
16851
				if (from !== UNDEFINED && to !== UNDEFINED) {
16852
					name += ' - ';
16853
				}
16854
				if (to !== UNDEFINED) {
16855
					name += Highcharts.numberFormat(to, valueDecimals) + valueSuffix;
16856
				}
16857
 
16858
				// Add a mock object to the legend items
16859
				legendItems.push(extend({
16860
					chart: chart,
16861
					name: name,
16862
					options: {},
16863
					drawLegendSymbol: LegendSymbolMixin.drawRectangle,
16864
					visible: true,
16865
					setState: noop,
16866
					isDataClass: true,
16867
					setVisible: function () {
16868
						vis = this.visible = !vis;
16869
						each(axis.series, function (series) {
16870
							each(series.points, function (point) {
16871
								if (point.dataClass === i) {
16872
									point.setVisible(vis);
16873
								}
16874
							});
16875
						});
16876
 
16877
						chart.legend.colorizeItem(this, vis);
16878
					}
16879
				}, dataClass));
16880
			});
16881
		}
16882
		return legendItems;
16883
	},
16884
	name: '' // Prevents 'undefined' in legend in IE8
16885
});
16886
 
16887
/**
16888
 * Handle animation of the color attributes directly
16889
 */
16890
each(['fill', 'stroke'], function (prop) {
16891
	HighchartsAdapter.addAnimSetter(prop, function (fx) {
16892
		fx.elem.attr(prop, ColorAxis.prototype.tweenColors(Color(fx.start), Color(fx.end), fx.pos));
16893
	});
16894
});
16895
 
16896
/**
16897
 * Extend the chart getAxes method to also get the color axis
16898
 */
16899
wrap(Chart.prototype, 'getAxes', function (proceed) {
16900
 
16901
	var options = this.options,
16902
		colorAxisOptions = options.colorAxis;
16903
 
16904
	proceed.call(this);
16905
 
16906
	this.colorAxis = [];
16907
	if (colorAxisOptions) {
16908
		proceed = new ColorAxis(this, colorAxisOptions); // Fake assignment for jsLint
16909
	}
16910
});
16911
 
16912
 
16913
/**
16914
 * Wrap the legend getAllItems method to add the color axis. This also removes the
16915
 * axis' own series to prevent them from showing up individually.
16916
 */
16917
wrap(Legend.prototype, 'getAllItems', function (proceed) {
16918
	var allItems = [],
16919
		colorAxis = this.chart.colorAxis[0];
16920
 
16921
	if (colorAxis) {
16922
 
16923
		// Data classes
16924
		if (colorAxis.options.dataClasses) {
16925
			allItems = allItems.concat(colorAxis.getDataClassLegendSymbols());
16926
		// Gradient legend
16927
		} else {
16928
			// Add this axis on top
16929
			allItems.push(colorAxis);
16930
		}
16931
 
16932
		// Don't add the color axis' series
16933
		each(colorAxis.series, function (series) {
16934
			series.options.showInLegend = false;
16935
		});
16936
	}
16937
 
16938
	return allItems.concat(proceed.call(this));
16939
});/**
16940
 * Mixin for maps and heatmaps
16941
 */
16942
var colorPointMixin = {
16943
	/**
16944
	 * Set the visibility of a single point
16945
	 */
16946
	setVisible: function (vis) {
16947
		var point = this,
16948
			method = vis ? 'show' : 'hide';
16949
 
16950
		// Show and hide associated elements
16951
		each(['graphic', 'dataLabel'], function (key) {
16952
			if (point[key]) {
16953
				point[key][method]();
16954
			}
16955
		});
16956
	}
16957
};
16958
var colorSeriesMixin = {
16959
 
16960
	pointAttrToOptions: { // mapping between SVG attributes and the corresponding options
16961
		stroke: 'borderColor',
16962
		'stroke-width': 'borderWidth',
16963
		fill: 'color',
16964
		dashstyle: 'dashStyle'
16965
	},
16966
	pointArrayMap: ['value'],
16967
	axisTypes: ['xAxis', 'yAxis', 'colorAxis'],
16968
	optionalAxis: 'colorAxis',
16969
	trackerGroups: ['group', 'markerGroup', 'dataLabelsGroup'],
16970
	getSymbol: noop,
16971
	parallelArrays: ['x', 'y', 'value'],
16972
	colorKey: 'value',
16973
 
16974
	/**
16975
	 * In choropleth maps, the color is a result of the value, so this needs translation too
16976
	 */
16977
	translateColors: function () {
16978
		var series = this,
16979
			nullColor = this.options.nullColor,
16980
			colorAxis = this.colorAxis,
16981
			colorKey = this.colorKey;
16982
 
16983
		each(this.data, function (point) {
16984
			var value = point[colorKey],
16985
				color;
16986
 
16987
			color = point.options.color ||
16988
				(value === null ? nullColor : (colorAxis && value !== undefined) ? colorAxis.toColor(value, point) : point.color || series.color);
16989
 
16990
			if (color) {
16991
				point.color = color;
16992
			}
16993
		});
16994
	}
16995
};
16996
// The vector-effect attribute is not supported in IE <= 11 (at least), so we need
16997
// diffent logic (#3218)
16998
var supportsVectorEffect = document.documentElement.style.vectorEffect !== undefined;
16999
 
17000
/**
17001
 * Extend the default options with map options
17002
 */
17003
defaultPlotOptions.map = merge(defaultPlotOptions.scatter, {
17004
	allAreas: true,
17005
 
17006
	animation: false, // makes the complex shapes slow
17007
	nullColor: '#F8F8F8',
17008
	borderColor: 'silver',
17009
	borderWidth: 1,
17010
	marker: null,
17011
	stickyTracking: false,
17012
	dataLabels: {
17013
		formatter: function () { // #2945
17014
			return this.point.value;
17015
		},
17016
		inside: true, // for the color
17017
		verticalAlign: 'middle',
17018
		crop: false,
17019
		overflow: false,
17020
		padding: 0
17021
	},
17022
	turboThreshold: 0,
17023
	tooltip: {
17024
		followPointer: true,
17025
		pointFormat: '{point.name}: {point.value}<br/>'
17026
	},
17027
	states: {
17028
		normal: {
17029
			animation: true
17030
		},
17031
		hover: {
17032
			brightness: 0.2,
17033
			halo: null
17034
		}
17035
	}
17036
});
17037
 
17038
/**
17039
 * The MapAreaPoint object
17040
 */
17041
var MapAreaPoint = extendClass(Point, extend({
17042
	/**
17043
	 * Extend the Point object to split paths
17044
	 */
17045
	applyOptions: function (options, x) {
17046
 
17047
		var point = Point.prototype.applyOptions.call(this, options, x),
17048
			series = this.series,
17049
			joinBy = series.joinBy,
17050
			mapPoint;
17051
 
17052
		if (series.mapData) {
17053
			mapPoint = point[joinBy[1]] !== undefined && series.mapMap[point[joinBy[1]]];
17054
			if (mapPoint) {
17055
				// This applies only to bubbles
17056
				if (series.xyFromShape) {
17057
					point.x = mapPoint._midX;
17058
					point.y = mapPoint._midY;
17059
				}
17060
				extend(point, mapPoint); // copy over properties
17061
			} else {
17062
				point.value = point.value || null;
17063
			}
17064
		}
17065
 
17066
		return point;
17067
	},
17068
 
17069
	/**
17070
	 * Stop the fade-out
17071
	 */
17072
	onMouseOver: function (e) {
17073
		clearTimeout(this.colorInterval);
17074
		if (this.value !== null) {
17075
			Point.prototype.onMouseOver.call(this, e);
17076
		} else { //#3401 Tooltip doesn't hide when hovering over null points
17077
			this.series.onMouseOut(e);
17078
		}
17079
	},
17080
	/**
17081
	 * Custom animation for tweening out the colors. Animation reduces blinking when hovering
17082
	 * over islands and coast lines. We run a custom implementation of animation becuase we
17083
	 * need to be able to run this independently from other animations like zoom redraw. Also,
17084
	 * adding color animation to the adapters would introduce almost the same amount of code.
17085
	 */
17086
	onMouseOut: function () {
17087
		var point = this,
17088
			start = +new Date(),
17089
			normalColor = Color(point.color),
17090
			hoverColor = Color(point.pointAttr.hover.fill),
17091
			animation = point.series.options.states.normal.animation,
17092
			duration = animation && (animation.duration || 500),
17093
			fill;
17094
 
17095
		if (duration && normalColor.rgba.length === 4 && hoverColor.rgba.length === 4 && point.state !== 'select') {
17096
			fill = point.pointAttr[''].fill;
17097
			delete point.pointAttr[''].fill; // avoid resetting it in Point.setState
17098
 
17099
			clearTimeout(point.colorInterval);
17100
			point.colorInterval = setInterval(function () {
17101
				var pos = (new Date() - start) / duration,
17102
					graphic = point.graphic;
17103
				if (pos > 1) {
17104
					pos = 1;
17105
				}
17106
				if (graphic) {
17107
					graphic.attr('fill', ColorAxis.prototype.tweenColors.call(0, hoverColor, normalColor, pos));
17108
				}
17109
				if (pos >= 1) {
17110
					clearTimeout(point.colorInterval);
17111
				}
17112
			}, 13);
17113
		}
17114
		Point.prototype.onMouseOut.call(point);
17115
 
17116
		if (fill) {
17117
			point.pointAttr[''].fill = fill;
17118
		}
17119
	},
17120
 
17121
	/**
17122
	 * Zoom the chart to view a specific area point
17123
	 */
17124
	zoomTo: function () {
17125
		var point = this,
17126
			series = point.series;
17127
 
17128
		series.xAxis.setExtremes(
17129
			point._minX,
17130
			point._maxX,
17131
			false
17132
		);
17133
		series.yAxis.setExtremes(
17134
			point._minY,
17135
			point._maxY,
17136
			false
17137
		);
17138
		series.chart.redraw();
17139
	}
17140
}, colorPointMixin)
17141
);
17142
 
17143
/**
17144
 * Add the series type
17145
 */
17146
seriesTypes.map = extendClass(seriesTypes.scatter, merge(colorSeriesMixin, {
17147
	type: 'map',
17148
	pointClass: MapAreaPoint,
17149
	supportsDrilldown: true,
17150
	getExtremesFromAll: true,
17151
	useMapGeometry: true, // get axis extremes from paths, not values
17152
	forceDL: true,
17153
	searchPoint: noop,
17154
	directTouch: true, // When tooltip is not shared, this series (and derivatives) requires direct touch/hover. KD-tree does not apply.
17155
	preserveAspectRatio: true, // X axis and Y axis must have same translation slope
17156
	/**
17157
	 * Get the bounding box of all paths in the map combined.
17158
	 */
17159
	getBox: function (paths) {
17160
		var MAX_VALUE = Number.MAX_VALUE,
17161
			maxX = -MAX_VALUE,
17162
			minX =  MAX_VALUE,
17163
			maxY = -MAX_VALUE,
17164
			minY =  MAX_VALUE,
17165
			minRange = MAX_VALUE,
17166
			xAxis = this.xAxis,
17167
			yAxis = this.yAxis,
17168
			hasBox;
17169
 
17170
		// Find the bounding box
17171
		each(paths || [], function (point) {
17172
 
17173
			if (point.path) {
17174
				if (typeof point.path === 'string') {
17175
					point.path = Highcharts.splitPath(point.path);
17176
				}
17177
 
17178
				var path = point.path || [],
17179
					i = path.length,
17180
					even = false, // while loop reads from the end
17181
					pointMaxX = -MAX_VALUE,
17182
					pointMinX =  MAX_VALUE,
17183
					pointMaxY = -MAX_VALUE,
17184
					pointMinY =  MAX_VALUE,
17185
					properties = point.properties;
17186
 
17187
				// The first time a map point is used, analyze its box
17188
				if (!point._foundBox) {
17189
					while (i--) {
17190
						if (typeof path[i] === 'number' && !isNaN(path[i])) {
17191
							if (even) { // even = x
17192
								pointMaxX = Math.max(pointMaxX, path[i]);
17193
								pointMinX = Math.min(pointMinX, path[i]);
17194
							} else { // odd = Y
17195
								pointMaxY = Math.max(pointMaxY, path[i]);
17196
								pointMinY = Math.min(pointMinY, path[i]);
17197
							}
17198
							even = !even;
17199
						}
17200
					}
17201
					// Cache point bounding box for use to position data labels, bubbles etc
17202
					point._midX = pointMinX + (pointMaxX - pointMinX) *
17203
						(point.middleX || (properties && properties['hc-middle-x']) || 0.5); // pick is slower and very marginally needed
17204
					point._midY = pointMinY + (pointMaxY - pointMinY) *
17205
						(point.middleY || (properties && properties['hc-middle-y']) || 0.5);
17206
					point._maxX = pointMaxX;
17207
					point._minX = pointMinX;
17208
					point._maxY = pointMaxY;
17209
					point._minY = pointMinY;
17210
					point.labelrank = pick(point.labelrank, (pointMaxX - pointMinX) * (pointMaxY - pointMinY));
17211
					point._foundBox = true;
17212
				}
17213
 
17214
				maxX = Math.max(maxX, point._maxX);
17215
				minX = Math.min(minX, point._minX);
17216
				maxY = Math.max(maxY, point._maxY);
17217
				minY = Math.min(minY, point._minY);
17218
				minRange = Math.min(point._maxX - point._minX, point._maxY - point._minY, minRange);
17219
				hasBox = true;
17220
			}
17221
		});
17222
 
17223
		// Set the box for the whole series
17224
		if (hasBox) {
17225
			this.minY = Math.min(minY, pick(this.minY, MAX_VALUE));
17226
			this.maxY = Math.max(maxY, pick(this.maxY, -MAX_VALUE));
17227
			this.minX = Math.min(minX, pick(this.minX, MAX_VALUE));
17228
			this.maxX = Math.max(maxX, pick(this.maxX, -MAX_VALUE));
17229
 
17230
			// If no minRange option is set, set the default minimum zooming range to 5 times the
17231
			// size of the smallest element
17232
			if (xAxis && xAxis.options.minRange === undefined) {
17233
				xAxis.minRange = Math.min(5 * minRange, (this.maxX - this.minX) / 5, xAxis.minRange || MAX_VALUE);
17234
			}
17235
			if (yAxis && yAxis.options.minRange === undefined) {
17236
				yAxis.minRange = Math.min(5 * minRange, (this.maxY - this.minY) / 5, yAxis.minRange || MAX_VALUE);
17237
			}
17238
		}
17239
	},
17240
 
17241
	getExtremes: function () {
17242
		// Get the actual value extremes for colors
17243
		Series.prototype.getExtremes.call(this, this.valueData);
17244
 
17245
		// Recalculate box on updated data
17246
		if (this.chart.hasRendered && this.isDirtyData) {
17247
			this.getBox(this.options.data);
17248
		}
17249
 
17250
		this.valueMin = this.dataMin;
17251
		this.valueMax = this.dataMax;
17252
 
17253
		// Extremes for the mock Y axis
17254
		this.dataMin = this.minY;
17255
		this.dataMax = this.maxY;
17256
	},
17257
 
17258
	/**
17259
	 * Translate the path so that it automatically fits into the plot area box
17260
	 * @param {Object} path
17261
	 */
17262
	translatePath: function (path) {
17263
 
17264
		var series = this,
17265
			even = false, // while loop reads from the end
17266
			xAxis = series.xAxis,
17267
			yAxis = series.yAxis,
17268
			xMin = xAxis.min,
17269
			xTransA = xAxis.transA,
17270
			xMinPixelPadding = xAxis.minPixelPadding,
17271
			yMin = yAxis.min,
17272
			yTransA = yAxis.transA,
17273
			yMinPixelPadding = yAxis.minPixelPadding,
17274
			i,
17275
			ret = []; // Preserve the original
17276
 
17277
		// Do the translation
17278
		if (path) {
17279
			i = path.length;
17280
			while (i--) {
17281
				if (typeof path[i] === 'number') {
17282
					ret[i] = even ?
17283
						(path[i] - xMin) * xTransA + xMinPixelPadding :
17284
						(path[i] - yMin) * yTransA + yMinPixelPadding;
17285
					even = !even;
17286
				} else {
17287
					ret[i] = path[i];
17288
				}
17289
			}
17290
		}
17291
 
17292
		return ret;
17293
	},
17294
 
17295
	/**
17296
	 * Extend setData to join in mapData. If the allAreas option is true, all areas
17297
	 * from the mapData are used, and those that don't correspond to a data value
17298
	 * are given null values.
17299
	 */
17300
	setData: function (data, redraw) {
17301
		var options = this.options,
17302
			mapData = options.mapData,
17303
			joinBy = options.joinBy,
17304
			joinByNull = joinBy === null,
17305
			dataUsed = [],
17306
			mapPoint,
17307
			transform,
17308
			mapTransforms,
17309
			props,
17310
			i;
17311
 
17312
		if (joinByNull) {
17313
			joinBy = '_i';
17314
		}
17315
		joinBy = this.joinBy = Highcharts.splat(joinBy);
17316
		if (!joinBy[1]) {
17317
			joinBy[1] = joinBy[0];
17318
		}
17319
 
17320
		// Pick up numeric values, add index
17321
		if (data) {
17322
			each(data, function (val, i) {
17323
				if (typeof val === 'number') {
17324
					data[i] = {
17325
						value: val
17326
					};
17327
				}
17328
				if (joinByNull) {
17329
					data[i]._i = i;
17330
				}
17331
			});
17332
		}
17333
 
17334
		this.getBox(data);
17335
		if (mapData) {
17336
			if (mapData.type === 'FeatureCollection') {
17337
				if (mapData['hc-transform']) {
17338
					this.chart.mapTransforms = mapTransforms = mapData['hc-transform'];
17339
					// Cache cos/sin of transform rotation angle
17340
					for (transform in mapTransforms) {
17341
						if (mapTransforms.hasOwnProperty(transform) && transform.rotation) {
17342
							transform.cosAngle = Math.cos(transform.rotation);
17343
							transform.sinAngle = Math.sin(transform.rotation);
17344
						}
17345
					}
17346
				}
17347
				mapData = Highcharts.geojson(mapData, this.type, this);
17348
			}
17349
 
17350
			this.getBox(mapData);
17351
			this.mapData = mapData;
17352
			this.mapMap = {};
17353
 
17354
			for (i = 0; i < mapData.length; i++) {
17355
				mapPoint = mapData[i];
17356
				props = mapPoint.properties;
17357
 
17358
				mapPoint._i = i;
17359
				// Copy the property over to root for faster access
17360
				if (joinBy[0] && props && props[joinBy[0]]) {
17361
					mapPoint[joinBy[0]] = props[joinBy[0]];
17362
				}
17363
				this.mapMap[mapPoint[joinBy[0]]] = mapPoint;
17364
			}
17365
 
17366
			if (options.allAreas) {
17367
 
17368
				data = data || [];
17369
 
17370
				// Registered the point codes that actually hold data
17371
				if (joinBy[1]) {
17372
					each(data, function (point) {
17373
						dataUsed.push(point[joinBy[1]]);
17374
					});
17375
				}
17376
 
17377
				// Add those map points that don't correspond to data, which will be drawn as null points
17378
				dataUsed = '|' + dataUsed.join('|') + '|'; // String search is faster than array.indexOf
17379
 
17380
				each(mapData, function (mapPoint) {
17381
					if (!joinBy[0] || dataUsed.indexOf('|' + mapPoint[joinBy[0]] + '|') === -1) {
17382
						data.push(merge(mapPoint, { value: null }));
17383
					}
17384
				});
17385
			}
17386
		}
17387
		Series.prototype.setData.call(this, data, redraw);
17388
	},
17389
 
17390
 
17391
	/**
17392
	 * No graph for the map series
17393
	 */
17394
	drawGraph: noop,
17395
 
17396
	/**
17397
	 * We need the points' bounding boxes in order to draw the data labels, so
17398
	 * we skip it now and call it from drawPoints instead.
17399
	 */
17400
	drawDataLabels: noop,
17401
 
17402
	/**
17403
	 * Allow a quick redraw by just translating the area group. Used for zooming and panning
17404
	 * in capable browsers.
17405
	 */
17406
	doFullTranslate: function () {
17407
		return this.isDirtyData || this.chart.isResizing || this.chart.renderer.isVML || !this.baseTrans;
17408
	},
17409
 
17410
	/**
17411
	 * Add the path option for data points. Find the max value for color calculation.
17412
	 */
17413
	translate: function () {
17414
		var series = this,
17415
			xAxis = series.xAxis,
17416
			yAxis = series.yAxis,
17417
			doFullTranslate = series.doFullTranslate();
17418
 
17419
		series.generatePoints();
17420
 
17421
		each(series.data, function (point) {
17422
 
17423
			// Record the middle point (loosely based on centroid), determined
17424
			// by the middleX and middleY options.
17425
			point.plotX = xAxis.toPixels(point._midX, true);
17426
			point.plotY = yAxis.toPixels(point._midY, true);
17427
 
17428
			if (doFullTranslate) {
17429
 
17430
				point.shapeType = 'path';
17431
				point.shapeArgs = {
17432
					d: series.translatePath(point.path)
17433
				};
17434
				if (supportsVectorEffect) {
17435
					point.shapeArgs['vector-effect'] = 'non-scaling-stroke';
17436
				}
17437
			}
17438
		});
17439
 
17440
		series.translateColors();
17441
	},
17442
 
17443
	/**
17444
	 * Use the drawPoints method of column, that is able to handle simple shapeArgs.
17445
	 * Extend it by assigning the tooltip position.
17446
	 */
17447
	drawPoints: function () {
17448
		var series = this,
17449
			xAxis = series.xAxis,
17450
			yAxis = series.yAxis,
17451
			group = series.group,
17452
			chart = series.chart,
17453
			renderer = chart.renderer,
17454
			scaleX,
17455
			scaleY,
17456
			translateX,
17457
			translateY,
17458
			baseTrans = this.baseTrans;
17459
 
17460
		// Set a group that handles transform during zooming and panning in order to preserve clipping
17461
		// on series.group
17462
		if (!series.transformGroup) {
17463
			series.transformGroup = renderer.g()
17464
				.attr({
17465
					scaleX: 1,
17466
					scaleY: 1
17467
				})
17468
				.add(group);
17469
			series.transformGroup.survive = true;
17470
		}
17471
 
17472
		// Draw the shapes again
17473
		if (series.doFullTranslate()) {
17474
 
17475
			// Individual point actions
17476
			if (chart.hasRendered && series.pointAttrToOptions.fill === 'color') {
17477
				each(series.points, function (point) {
17478
 
17479
					// Reset color on update/redraw
17480
					if (point.shapeArgs) {
17481
						point.shapeArgs.fill = point.pointAttr[pick(point.state, '')].fill; // #3529
17482
					}
17483
				});
17484
			}
17485
 
17486
			// If vector-effect is not supported, we set the stroke-width on the group element
17487
			// and let all point graphics inherit. That way we don't have to iterate over all
17488
			// points to update the stroke-width on zooming.
17489
			if (!supportsVectorEffect) {
17490
				each(series.points, function (point) {
17491
					var attr = point.pointAttr[''];
17492
					if (attr['stroke-width'] === series.pointAttr['']['stroke-width']) {
17493
						attr['stroke-width'] = 'inherit';
17494
					}
17495
				});
17496
			}
17497
 
17498
			// Draw them in transformGroup
17499
			series.group = series.transformGroup;
17500
			seriesTypes.column.prototype.drawPoints.apply(series);
17501
			series.group = group; // Reset
17502
 
17503
			// Add class names
17504
			each(series.points, function (point) {
17505
				if (point.graphic) {
17506
					if (point.name) {
17507
						point.graphic.addClass('highcharts-name-' + point.name.replace(' ', '-').toLowerCase());
17508
					}
17509
					if (point.properties && point.properties['hc-key']) {
17510
						point.graphic.addClass('highcharts-key-' + point.properties['hc-key'].toLowerCase());
17511
					}
17512
 
17513
					if (!supportsVectorEffect) {
17514
						point.graphic['stroke-widthSetter'] = noop;
17515
					}
17516
				}
17517
			});
17518
 
17519
			// Set the base for later scale-zooming. The originX and originY properties are the
17520
			// axis values in the plot area's upper left corner.
17521
			this.baseTrans = {
17522
				originX: xAxis.min - xAxis.minPixelPadding / xAxis.transA,
17523
				originY: yAxis.min - yAxis.minPixelPadding / yAxis.transA + (yAxis.reversed ? 0 : yAxis.len / yAxis.transA),
17524
				transAX: xAxis.transA,
17525
				transAY: yAxis.transA
17526
			};
17527
 
17528
			// Reset transformation in case we're doing a full translate (#3789)
17529
			this.transformGroup.animate({
17530
				translateX: 0,
17531
				translateY: 0,
17532
				scaleX: 1,
17533
				scaleY: 1
17534
			});
17535
 
17536
		// Just update the scale and transform for better performance
17537
		} else {
17538
			scaleX = xAxis.transA / baseTrans.transAX;
17539
			scaleY = yAxis.transA / baseTrans.transAY;
17540
			translateX = xAxis.toPixels(baseTrans.originX, true);
17541
			translateY = yAxis.toPixels(baseTrans.originY, true);
17542
 
17543
			// Handle rounding errors in normal view (#3789)
17544
			if (scaleX > 0.99 && scaleX < 1.01 && scaleY > 0.99 && scaleY < 1.01) {
17545
				scaleX = 1;
17546
				scaleY = 1;
17547
				translateX = Math.round(translateX);
17548
				translateY = Math.round(translateY);
17549
			}
17550
 
17551
			this.transformGroup.animate({
17552
				translateX: translateX,
17553
				translateY: translateY,
17554
				scaleX: scaleX,
17555
				scaleY: scaleY
17556
			});
17557
 
17558
		}
17559
 
17560
		// Set the stroke-width directly on the group element so the children inherit it. We need to use
17561
		// setAttribute directly, because the stroke-widthSetter method expects a stroke color also to be
17562
		// set.
17563
		if (!supportsVectorEffect) {
17564
			series.group.element.setAttribute('stroke-width', series.options.borderWidth / (scaleX || 1));
17565
		}
17566
 
17567
		this.drawMapDataLabels();
17568
 
17569
 
17570
	},
17571
 
17572
	/**
17573
	 * Draw the data labels. Special for maps is the time that the data labels are drawn (after points),
17574
	 * and the clipping of the dataLabelsGroup.
17575
	 */
17576
	drawMapDataLabels: function () {
17577
 
17578
		Series.prototype.drawDataLabels.call(this);
17579
		if (this.dataLabelsGroup) {
17580
			this.dataLabelsGroup.clip(this.chart.clipRect);
17581
		}
17582
	},
17583
 
17584
	/**
17585
	 * Override render to throw in an async call in IE8. Otherwise it chokes on the US counties demo.
17586
	 */
17587
	render: function () {
17588
		var series = this,
17589
			render = Series.prototype.render;
17590
 
17591
		// Give IE8 some time to breathe.
17592
		if (series.chart.renderer.isVML && series.data.length > 3000) {
17593
			setTimeout(function () {
17594
				render.call(series);
17595
			});
17596
		} else {
17597
			render.call(series);
17598
		}
17599
	},
17600
 
17601
	/**
17602
	 * The initial animation for the map series. By default, animation is disabled.
17603
	 * Animation of map shapes is not at all supported in VML browsers.
17604
	 */
17605
	animate: function (init) {
17606
		var chart = this.chart,
17607
			animation = this.options.animation,
17608
			group = this.group,
17609
			xAxis = this.xAxis,
17610
			yAxis = this.yAxis,
17611
			left = xAxis.pos,
17612
			top = yAxis.pos;
17613
 
17614
		if (chart.renderer.isSVG) {
17615
 
17616
			if (animation === true) {
17617
				animation = {
17618
					duration: 1000
17619
				};
17620
			}
17621
 
17622
			// Initialize the animation
17623
			if (init) {
17624
 
17625
				// Scale down the group and place it in the center
17626
				group.attr({
17627
					translateX: left + xAxis.len / 2,
17628
					translateY: top + yAxis.len / 2,
17629
					scaleX: 0.001, // #1499
17630
					scaleY: 0.001
17631
				});
17632
 
17633
			// Run the animation
17634
			} else {
17635
				group.animate({
17636
					translateX: left,
17637
					translateY: top,
17638
					scaleX: 1,
17639
					scaleY: 1
17640
				}, animation);
17641
 
17642
				// Delete this function to allow it only once
17643
				this.animate = null;
17644
			}
17645
		}
17646
	},
17647
 
17648
	/**
17649
	 * Animate in the new series from the clicked point in the old series.
17650
	 * Depends on the drilldown.js module
17651
	 */
17652
	animateDrilldown: function (init) {
17653
		var toBox = this.chart.plotBox,
17654
			level = this.chart.drilldownLevels[this.chart.drilldownLevels.length - 1],
17655
			fromBox = level.bBox,
17656
			animationOptions = this.chart.options.drilldown.animation,
17657
			scale;
17658
 
17659
		if (!init) {
17660
 
17661
			scale = Math.min(fromBox.width / toBox.width, fromBox.height / toBox.height);
17662
			level.shapeArgs = {
17663
				scaleX: scale,
17664
				scaleY: scale,
17665
				translateX: fromBox.x,
17666
				translateY: fromBox.y
17667
			};
17668
 
17669
			// TODO: Animate this.group instead
17670
			each(this.points, function (point) {
17671
				if (point.graphic) {
17672
					point.graphic
17673
						.attr(level.shapeArgs)
17674
						.animate({
17675
							scaleX: 1,
17676
							scaleY: 1,
17677
							translateX: 0,
17678
							translateY: 0
17679
						}, animationOptions);
17680
				}
17681
			});
17682
 
17683
			this.animate = null;
17684
		}
17685
 
17686
	},
17687
 
17688
	drawLegendSymbol: LegendSymbolMixin.drawRectangle,
17689
 
17690
	/**
17691
	 * When drilling up, pull out the individual point graphics from the lower series
17692
	 * and animate them into the origin point in the upper series.
17693
	 */
17694
	animateDrillupFrom: function (level) {
17695
		seriesTypes.column.prototype.animateDrillupFrom.call(this, level);
17696
	},
17697
 
17698
 
17699
	/**
17700
	 * When drilling up, keep the upper series invisible until the lower series has
17701
	 * moved into place
17702
	 */
17703
	animateDrillupTo: function (init) {
17704
		seriesTypes.column.prototype.animateDrillupTo.call(this, init);
17705
	}
17706
}));/**
17707
 * Highmaps JS v1.1.9 (2015-10-07)
17708
 * Highcharts module to hide overlapping data labels. This module is included by default in Highmaps.
17709
 *
17710
 * (c) 2010-2014 Torstein Honsi
17711
 *
17712
 * License: www.highcharts.com/license
17713
 */
17714
 
17715
/*global Highcharts, HighchartsAdapter */
17716
(function (H) {
17717
	var Chart = H.Chart,
17718
		each = H.each,
17719
		pick = H.pick,
17720
		addEvent = HighchartsAdapter.addEvent;
17721
 
17722
	// Collect potensial overlapping data labels. Stack labels probably don't need to be
17723
	// considered because they are usually accompanied by data labels that lie inside the columns.
17724
	Chart.prototype.callbacks.push(function (chart) {
17725
		function collectAndHide() {
17726
			var labels = [];
17727
 
17728
			each(chart.series, function (series) {
17729
				var dlOptions = series.options.dataLabels,
17730
					collections = series.dataLabelCollections || ['dataLabel']; // Range series have two collections
17731
				if ((dlOptions.enabled || series._hasPointLabels) && !dlOptions.allowOverlap && series.visible) { // #3866
17732
					each(collections, function (coll) {
17733
						each(series.points, function (point) {
17734
							if (point[coll]) {
17735
								point[coll].labelrank = pick(point.labelrank, point.shapeArgs && point.shapeArgs.height); // #4118
17736
								labels.push(point[coll]);
17737
							}
17738
						});
17739
					});
17740
				}
17741
			});
17742
			chart.hideOverlappingLabels(labels);
17743
		}
17744
 
17745
		// Do it now ...
17746
		collectAndHide();
17747
 
17748
		// ... and after each chart redraw
17749
		addEvent(chart, 'redraw', collectAndHide);
17750
 
17751
	});
17752
 
17753
	/**
17754
	 * Hide overlapping labels. Labels are moved and faded in and out on zoom to provide a smooth
17755
	 * visual imression.
17756
	 */
17757
	Chart.prototype.hideOverlappingLabels = function (labels) {
17758
 
17759
		var len = labels.length,
17760
			label,
17761
			i,
17762
			j,
17763
			label1,
17764
			label2,
17765
			isIntersecting,
17766
			pos1,
17767
			pos2,
17768
			padding,
17769
			intersectRect = function (x1, y1, w1, h1, x2, y2, w2, h2) {
17770
				return !(
17771
					x2 > x1 + w1 ||
17772
					x2 + w2 < x1 ||
17773
					y2 > y1 + h1 ||
17774
					y2 + h2 < y1
17775
				);
17776
			};
17777
 
17778
		// Mark with initial opacity
17779
		for (i = 0; i < len; i++) {
17780
			label = labels[i];
17781
			if (label) {
17782
				label.oldOpacity = label.opacity;
17783
				label.newOpacity = 1;
17784
			}
17785
		}
17786
 
17787
		// Prevent a situation in a gradually rising slope, that each label
17788
		// will hide the previous one because the previous one always has
17789
		// lower rank.
17790
		labels.sort(function (a, b) {
17791
			return (b.labelrank || 0) - (a.labelrank || 0);
17792
		});
17793
 
17794
		// Detect overlapping labels
17795
		for (i = 0; i < len; i++) {
17796
			label1 = labels[i];
17797
 
17798
			for (j = i + 1; j < len; ++j) {
17799
				label2 = labels[j];
17800
				if (label1 && label2 && label1.placed && label2.placed && label1.newOpacity !== 0 && label2.newOpacity !== 0) {
17801
					pos1 = label1.alignAttr;
17802
					pos2 = label2.alignAttr;
17803
					padding = 2 * (label1.box ? 0 : label1.padding); // Substract the padding if no background or border (#4333)
17804
					isIntersecting = intersectRect(
17805
						pos1.x,
17806
						pos1.y,
17807
						label1.width - padding,
17808
						label1.height - padding,
17809
						pos2.x,
17810
						pos2.y,
17811
						label2.width - padding,
17812
						label2.height - padding
17813
					);
17814
 
17815
					if (isIntersecting) {
17816
						(label1.labelrank < label2.labelrank ? label1 : label2).newOpacity = 0;
17817
					}
17818
				}
17819
			}
17820
		}
17821
 
17822
		// Hide or show
17823
		each(labels, function (label) {
17824
			var complete,
17825
				newOpacity;
17826
 
17827
			if (label) {
17828
				newOpacity = label.newOpacity;
17829
 
17830
				if (label.oldOpacity !== newOpacity && label.placed) {
17831
 
17832
					// Make sure the label is completely hidden to avoid catching clicks (#4362)
17833
					if (newOpacity) {
17834
						label.show(true);
17835
					} else {
17836
						complete = function () {
17837
							label.hide();
17838
						};
17839
					}
17840
 
17841
					// Animate or set the opacity
17842
					label.alignAttr.opacity = newOpacity;
17843
					label[label.isOld ? 'animate' : 'attr'](label.alignAttr, null, complete);
17844
 
17845
				}
17846
				label.isOld = true;
17847
			}
17848
		});
17849
	};
17850
 
17851
}(Highcharts));
17852
// Add events to the Chart object itself
17853
extend(Chart.prototype, {
17854
	renderMapNavigation: function () {
17855
		var chart = this,
17856
			options = this.options.mapNavigation,
17857
			buttons = options.buttons,
17858
			n,
17859
			button,
17860
			buttonOptions,
17861
			attr,
17862
			states,
17863
			stopEvent = function (e) {
17864
				if (e) {
17865
					if (e.preventDefault) {
17866
						e.preventDefault();
17867
					}
17868
					if (e.stopPropagation) {
17869
						e.stopPropagation();
17870
					}
17871
					e.cancelBubble = true;
17872
				}
17873
			},
17874
			outerHandler = function (e) {
17875
				this.handler.call(chart, e);
17876
				stopEvent(e); // Stop default click event (#4444)
17877
			};
17878
 
17879
		if (pick(options.enableButtons, options.enabled) && !chart.renderer.forExport) {
17880
			for (n in buttons) {
17881
				if (buttons.hasOwnProperty(n)) {
17882
					buttonOptions = merge(options.buttonOptions, buttons[n]);
17883
					attr = buttonOptions.theme;
17884
					attr.style = merge(buttonOptions.theme.style, buttonOptions.style); // #3203
17885
					states = attr.states;
17886
					button = chart.renderer.button(
17887
							buttonOptions.text,
17888
							0,
17889
							0,
17890
							outerHandler,
17891
							attr,
17892
							states && states.hover,
17893
							states && states.select,
17894
							0,
17895
							n === 'zoomIn' ? 'topbutton' : 'bottombutton'
17896
						)
17897
						.attr({
17898
							width: buttonOptions.width,
17899
							height: buttonOptions.height,
17900
							title: chart.options.lang[n],
17901
							zIndex: 5
17902
						})
17903
						.add();
17904
					button.handler = buttonOptions.onclick;
17905
					button.align(extend(buttonOptions, { width: button.width, height: 2 * button.height }), null, buttonOptions.alignTo);
17906
					addEvent(button.element, 'dblclick', stopEvent); // Stop double click event (#4444)
17907
				}
17908
			}
17909
		}
17910
	},
17911
 
17912
	/**
17913
	 * Fit an inner box to an outer. If the inner box overflows left or right, align it to the sides of the
17914
	 * outer. If it overflows both sides, fit it within the outer. This is a pattern that occurs more places
17915
	 * in Highcharts, perhaps it should be elevated to a common utility function.
17916
	 */
17917
	fitToBox: function (inner, outer) {
17918
		each([['x', 'width'], ['y', 'height']], function (dim) {
17919
			var pos = dim[0],
17920
				size = dim[1];
17921
 
17922
			if (inner[pos] + inner[size] > outer[pos] + outer[size]) { // right overflow
17923
				if (inner[size] > outer[size]) { // the general size is greater, fit fully to outer
17924
					inner[size] = outer[size];
17925
					inner[pos] = outer[pos];
17926
				} else { // align right
17927
					inner[pos] = outer[pos] + outer[size] - inner[size];
17928
				}
17929
			}
17930
			if (inner[size] > outer[size]) {
17931
				inner[size] = outer[size];
17932
			}
17933
			if (inner[pos] < outer[pos]) {
17934
				inner[pos] = outer[pos];
17935
			}
17936
		});
17937
 
17938
 
17939
		return inner;
17940
	},
17941
 
17942
	/**
17943
	 * Zoom the map in or out by a certain amount. Less than 1 zooms in, greater than 1 zooms out.
17944
	 */
17945
	mapZoom: function (howMuch, centerXArg, centerYArg, mouseX, mouseY) {
17946
		/*if (this.isMapZooming) {
17947
			this.mapZoomQueue = arguments;
17948
			return;
17949
		}*/
17950
 
17951
		var chart = this,
17952
			xAxis = chart.xAxis[0],
17953
			xRange = xAxis.max - xAxis.min,
17954
			centerX = pick(centerXArg, xAxis.min + xRange / 2),
17955
			newXRange = xRange * howMuch,
17956
			yAxis = chart.yAxis[0],
17957
			yRange = yAxis.max - yAxis.min,
17958
			centerY = pick(centerYArg, yAxis.min + yRange / 2),
17959
			newYRange = yRange * howMuch,
17960
			fixToX = mouseX ? ((mouseX - xAxis.pos) / xAxis.len) : 0.5,
17961
			fixToY = mouseY ? ((mouseY - yAxis.pos) / yAxis.len) : 0.5,
17962
			newXMin = centerX - newXRange * fixToX,
17963
			newYMin = centerY - newYRange * fixToY,
17964
			newExt = chart.fitToBox({
17965
				x: newXMin,
17966
				y: newYMin,
17967
				width: newXRange,
17968
				height: newYRange
17969
			}, {
17970
				x: xAxis.dataMin,
17971
				y: yAxis.dataMin,
17972
				width: xAxis.dataMax - xAxis.dataMin,
17973
				height: yAxis.dataMax - yAxis.dataMin
17974
			});
17975
 
17976
		// When mousewheel zooming, fix the point under the mouse
17977
		if (mouseX) {
17978
			xAxis.fixTo = [mouseX - xAxis.pos, centerXArg];
17979
		}
17980
		if (mouseY) {
17981
			yAxis.fixTo = [mouseY - yAxis.pos, centerYArg];
17982
		}
17983
 
17984
		// Zoom
17985
		if (howMuch !== undefined) {
17986
			xAxis.setExtremes(newExt.x, newExt.x + newExt.width, false);
17987
			yAxis.setExtremes(newExt.y, newExt.y + newExt.height, false);
17988
 
17989
		// Reset zoom
17990
		} else {
17991
			xAxis.setExtremes(undefined, undefined, false);
17992
			yAxis.setExtremes(undefined, undefined, false);
17993
		}
17994
 
17995
		// Prevent zooming until this one is finished animating
17996
		/*chart.holdMapZoom = true;
17997
		setTimeout(function () {
17998
			chart.holdMapZoom = false;
17999
		}, 200);*/
18000
		/*delay = animation ? animation.duration || 500 : 0;
18001
		if (delay) {
18002
			chart.isMapZooming = true;
18003
			setTimeout(function () {
18004
				chart.isMapZooming = false;
18005
				if (chart.mapZoomQueue) {
18006
					chart.mapZoom.apply(chart, chart.mapZoomQueue);
18007
				}
18008
				chart.mapZoomQueue = null;
18009
			}, delay);
18010
		}*/
18011
 
18012
		chart.redraw();
18013
	}
18014
});
18015
 
18016
/**
18017
 * Extend the Chart.render method to add zooming and panning
18018
 */
18019
wrap(Chart.prototype, 'render', function (proceed) {
18020
	var chart = this,
18021
		mapNavigation = chart.options.mapNavigation;
18022
 
18023
	// Render the plus and minus buttons. Doing this before the shapes makes getBBox much quicker, at least in Chrome.
18024
	chart.renderMapNavigation();
18025
 
18026
	proceed.call(chart);
18027
 
18028
	// Add the double click event
18029
	if (pick(mapNavigation.enableDoubleClickZoom, mapNavigation.enabled) || mapNavigation.enableDoubleClickZoomTo) {
18030
		addEvent(chart.container, 'dblclick', function (e) {
18031
			chart.pointer.onContainerDblClick(e);
18032
		});
18033
	}
18034
 
18035
	// Add the mousewheel event
18036
	if (pick(mapNavigation.enableMouseWheelZoom, mapNavigation.enabled)) {
18037
		addEvent(chart.container, document.onmousewheel === undefined ? 'DOMMouseScroll' : 'mousewheel', function (e) {
18038
			chart.pointer.onContainerMouseWheel(e);
18039
			return false;
18040
		});
18041
	}
18042
});
18043
 
18044
// Extend the Pointer
18045
extend(Pointer.prototype, {
18046
 
18047
	/**
18048
	 * The event handler for the doubleclick event
18049
	 */
18050
	onContainerDblClick: function (e) {
18051
		var chart = this.chart;
18052
 
18053
		e = this.normalize(e);
18054
 
18055
		if (chart.options.mapNavigation.enableDoubleClickZoomTo) {
18056
			if (chart.pointer.inClass(e.target, 'highcharts-tracker')) {
18057
				chart.hoverPoint.zoomTo();
18058
			}
18059
		} else if (chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) {
18060
			chart.mapZoom(
18061
				0.5,
18062
				chart.xAxis[0].toValue(e.chartX),
18063
				chart.yAxis[0].toValue(e.chartY),
18064
				e.chartX,
18065
				e.chartY
18066
			);
18067
		}
18068
	},
18069
 
18070
	/**
18071
	 * The event handler for the mouse scroll event
18072
	 */
18073
	onContainerMouseWheel: function (e) {
18074
		var chart = this.chart,
18075
			delta;
18076
 
18077
		e = this.normalize(e);
18078
 
18079
		// Firefox uses e.detail, WebKit and IE uses wheelDelta
18080
		delta = e.detail || -(e.wheelDelta / 120);
18081
		if (chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) {
18082
			chart.mapZoom(
18083
				//delta > 0 ? 2 : 0.5,
18084
				Math.pow(2, delta),
18085
				chart.xAxis[0].toValue(e.chartX),
18086
				chart.yAxis[0].toValue(e.chartY),
18087
				e.chartX,
18088
				e.chartY
18089
			);
18090
		}
18091
	}
18092
});
18093
 
18094
// Implement the pinchType option
18095
wrap(Pointer.prototype, 'init', function (proceed, chart, options) {
18096
 
18097
	proceed.call(this, chart, options);
18098
 
18099
	// Pinch status
18100
	if (pick(options.mapNavigation.enableTouchZoom, options.mapNavigation.enabled)) {
18101
		this.pinchX = this.pinchHor = this.pinchY = this.pinchVert = this.hasZoom = true;
18102
	}
18103
});
18104
 
18105
// Extend the pinchTranslate method to preserve fixed ratio when zooming
18106
wrap(Pointer.prototype, 'pinchTranslate', function (proceed, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch) {
18107
	var xBigger;
18108
	proceed.call(this, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch);
18109
 
18110
	// Keep ratio
18111
	if (this.chart.options.chart.type === 'map' && this.hasZoom) {
18112
		xBigger = transform.scaleX > transform.scaleY;
18113
		this.pinchTranslateDirection(
18114
			!xBigger,
18115
			pinchDown,
18116
			touches,
18117
			transform,
18118
			selectionMarker,
18119
			clip,
18120
			lastValidTouch,
18121
			xBigger ? transform.scaleX : transform.scaleY
18122
		);
18123
	}
18124
});
18125
 
18126
 
18127
 
18128
 
18129
// The mapline series type
18130
defaultPlotOptions.mapline = merge(defaultPlotOptions.map, {
18131
	lineWidth: 1,
18132
	fillColor: 'none'
18133
});
18134
seriesTypes.mapline = extendClass(seriesTypes.map, {
18135
	type: 'mapline',
18136
	pointAttrToOptions: { // mapping between SVG attributes and the corresponding options
18137
		stroke: 'color',
18138
		'stroke-width': 'lineWidth',
18139
		fill: 'fillColor',
18140
		dashstyle: 'dashStyle'
18141
	},
18142
	drawLegendSymbol: seriesTypes.line.prototype.drawLegendSymbol
18143
});
18144
 
18145
// The mappoint series type
18146
defaultPlotOptions.mappoint = merge(defaultPlotOptions.scatter, {
18147
	dataLabels: {
18148
		enabled: true,
18149
		formatter: function () { // #2945
18150
			return this.point.name;
18151
		},
18152
		crop: false,
18153
		defer: false,
18154
		overflow: false,
18155
		style: {
18156
			color: '#000000'
18157
		}
18158
	}
18159
});
18160
seriesTypes.mappoint = extendClass(seriesTypes.scatter, {
18161
	type: 'mappoint',
18162
	forceDL: true,
18163
	pointClass: extendClass(Point, {
18164
		applyOptions: function (options, x) {
18165
			var point = Point.prototype.applyOptions.call(this, options, x);
18166
			if (options.lat !== undefined && options.lon !== undefined) {
18167
				point = extend(point, this.series.chart.fromLatLonToPoint(point));
18168
			}
18169
			return point;
18170
		}
18171
	})
18172
});/* ****************************************************************************
18173
 * Start Bubble series code											          *
18174
 *****************************************************************************/
18175
 
18176
// 1 - set default options
18177
defaultPlotOptions.bubble = merge(defaultPlotOptions.scatter, {
18178
	dataLabels: {
18179
		formatter: function () { // #2945
18180
			return this.point.z;
18181
		},
18182
		inside: true,
18183
		verticalAlign: 'middle'
18184
	},
18185
	// displayNegative: true,
18186
	marker: {
18187
		// fillOpacity: 0.5,
18188
		lineColor: null, // inherit from series.color
18189
		lineWidth: 1
18190
	},
18191
	minSize: 8,
18192
	maxSize: '20%',
18193
	// negativeColor: null,
18194
	// sizeBy: 'area'
18195
	softThreshold: false,
18196
	states: {
18197
		hover: {
18198
			halo: {
18199
				size: 5
18200
			}
18201
		}
18202
	},
18203
	tooltip: {
18204
		pointFormat: '({point.x}, {point.y}), Size: {point.z}'
18205
	},
18206
	turboThreshold: 0,
18207
	zThreshold: 0,
18208
	zoneAxis: 'z'
18209
});
18210
 
18211
var BubblePoint = extendClass(Point, {
18212
	haloPath: function () {
18213
		return Point.prototype.haloPath.call(this, this.shapeArgs.r + this.series.options.states.hover.halo.size);
18214
	},
18215
	ttBelow: false
18216
});
18217
 
18218
// 2 - Create the series object
18219
seriesTypes.bubble = extendClass(seriesTypes.scatter, {
18220
	type: 'bubble',
18221
	pointClass: BubblePoint,
18222
	pointArrayMap: ['y', 'z'],
18223
	parallelArrays: ['x', 'y', 'z'],
18224
	trackerGroups: ['group', 'dataLabelsGroup'],
18225
	bubblePadding: true,
18226
	zoneAxis: 'z',
18227
 
18228
	/**
18229
	 * Mapping between SVG attributes and the corresponding options
18230
	 */
18231
	pointAttrToOptions: {
18232
		stroke: 'lineColor',
18233
		'stroke-width': 'lineWidth',
18234
		fill: 'fillColor'
18235
	},
18236
 
18237
	/**
18238
	 * Apply the fillOpacity to all fill positions
18239
	 */
18240
	applyOpacity: function (fill) {
18241
		var markerOptions = this.options.marker,
18242
			fillOpacity = pick(markerOptions.fillOpacity, 0.5);
18243
 
18244
		// When called from Legend.colorizeItem, the fill isn't predefined
18245
		fill = fill || markerOptions.fillColor || this.color;
18246
 
18247
		if (fillOpacity !== 1) {
18248
			fill = Color(fill).setOpacity(fillOpacity).get('rgba');
18249
		}
18250
		return fill;
18251
	},
18252
 
18253
	/**
18254
	 * Extend the convertAttribs method by applying opacity to the fill
18255
	 */
18256
	convertAttribs: function () {
18257
		var obj = Series.prototype.convertAttribs.apply(this, arguments);
18258
 
18259
		obj.fill = this.applyOpacity(obj.fill);
18260
 
18261
		return obj;
18262
	},
18263
 
18264
	/**
18265
	 * Get the radius for each point based on the minSize, maxSize and each point's Z value. This
18266
	 * must be done prior to Series.translate because the axis needs to add padding in
18267
	 * accordance with the point sizes.
18268
	 */
18269
	getRadii: function (zMin, zMax, minSize, maxSize) {
18270
		var len,
18271
			i,
18272
			pos,
18273
			zData = this.zData,
18274
			radii = [],
18275
			options = this.options,
18276
			sizeByArea = options.sizeBy !== 'width',
18277
			zThreshold = options.zThreshold,
18278
			zRange = zMax - zMin,
18279
			value,
18280
			radius;
18281
 
18282
		// Set the shape type and arguments to be picked up in drawPoints
18283
		for (i = 0, len = zData.length; i < len; i++) {
18284
 
18285
			value = zData[i];
18286
 
18287
			// When sizing by threshold, the absolute value of z determines the size
18288
			// of the bubble.
18289
			if (options.sizeByAbsoluteValue) {
18290
				value = Math.abs(value - zThreshold);
18291
				zMax = Math.max(zMax - zThreshold, Math.abs(zMin - zThreshold));
18292
				zMin = 0;
18293
			}
18294
 
18295
			if (value === null) {
18296
				radius = null;
18297
			// Issue #4419 - if value is less than zMin, push a radius that's always smaller than the minimum size
18298
			} else if (value < zMin) {
18299
				radius = minSize / 2 - 1;
18300
			} else {
18301
				// Relative size, a number between 0 and 1
18302
				pos = zRange > 0 ? (value - zMin) / zRange : 0.5;
18303
 
18304
				if (sizeByArea && pos >= 0) {
18305
					pos = Math.sqrt(pos);
18306
				}
18307
				radius = math.ceil(minSize + pos * (maxSize - minSize)) / 2;
18308
			}
18309
			radii.push(radius);
18310
		}
18311
		this.radii = radii;
18312
	},
18313
 
18314
	/**
18315
	 * Perform animation on the bubbles
18316
	 */
18317
	animate: function (init) {
18318
		var animation = this.options.animation;
18319
 
18320
		if (!init) { // run the animation
18321
			each(this.points, function (point) {
18322
				var graphic = point.graphic,
18323
					shapeArgs = point.shapeArgs;
18324
 
18325
				if (graphic && shapeArgs) {
18326
					// start values
18327
					graphic.attr('r', 1);
18328
 
18329
					// animate
18330
					graphic.animate({
18331
						r: shapeArgs.r
18332
					}, animation);
18333
				}
18334
			});
18335
 
18336
			// delete this function to allow it only once
18337
			this.animate = null;
18338
		}
18339
	},
18340
 
18341
	/**
18342
	 * Extend the base translate method to handle bubble size
18343
	 */
18344
	translate: function () {
18345
 
18346
		var i,
18347
			data = this.data,
18348
			point,
18349
			radius,
18350
			radii = this.radii;
18351
 
18352
		// Run the parent method
18353
		seriesTypes.scatter.prototype.translate.call(this);
18354
 
18355
		// Set the shape type and arguments to be picked up in drawPoints
18356
		i = data.length;
18357
 
18358
		while (i--) {
18359
			point = data[i];
18360
			radius = radii ? radii[i] : 0; // #1737
18361
 
18362
			if (typeof radius === 'number' && radius >= this.minPxSize / 2) {
18363
				// Shape arguments
18364
				point.shapeType = 'circle';
18365
				point.shapeArgs = {
18366
					x: point.plotX,
18367
					y: point.plotY,
18368
					r: radius
18369
				};
18370
 
18371
				// Alignment box for the data label
18372
				point.dlBox = {
18373
					x: point.plotX - radius,
18374
					y: point.plotY - radius,
18375
					width: 2 * radius,
18376
					height: 2 * radius
18377
				};
18378
			} else { // below zThreshold or z = null
18379
				point.shapeArgs = point.plotY = point.dlBox = UNDEFINED; // #1691
18380
			}
18381
		}
18382
	},
18383
 
18384
	/**
18385
	 * Get the series' symbol in the legend
18386
	 *
18387
	 * @param {Object} legend The legend object
18388
	 * @param {Object} item The series (this) or point
18389
	 */
18390
	drawLegendSymbol: function (legend, item) {
18391
		var radius = pInt(legend.itemStyle.fontSize) / 2;
18392
 
18393
		item.legendSymbol = this.chart.renderer.circle(
18394
			radius,
18395
			legend.baseline - radius,
18396
			radius
18397
		).attr({
18398
			zIndex: 3
18399
		}).add(item.legendGroup);
18400
		item.legendSymbol.isMarker = true;
18401
 
18402
	},
18403
 
18404
	drawPoints: seriesTypes.column.prototype.drawPoints,
18405
	alignDataLabel: seriesTypes.column.prototype.alignDataLabel,
18406
	buildKDTree: noop,
18407
	applyZones: noop
18408
});
18409
 
18410
/**
18411
 * Add logic to pad each axis with the amount of pixels
18412
 * necessary to avoid the bubbles to overflow.
18413
 */
18414
Axis.prototype.beforePadding = function () {
18415
	var axis = this,
18416
		axisLength = this.len,
18417
		chart = this.chart,
18418
		pxMin = 0,
18419
		pxMax = axisLength,
18420
		isXAxis = this.isXAxis,
18421
		dataKey = isXAxis ? 'xData' : 'yData',
18422
		min = this.min,
18423
		extremes = {},
18424
		smallestSize = math.min(chart.plotWidth, chart.plotHeight),
18425
		zMin = Number.MAX_VALUE,
18426
		zMax = -Number.MAX_VALUE,
18427
		range = this.max - min,
18428
		transA = axisLength / range,
18429
		activeSeries = [];
18430
 
18431
	// Handle padding on the second pass, or on redraw
18432
	each(this.series, function (series) {
18433
 
18434
		var seriesOptions = series.options,
18435
			zData;
18436
 
18437
		if (series.bubblePadding && (series.visible || !chart.options.chart.ignoreHiddenSeries)) {
18438
 
18439
			// Correction for #1673
18440
			axis.allowZoomOutside = true;
18441
 
18442
			// Cache it
18443
			activeSeries.push(series);
18444
 
18445
			if (isXAxis) { // because X axis is evaluated first
18446
 
18447
				// For each series, translate the size extremes to pixel values
18448
				each(['minSize', 'maxSize'], function (prop) {
18449
					var length = seriesOptions[prop],
18450
						isPercent = /%$/.test(length);
18451
 
18452
					length = pInt(length);
18453
					extremes[prop] = isPercent ?
18454
						smallestSize * length / 100 :
18455
						length;
18456
 
18457
				});
18458
				series.minPxSize = extremes.minSize;
18459
				series.maxPxSize = extremes.maxSize;
18460
 
18461
				// Find the min and max Z
18462
				zData = series.zData;
18463
				if (zData.length) { // #1735
18464
					zMin = pick(seriesOptions.zMin, math.min(
18465
						zMin,
18466
						math.max(
18467
							arrayMin(zData),
18468
							seriesOptions.displayNegative === false ? seriesOptions.zThreshold : -Number.MAX_VALUE
18469
						)
18470
					));
18471
					zMax = pick(seriesOptions.zMax, math.max(zMax, arrayMax(zData)));
18472
				}
18473
			}
18474
		}
18475
	});
18476
 
18477
	each(activeSeries, function (series) {
18478
 
18479
		var data = series[dataKey],
18480
			i = data.length,
18481
			radius;
18482
 
18483
		if (isXAxis) {
18484
			series.getRadii(zMin, zMax, series.minPxSize, series.maxPxSize);
18485
		}
18486
 
18487
		if (range > 0) {
18488
			while (i--) {
18489
				if (typeof data[i] === 'number') {
18490
					radius = series.radii[i];
18491
					pxMin = Math.min(((data[i] - min) * transA) - radius, pxMin);
18492
					pxMax = Math.max(((data[i] - min) * transA) + radius, pxMax);
18493
				}
18494
			}
18495
		}
18496
	});
18497
 
18498
 
18499
	if (activeSeries.length && range > 0 && !this.isLog) {
18500
		pxMax -= axisLength;
18501
		transA *= (axisLength + pxMin - pxMax) / axisLength;
18502
		each([['min', 'userMin', pxMin], ['max', 'userMax', pxMax]], function (keys) {
18503
			if (pick(axis.options[keys[0]], axis[keys[1]]) === UNDEFINED) {
18504
				axis[keys[0]] += keys[2] / transA;
18505
			}
18506
		});
18507
	}
18508
};
18509
 
18510
/* ****************************************************************************
18511
 * End Bubble series code                                                     *
18512
 *****************************************************************************/
18513
 
18514
 
18515
// The mapbubble series type
18516
if (seriesTypes.bubble) {
18517
 
18518
	defaultPlotOptions.mapbubble = merge(defaultPlotOptions.bubble, {
18519
		animationLimit: 500,
18520
		tooltip: {
18521
			pointFormat: '{point.name}: {point.z}'
18522
		}
18523
	});
18524
	seriesTypes.mapbubble = extendClass(seriesTypes.bubble, {
18525
		pointClass: extendClass(Point, {
18526
			applyOptions: function (options, x) {
18527
				var point;
18528
				if (options && options.lat !== undefined && options.lon !== undefined) {
18529
					point = Point.prototype.applyOptions.call(this, options, x);
18530
					point = extend(point, this.series.chart.fromLatLonToPoint(point));
18531
				} else {
18532
					point = MapAreaPoint.prototype.applyOptions.call(this, options, x);
18533
				}
18534
				return point;
18535
			},
18536
			ttBelow: false
18537
		}),
18538
		xyFromShape: true,
18539
		type: 'mapbubble',
18540
		pointArrayMap: ['z'], // If one single value is passed, it is interpreted as z
18541
		/**
18542
		 * Return the map area identified by the dataJoinBy option
18543
		 */
18544
		getMapData: seriesTypes.map.prototype.getMapData,
18545
		getBox: seriesTypes.map.prototype.getBox,
18546
		setData: seriesTypes.map.prototype.setData
18547
	});
18548
}
18549
 
18550
/**
18551
 * Test for point in polygon. Polygon defined as array of [x,y] points.
18552
 */
18553
function pointInPolygon(point, polygon) {
18554
	var i, j, rel1, rel2, c = false,
18555
		x = point.x,
18556
		y = point.y;
18557
 
18558
	for (i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
18559
		rel1 = polygon[i][1] > y;
18560
		rel2 = polygon[j][1] > y;
18561
		if (rel1 !== rel2 && (x < (polygon[j][0] - polygon[i][0]) * (y - polygon[i][1]) / (polygon[j][1] - polygon[i][1]) + polygon[i][0])) {
18562
			c = !c;
18563
		}
18564
	}
18565
 
18566
	return c;
18567
}
18568
 
18569
/**
18570
 * Get point from latLon using specified transform definition
18571
 */
18572
Chart.prototype.transformFromLatLon = function (latLon, transform) {
18573
	if (window.proj4 === undefined) {
18574
		error(21);
18575
		return {
18576
			x: 0,
18577
			y: null
18578
		};
18579
	}
18580
 
18581
	var projected = window.proj4(transform.crs, [latLon.lon, latLon.lat]),
18582
		cosAngle = transform.cosAngle || (transform.rotation && Math.cos(transform.rotation)),
18583
		sinAngle = transform.sinAngle || (transform.rotation && Math.sin(transform.rotation)),
18584
		rotated = transform.rotation ? [projected[0] * cosAngle + projected[1] * sinAngle, -projected[0] * sinAngle + projected[1] * cosAngle] : projected;
18585
 
18586
	return {
18587
		x: ((rotated[0] - (transform.xoffset || 0)) * (transform.scale || 1) + (transform.xpan || 0)) * (transform.jsonres || 1) + (transform.jsonmarginX || 0),
18588
		y: (((transform.yoffset || 0) - rotated[1]) * (transform.scale || 1) + (transform.ypan || 0)) * (transform.jsonres || 1) - (transform.jsonmarginY || 0)
18589
	};
18590
};
18591
 
18592
/**
18593
 * Get latLon from point using specified transform definition
18594
 */
18595
Chart.prototype.transformToLatLon = function (point, transform) {
18596
	if (window.proj4 === undefined) {
18597
		error(21);
18598
		return;
18599
	}
18600
 
18601
	var normalized = {
18602
			x: ((point.x - (transform.jsonmarginX || 0)) / (transform.jsonres || 1) - (transform.xpan || 0)) / (transform.scale || 1) + (transform.xoffset || 0),
18603
			y: ((-point.y - (transform.jsonmarginY || 0)) / (transform.jsonres || 1) + (transform.ypan || 0)) / (transform.scale || 1) + (transform.yoffset || 0)
18604
		},
18605
		cosAngle = transform.cosAngle || (transform.rotation && Math.cos(transform.rotation)),
18606
		sinAngle = transform.sinAngle || (transform.rotation && Math.sin(transform.rotation)),
18607
		// Note: Inverted sinAngle to reverse rotation direction
18608
		projected = window.proj4(transform.crs, 'WGS84', transform.rotation ? {
18609
			x: normalized.x * cosAngle + normalized.y * -sinAngle,
18610
			y: normalized.x * sinAngle + normalized.y * cosAngle
18611
		} : normalized);
18612
 
18613
	return {lat: projected.y, lon: projected.x};
18614
};
18615
 
18616
Chart.prototype.fromPointToLatLon = function (point) {
18617
	var transforms = this.mapTransforms,
18618
		transform;
18619
 
18620
	if (!transforms) {
18621
		error(22);
18622
		return;
18623
	}
18624
 
18625
	for (transform in transforms) {
18626
		if (transforms.hasOwnProperty(transform) && transforms[transform].hitZone && pointInPolygon({x: point.x, y: -point.y}, transforms[transform].hitZone.coordinates[0])) {
18627
			return this.transformToLatLon(point, transforms[transform]);
18628
		}
18629
	}
18630
 
18631
	return this.transformToLatLon(point, transforms['default']);
18632
};
18633
 
18634
Chart.prototype.fromLatLonToPoint = function (latLon) {
18635
	var transforms = this.mapTransforms,
18636
		transform,
18637
		coords;
18638
 
18639
	if (!transforms) {
18640
		error(22);
18641
		return {
18642
			x: 0,
18643
			y: null
18644
		};
18645
	}
18646
 
18647
	for (transform in transforms) {
18648
		if (transforms.hasOwnProperty(transform) && transforms[transform].hitZone) {
18649
			coords = this.transformFromLatLon(latLon, transforms[transform]);
18650
			if (pointInPolygon({x: coords.x, y: -coords.y}, transforms[transform].hitZone.coordinates[0])) {
18651
				return coords;
18652
			}
18653
		}
18654
	}
18655
 
18656
	return this.transformFromLatLon(latLon, transforms['default']);
18657
};
18658
 
18659
/**
18660
 * Convert a geojson object to map data of a given Highcharts type (map, mappoint or mapline).
18661
 */
18662
Highcharts.geojson = function (geojson, hType, series) {
18663
	var mapData = [],
18664
		path = [],
18665
		polygonToPath = function (polygon) {
18666
			var i = 0,
18667
				len = polygon.length;
18668
			path.push('M');
18669
			for (; i < len; i++) {
18670
				if (i === 1) {
18671
					path.push('L');
18672
				}
18673
				path.push(polygon[i][0], -polygon[i][1]);
18674
			}
18675
		};
18676
 
18677
	hType = hType || 'map';
18678
 
18679
	each(geojson.features, function (feature) {
18680
 
18681
		var geometry = feature.geometry,
18682
			type = geometry.type,
18683
			coordinates = geometry.coordinates,
18684
			properties = feature.properties,
18685
			point;
18686
 
18687
		path = [];
18688
 
18689
		if (hType === 'map' || hType === 'mapbubble') {
18690
			if (type === 'Polygon') {
18691
				each(coordinates, polygonToPath);
18692
				path.push('Z');
18693
 
18694
			} else if (type === 'MultiPolygon') {
18695
				each(coordinates, function (items) {
18696
					each(items, polygonToPath);
18697
				});
18698
				path.push('Z');
18699
			}
18700
 
18701
			if (path.length) {
18702
				point = { path: path };
18703
			}
18704
 
18705
		} else if (hType === 'mapline') {
18706
			if (type === 'LineString') {
18707
				polygonToPath(coordinates);
18708
			} else if (type === 'MultiLineString') {
18709
				each(coordinates, polygonToPath);
18710
			}
18711
 
18712
			if (path.length) {
18713
				point = { path: path };
18714
			}
18715
 
18716
		} else if (hType === 'mappoint') {
18717
			if (type === 'Point') {
18718
				point = {
18719
					x: coordinates[0],
18720
					y: -coordinates[1]
18721
				};
18722
			}
18723
		}
18724
		if (point) {
18725
			mapData.push(extend(point, {
18726
				name: properties.name || properties.NAME,
18727
				properties: properties
18728
			}));
18729
		}
18730
 
18731
	});
18732
 
18733
	// Create a credits text that includes map source, to be picked up in Chart.showCredits
18734
	if (series && geojson.copyrightShort) {
18735
		series.chart.mapCredits = '<a href="http://www.highcharts.com">Highcharts</a> \u00A9 ' +
18736
			'<a href="' + geojson.copyrightUrl + '">' + geojson.copyrightShort + '</a>';
18737
		series.chart.mapCreditsFull = geojson.copyright;
18738
	}
18739
 
18740
	return mapData;
18741
};
18742
 
18743
/**
18744
 * Override showCredits to include map source by default
18745
 */
18746
wrap(Chart.prototype, 'showCredits', function (proceed, credits) {
18747
 
18748
	if (defaultOptions.credits.text === this.options.credits.text && this.mapCredits) { // default text and mapCredits is set
18749
		credits.text = this.mapCredits;
18750
		credits.href = null;
18751
	}
18752
 
18753
	proceed.call(this, credits);
18754
 
18755
	if (this.credits) {
18756
		this.credits.attr({
18757
			title: this.mapCreditsFull
18758
		});
18759
	}
18760
});
18761
 
18762
// Add language
18763
extend(defaultOptions.lang, {
18764
	zoomIn: 'Zoom in',
18765
	zoomOut: 'Zoom out'
18766
});
18767
 
18768
 
18769
// Set the default map navigation options
18770
defaultOptions.mapNavigation = {
18771
	buttonOptions: {
18772
		alignTo: 'plotBox',
18773
		align: 'left',
18774
		verticalAlign: 'top',
18775
		x: 0,
18776
		width: 18,
18777
		height: 18,
18778
		style: {
18779
			fontSize: '15px',
18780
			fontWeight: 'bold',
18781
			textAlign: 'center'
18782
		},
18783
		theme: {
18784
			'stroke-width': 1
18785
		}
18786
	},
18787
	buttons: {
18788
		zoomIn: {
18789
			onclick: function () {
18790
				this.mapZoom(0.5);
18791
			},
18792
			text: '+',
18793
			y: 0
18794
		},
18795
		zoomOut: {
18796
			onclick: function () {
18797
				this.mapZoom(2);
18798
			},
18799
			text: '-',
18800
			y: 28
18801
		}
18802
	}
18803
	// enabled: false,
18804
	// enableButtons: null, // inherit from enabled
18805
	// enableTouchZoom: null, // inherit from enabled
18806
	// enableDoubleClickZoom: null, // inherit from enabled
18807
	// enableDoubleClickZoomTo: false
18808
	// enableMouseWheelZoom: null, // inherit from enabled
18809
};
18810
 
18811
/**
18812
 * Utility for reading SVG paths directly.
18813
 */
18814
Highcharts.splitPath = function (path) {
18815
	var i;
18816
 
18817
	// Move letters apart
18818
	path = path.replace(/([A-Za-z])/g, ' $1 ');
18819
	// Trim
18820
	path = path.replace(/^\s*/, "").replace(/\s*$/, "");
18821
 
18822
	// Split on spaces and commas
18823
	path = path.split(/[ ,]+/);
18824
 
18825
	// Parse numbers
18826
	for (i = 0; i < path.length; i++) {
18827
		if (!/[a-zA-Z]/.test(path[i])) {
18828
			path[i] = parseFloat(path[i]);
18829
		}
18830
	}
18831
	return path;
18832
};
18833
 
18834
// A placeholder for map definitions
18835
Highcharts.maps = {};
18836
 
18837
 
18838
 
18839
 
18840
 
18841
// Create symbols for the zoom buttons
18842
function selectiveRoundedRect(x, y, w, h, rTopLeft, rTopRight, rBottomRight, rBottomLeft) {
18843
	return ['M', x + rTopLeft, y,
18844
        // top side
18845
        'L', x + w - rTopRight, y,
18846
        // top right corner
18847
        'C', x + w - rTopRight / 2, y, x + w, y + rTopRight / 2, x + w, y + rTopRight,
18848
        // right side
18849
        'L', x + w, y + h - rBottomRight,
18850
        // bottom right corner
18851
        'C', x + w, y + h - rBottomRight / 2, x + w - rBottomRight / 2, y + h, x + w - rBottomRight, y + h,
18852
        // bottom side
18853
        'L', x + rBottomLeft, y + h,
18854
        // bottom left corner
18855
        'C', x + rBottomLeft / 2, y + h, x, y + h - rBottomLeft / 2, x, y + h - rBottomLeft,
18856
        // left side
18857
        'L', x, y + rTopLeft,
18858
        // top left corner
18859
        'C', x, y + rTopLeft / 2, x + rTopLeft / 2, y, x + rTopLeft, y,
18860
        'Z'
18861
    ];
18862
}
18863
SVGRenderer.prototype.symbols.topbutton = function (x, y, w, h, attr) {
18864
	return selectiveRoundedRect(x - 1, y - 1, w, h, attr.r, attr.r, 0, 0);
18865
};
18866
SVGRenderer.prototype.symbols.bottombutton = function (x, y, w, h, attr) {
18867
	return selectiveRoundedRect(x - 1, y - 1, w, h, 0, 0, attr.r, attr.r);
18868
};
18869
// The symbol callbacks are generated on the SVGRenderer object in all browsers. Even
18870
// VML browsers need this in order to generate shapes in export. Now share
18871
// them with the VMLRenderer.
18872
if (Renderer === VMLRenderer) {
18873
	each(['topbutton', 'bottombutton'], function (shape) {
18874
		VMLRenderer.prototype.symbols[shape] = SVGRenderer.prototype.symbols[shape];
18875
	});
18876
}
18877
 
18878
 
18879
/**
18880
 * A wrapper for Chart with all the default values for a Map
18881
 */
18882
Highcharts.Map = function (options, callback) {
18883
 
18884
	var hiddenAxis = {
18885
			endOnTick: false,
18886
			gridLineWidth: 0,
18887
			lineWidth: 0,
18888
			minPadding: 0,
18889
			maxPadding: 0,
18890
			startOnTick: false,
18891
			title: null,
18892
			tickPositions: []
18893
		},
18894
		seriesOptions;
18895
 
18896
	/* For visual testing
18897
	hiddenAxis.gridLineWidth = 1;
18898
	hiddenAxis.gridZIndex = 10;
18899
	hiddenAxis.tickPositions = undefined;
18900
	// */
18901
 
18902
	// Don't merge the data
18903
	seriesOptions = options.series;
18904
	options.series = null;
18905
 
18906
	options = merge({
18907
		chart: {
18908
			panning: 'xy',
18909
			type: 'map'
18910
		},
18911
		xAxis: hiddenAxis,
18912
		yAxis: merge(hiddenAxis, { reversed: true })
18913
	},
18914
	options, // user's options
18915
 
18916
	{ // forced options
18917
		chart: {
18918
			inverted: false,
18919
			alignTicks: false
18920
		}
18921
	});
18922
 
18923
	options.series = seriesOptions;
18924
 
18925
 
18926
	return new Chart(options, callback);
18927
};
18928
 
18929
/**
18930
 * Extend the default options with map options
18931
 */
18932
defaultOptions.plotOptions.heatmap = merge(defaultOptions.plotOptions.scatter, {
18933
	animation: false,
18934
	borderWidth: 0,
18935
	nullColor: '#F8F8F8',
18936
	dataLabels: {
18937
		formatter: function () { // #2945
18938
			return this.point.value;
18939
		},
18940
		inside: true,
18941
		verticalAlign: 'middle',
18942
		crop: false,
18943
		overflow: false,
18944
		padding: 0 // #3837
18945
	},
18946
	marker: null,
18947
	pointRange: null, // dynamically set to colsize by default
18948
	tooltip: {
18949
		pointFormat: '{point.x}, {point.y}: {point.value}<br/>'
18950
	},
18951
	states: {
18952
		normal: {
18953
			animation: true
18954
		},
18955
		hover: {
18956
			halo: false,  // #3406, halo is not required on heatmaps
18957
			brightness: 0.2
18958
		}
18959
	}
18960
});
18961
 
18962
// The Heatmap series type
18963
seriesTypes.heatmap = extendClass(seriesTypes.scatter, merge(colorSeriesMixin, {
18964
	type: 'heatmap',
18965
	pointArrayMap: ['y', 'value'],
18966
	hasPointSpecificOptions: true,
18967
	pointClass: extendClass(Point, colorPointMixin),
18968
	supportsDrilldown: true,
18969
	getExtremesFromAll: true,
18970
	directTouch: true,
18971
 
18972
	/**
18973
	 * Override the init method to add point ranges on both axes.
18974
	 */
18975
	init: function () {
18976
		var options;
18977
		seriesTypes.scatter.prototype.init.apply(this, arguments);
18978
 
18979
		options = this.options;
18980
		this.pointRange = options.pointRange = pick(options.pointRange, options.colsize || 1); // #3758, prevent resetting in setData
18981
		this.yAxis.axisPointRange = options.rowsize || 1; // general point range
18982
	},
18983
	translate: function () {
18984
		var series = this,
18985
			options = series.options,
18986
			xAxis = series.xAxis,
18987
			yAxis = series.yAxis,
18988
			between = function (x, a, b) {
18989
				return Math.min(Math.max(a, x), b);
18990
			};
18991
 
18992
		series.generatePoints();
18993
 
18994
		each(series.points, function (point) {
18995
			var xPad = (options.colsize || 1) / 2,
18996
				yPad = (options.rowsize || 1) / 2,
18997
				x1 = between(Math.round(xAxis.len - xAxis.translate(point.x - xPad, 0, 1, 0, 1)), 0, xAxis.len),
18998
				x2 = between(Math.round(xAxis.len - xAxis.translate(point.x + xPad, 0, 1, 0, 1)), 0, xAxis.len),
18999
				y1 = between(Math.round(yAxis.translate(point.y - yPad, 0, 1, 0, 1)), 0, yAxis.len),
19000
				y2 = between(Math.round(yAxis.translate(point.y + yPad, 0, 1, 0, 1)), 0, yAxis.len);
19001
 
19002
			// Set plotX and plotY for use in K-D-Tree and more
19003
			point.plotX = point.clientX = (x1 + x2) / 2;
19004
			point.plotY = (y1 + y2) / 2;
19005
 
19006
			point.shapeType = 'rect';
19007
			point.shapeArgs = {
19008
				x: Math.min(x1, x2),
19009
				y: Math.min(y1, y2),
19010
				width: Math.abs(x2 - x1),
19011
				height: Math.abs(y2 - y1)
19012
			};
19013
		});
19014
 
19015
		series.translateColors();
19016
 
19017
		// Make sure colors are updated on colorAxis update (#2893)
19018
		if (this.chart.hasRendered) {
19019
			each(series.points, function (point) {
19020
				point.shapeArgs.fill = point.options.color || point.color; // #3311
19021
			});
19022
		}
19023
	},
19024
	drawPoints: seriesTypes.column.prototype.drawPoints,
19025
	animate: noop,
19026
	getBox: noop,
19027
	drawLegendSymbol: LegendSymbolMixin.drawRectangle,
19028
 
19029
	getExtremes: function () {
19030
		// Get the extremes from the value data
19031
		Series.prototype.getExtremes.call(this, this.valueData);
19032
		this.valueMin = this.dataMin;
19033
		this.valueMax = this.dataMax;
19034
 
19035
		// Get the extremes from the y data
19036
		Series.prototype.getExtremes.call(this);
19037
	}
19038
 
19039
}));
19040
 
19041
/**
19042
 * TrackerMixin for points and graphs
19043
 */
19044
 
19045
var TrackerMixin = Highcharts.TrackerMixin = {
19046
 
19047
	drawTrackerPoint: function () {
19048
		var series = this,
19049
			chart = series.chart,
19050
			pointer = chart.pointer,
19051
			cursor = series.options.cursor,
19052
			css = cursor && { cursor: cursor },
19053
			onMouseOver = function (e) {
19054
				var target = e.target,
19055
				point;
19056
 
19057
				while (target && !point) {
19058
					point = target.point;
19059
					target = target.parentNode;
19060
				}
19061
 
19062
				if (point !== UNDEFINED && point !== chart.hoverPoint) { // undefined on graph in scatterchart
19063
					point.onMouseOver(e);
19064
				}
19065
			};
19066
 
19067
		// Add reference to the point
19068
		each(series.points, function (point) {
19069
			if (point.graphic) {
19070
				point.graphic.element.point = point;
19071
			}
19072
			if (point.dataLabel) {
19073
				point.dataLabel.element.point = point;
19074
			}
19075
		});
19076
 
19077
		// Add the event listeners, we need to do this only once
19078
		if (!series._hasTracking) {
19079
			each(series.trackerGroups, function (key) {
19080
				if (series[key]) { // we don't always have dataLabelsGroup
19081
					series[key]
19082
						.addClass(PREFIX + 'tracker')
19083
						.on('mouseover', onMouseOver)
19084
						.on('mouseout', function (e) { pointer.onTrackerMouseOut(e); })
19085
						.css(css);
19086
					if (hasTouch) {
19087
						series[key].on('touchstart', onMouseOver);
19088
					}
19089
				}
19090
			});
19091
			series._hasTracking = true;
19092
		}
19093
	},
19094
 
19095
	/**
19096
	 * Draw the tracker object that sits above all data labels and markers to
19097
	 * track mouse events on the graph or points. For the line type charts
19098
	 * the tracker uses the same graphPath, but with a greater stroke width
19099
	 * for better control.
19100
	 */
19101
	drawTrackerGraph: function () {
19102
		var series = this,
19103
			options = series.options,
19104
			trackByArea = options.trackByArea,
19105
			trackerPath = [].concat(trackByArea ? series.areaPath : series.graphPath),
19106
			trackerPathLength = trackerPath.length,
19107
			chart = series.chart,
19108
			pointer = chart.pointer,
19109
			renderer = chart.renderer,
19110
			snap = chart.options.tooltip.snap,
19111
			tracker = series.tracker,
19112
			cursor = options.cursor,
19113
			css = cursor && { cursor: cursor },
19114
			singlePoints = series.singlePoints,
19115
			singlePoint,
19116
			i,
19117
			onMouseOver = function () {
19118
				if (chart.hoverSeries !== series) {
19119
					series.onMouseOver();
19120
				}
19121
			},
19122
			/*
19123
			 * Empirical lowest possible opacities for TRACKER_FILL for an element to stay invisible but clickable
19124
			 * IE6: 0.002
19125
			 * IE7: 0.002
19126
			 * IE8: 0.002
19127
			 * IE9: 0.00000000001 (unlimited)
19128
			 * IE10: 0.0001 (exporting only)
19129
			 * FF: 0.00000000001 (unlimited)
19130
			 * Chrome: 0.000001
19131
			 * Safari: 0.000001
19132
			 * Opera: 0.00000000001 (unlimited)
19133
			 */
19134
			TRACKER_FILL = 'rgba(192,192,192,' + (hasSVG ? 0.0001 : 0.002) + ')';
19135
 
19136
		// Extend end points. A better way would be to use round linecaps,
19137
		// but those are not clickable in VML.
19138
		if (trackerPathLength && !trackByArea) {
19139
			i = trackerPathLength + 1;
19140
			while (i--) {
19141
				if (trackerPath[i] === M) { // extend left side
19142
					trackerPath.splice(i + 1, 0, trackerPath[i + 1] - snap, trackerPath[i + 2], L);
19143
				}
19144
				if ((i && trackerPath[i] === M) || i === trackerPathLength) { // extend right side
19145
					trackerPath.splice(i, 0, L, trackerPath[i - 2] + snap, trackerPath[i - 1]);
19146
				}
19147
			}
19148
		}
19149
 
19150
		// handle single points
19151
		for (i = 0; i < singlePoints.length; i++) {
19152
			singlePoint = singlePoints[i];
19153
			trackerPath.push(M, singlePoint.plotX - snap, singlePoint.plotY,
19154
			L, singlePoint.plotX + snap, singlePoint.plotY);
19155
		}
19156
 
19157
		// draw the tracker
19158
		if (tracker) {
19159
			tracker.attr({ d: trackerPath });
19160
		} else { // create
19161
 
19162
			series.tracker = renderer.path(trackerPath)
19163
			.attr({
19164
				'stroke-linejoin': 'round', // #1225
19165
				visibility: series.visible ? VISIBLE : HIDDEN,
19166
				stroke: TRACKER_FILL,
19167
				fill: trackByArea ? TRACKER_FILL : NONE,
19168
				'stroke-width' : options.lineWidth + (trackByArea ? 0 : 2 * snap),
19169
				zIndex: 2
19170
			})
19171
			.add(series.group);
19172
 
19173
			// The tracker is added to the series group, which is clipped, but is covered
19174
			// by the marker group. So the marker group also needs to capture events.
19175
			each([series.tracker, series.markerGroup], function (tracker) {
19176
				tracker.addClass(PREFIX + 'tracker')
19177
					.on('mouseover', onMouseOver)
19178
					.on('mouseout', function (e) { pointer.onTrackerMouseOut(e); })
19179
					.css(css);
19180
 
19181
				if (hasTouch) {
19182
					tracker.on('touchstart', onMouseOver);
19183
				}
19184
			});
19185
		}
19186
	}
19187
};
19188
/* End TrackerMixin */
19189
 
19190
 
19191
/**
19192
 * Add tracking event listener to the series group, so the point graphics
19193
 * themselves act as trackers
19194
 */
19195
 
19196
if (seriesTypes.column) {
19197
	ColumnSeries.prototype.drawTracker = TrackerMixin.drawTrackerPoint;
19198
}
19199
 
19200
if (seriesTypes.pie) {
19201
	seriesTypes.pie.prototype.drawTracker = TrackerMixin.drawTrackerPoint;
19202
}
19203
 
19204
if (seriesTypes.scatter) {
19205
	ScatterSeries.prototype.drawTracker = TrackerMixin.drawTrackerPoint;
19206
}
19207
 
19208
/*
19209
 * Extend Legend for item events
19210
 */
19211
extend(Legend.prototype, {
19212
 
19213
	setItemEvents: function (item, legendItem, useHTML, itemStyle, itemHiddenStyle) {
19214
	var legend = this;
19215
	// Set the events on the item group, or in case of useHTML, the item itself (#1249)
19216
	(useHTML ? legendItem : item.legendGroup).on('mouseover', function () {
19217
			item.setState(HOVER_STATE);
19218
			legendItem.css(legend.options.itemHoverStyle);
19219
		})
19220
		.on('mouseout', function () {
19221
			legendItem.css(item.visible ? itemStyle : itemHiddenStyle);
19222
			item.setState();
19223
		})
19224
		.on('click', function (event) {
19225
			var strLegendItemClick = 'legendItemClick',
19226
				fnLegendItemClick = function () {
19227
					if (item.setVisible) {
19228
						item.setVisible();
19229
					}
19230
				};
19231
 
19232
			// Pass over the click/touch event. #4.
19233
			event = {
19234
				browserEvent: event
19235
			};
19236
 
19237
			// click the name or symbol
19238
			if (item.firePointEvent) { // point
19239
				item.firePointEvent(strLegendItemClick, event, fnLegendItemClick);
19240
			} else {
19241
				fireEvent(item, strLegendItemClick, event, fnLegendItemClick);
19242
			}
19243
		});
19244
	},
19245
 
19246
	createCheckboxForItem: function (item) {
19247
		var legend = this;
19248
 
19249
		item.checkbox = createElement('input', {
19250
			type: 'checkbox',
19251
			checked: item.selected,
19252
			defaultChecked: item.selected // required by IE7
19253
		}, legend.options.itemCheckboxStyle, legend.chart.container);
19254
 
19255
		addEvent(item.checkbox, 'click', function (event) {
19256
			var target = event.target;
19257
			fireEvent(item.series || item, 'checkboxClick', { // #3712
19258
					checked: target.checked,
19259
					item: item
19260
				},
19261
				function () {
19262
					item.select();
19263
				}
19264
			);
19265
		});
19266
	}
19267
});
19268
 
19269
/*
19270
 * Add pointer cursor to legend itemstyle in defaultOptions
19271
 */
19272
defaultOptions.legend.itemStyle.cursor = 'pointer';
19273
 
19274
 
19275
/*
19276
 * Extend the Chart object with interaction
19277
 */
19278
 
19279
extend(Chart.prototype, {
19280
	/**
19281
	 * Display the zoom button
19282
	 */
19283
	showResetZoom: function () {
19284
		var chart = this,
19285
			lang = defaultOptions.lang,
19286
			btnOptions = chart.options.chart.resetZoomButton,
19287
			theme = btnOptions.theme,
19288
			states = theme.states,
19289
			alignTo = btnOptions.relativeTo === 'chart' ? null : 'plotBox';
19290
 
19291
		this.resetZoomButton = chart.renderer.button(lang.resetZoom, null, null, function () { chart.zoomOut(); }, theme, states && states.hover)
19292
			.attr({
19293
				align: btnOptions.position.align,
19294
				title: lang.resetZoomTitle
19295
			})
19296
			.add()
19297
			.align(btnOptions.position, false, alignTo);
19298
 
19299
	},
19300
 
19301
	/**
19302
	 * Zoom out to 1:1
19303
	 */
19304
	zoomOut: function () {
19305
		var chart = this;
19306
		fireEvent(chart, 'selection', { resetSelection: true }, function () {
19307
			chart.zoom();
19308
		});
19309
	},
19310
 
19311
	/**
19312
	 * Zoom into a given portion of the chart given by axis coordinates
19313
	 * @param {Object} event
19314
	 */
19315
	zoom: function (event) {
19316
		var chart = this,
19317
			hasZoomed,
19318
			pointer = chart.pointer,
19319
			displayButton = false,
19320
			resetZoomButton;
19321
 
19322
		// If zoom is called with no arguments, reset the axes
19323
		if (!event || event.resetSelection) {
19324
			each(chart.axes, function (axis) {
19325
				hasZoomed = axis.zoom();
19326
			});
19327
		} else { // else, zoom in on all axes
19328
			each(event.xAxis.concat(event.yAxis), function (axisData) {
19329
				var axis = axisData.axis,
19330
					isXAxis = axis.isXAxis;
19331
 
19332
				// don't zoom more than minRange
19333
				if (pointer[isXAxis ? 'zoomX' : 'zoomY'] || pointer[isXAxis ? 'pinchX' : 'pinchY']) {
19334
					hasZoomed = axis.zoom(axisData.min, axisData.max);
19335
					if (axis.displayBtn) {
19336
						displayButton = true;
19337
					}
19338
				}
19339
			});
19340
		}
19341
 
19342
		// Show or hide the Reset zoom button
19343
		resetZoomButton = chart.resetZoomButton;
19344
		if (displayButton && !resetZoomButton) {
19345
			chart.showResetZoom();
19346
		} else if (!displayButton && isObject(resetZoomButton)) {
19347
			chart.resetZoomButton = resetZoomButton.destroy();
19348
		}
19349
 
19350
 
19351
		// Redraw
19352
		if (hasZoomed) {
19353
			chart.redraw(
19354
				pick(chart.options.chart.animation, event && event.animation, chart.pointCount < 100) // animation
19355
			);
19356
		}
19357
	},
19358
 
19359
	/**
19360
	 * Pan the chart by dragging the mouse across the pane. This function is called
19361
	 * on mouse move, and the distance to pan is computed from chartX compared to
19362
	 * the first chartX position in the dragging operation.
19363
	 */
19364
	pan: function (e, panning) {
19365
 
19366
		var chart = this,
19367
			hoverPoints = chart.hoverPoints,
19368
			doRedraw;
19369
 
19370
		// remove active points for shared tooltip
19371
		if (hoverPoints) {
19372
			each(hoverPoints, function (point) {
19373
				point.setState();
19374
			});
19375
		}
19376
 
19377
		each(panning === 'xy' ? [1, 0] : [1], function (isX) { // xy is used in maps
19378
			var mousePos = e[isX ? 'chartX' : 'chartY'],
19379
				axis = chart[isX ? 'xAxis' : 'yAxis'][0],
19380
				startPos = chart[isX ? 'mouseDownX' : 'mouseDownY'],
19381
				halfPointRange = (axis.pointRange || 0) / 2,
19382
				extremes = axis.getExtremes(),
19383
				newMin = axis.toValue(startPos - mousePos, true) + halfPointRange,
19384
				newMax = axis.toValue(startPos + chart[isX ? 'plotWidth' : 'plotHeight'] - mousePos, true) - halfPointRange,
19385
				goingLeft = startPos > mousePos; // #3613
19386
 
19387
			if (axis.series.length &&
19388
					(goingLeft || newMin > mathMin(extremes.dataMin, extremes.min)) &&
19389
					(!goingLeft || newMax < mathMax(extremes.dataMax, extremes.max))) {
19390
				axis.setExtremes(newMin, newMax, false, false, { trigger: 'pan' });
19391
				doRedraw = true;
19392
			}
19393
 
19394
			chart[isX ? 'mouseDownX' : 'mouseDownY'] = mousePos; // set new reference for next run
19395
		});
19396
 
19397
		if (doRedraw) {
19398
			chart.redraw(false);
19399
		}
19400
		css(chart.container, { cursor: 'move' });
19401
	}
19402
});
19403
 
19404
/*
19405
 * Extend the Point object with interaction
19406
 */
19407
extend(Point.prototype, {
19408
	/**
19409
	 * Toggle the selection status of a point
19410
	 * @param {Boolean} selected Whether to select or unselect the point.
19411
	 * @param {Boolean} accumulate Whether to add to the previous selection. By default,
19412
	 *		 this happens if the control key (Cmd on Mac) was pressed during clicking.
19413
	 */
19414
	select: function (selected, accumulate) {
19415
		var point = this,
19416
			series = point.series,
19417
			chart = series.chart;
19418
 
19419
		selected = pick(selected, !point.selected);
19420
 
19421
		// fire the event with the defalut handler
19422
		point.firePointEvent(selected ? 'select' : 'unselect', { accumulate: accumulate }, function () {
19423
			point.selected = point.options.selected = selected;
19424
			series.options.data[inArray(point, series.data)] = point.options;
19425
 
19426
			point.setState(selected && SELECT_STATE);
19427
 
19428
			// unselect all other points unless Ctrl or Cmd + click
19429
			if (!accumulate) {
19430
				each(chart.getSelectedPoints(), function (loopPoint) {
19431
					if (loopPoint.selected && loopPoint !== point) {
19432
						loopPoint.selected = loopPoint.options.selected = false;
19433
						series.options.data[inArray(loopPoint, series.data)] = loopPoint.options;
19434
						loopPoint.setState(NORMAL_STATE);
19435
							loopPoint.firePointEvent('unselect');
19436
					}
19437
				});
19438
			}
19439
		});
19440
	},
19441
 
19442
	/**
19443
	 * Runs on mouse over the point
19444
	 *
19445
	 * @param {Object} e The event arguments
19446
	 * @param {Boolean} byProximity Falsy for kd points that are closest to the mouse, or to
19447
	 *        actually hovered points. True for other points in shared tooltip.
19448
	 */
19449
	onMouseOver: function (e, byProximity) {
19450
		var point = this,
19451
			series = point.series,
19452
			chart = series.chart,
19453
			tooltip = chart.tooltip,
19454
			hoverPoint = chart.hoverPoint;
19455
 
19456
		if (chart.hoverSeries !== series) {
19457
			series.onMouseOver();
19458
		}
19459
 
19460
		// set normal state to previous series
19461
		if (hoverPoint && hoverPoint !== point) {
19462
			hoverPoint.onMouseOut();
19463
		}
19464
 
19465
		if (point.series) { // It may have been destroyed, #4130
19466
 
19467
			// trigger the event
19468
			point.firePointEvent('mouseOver');
19469
 
19470
			// update the tooltip
19471
			if (tooltip && (!tooltip.shared || series.noSharedTooltip)) {
19472
				tooltip.refresh(point, e);
19473
			}
19474
 
19475
			// hover this
19476
			point.setState(HOVER_STATE);
19477
			if (!byProximity) {
19478
				chart.hoverPoint = point;
19479
			}
19480
		}
19481
	},
19482
 
19483
	/**
19484
	 * Runs on mouse out from the point
19485
	 */
19486
	onMouseOut: function () {
19487
		var chart = this.series.chart,
19488
			hoverPoints = chart.hoverPoints;
19489
 
19490
		this.firePointEvent('mouseOut');
19491
 
19492
		if (!hoverPoints || inArray(this, hoverPoints) === -1) { // #887, #2240
19493
			this.setState();
19494
			chart.hoverPoint = null;
19495
		}
19496
	},
19497
 
19498
	/**
19499
	 * Import events from the series' and point's options. Only do it on
19500
	 * demand, to save processing time on hovering.
19501
	 */
19502
	importEvents: function () {
19503
		if (!this.hasImportedEvents) {
19504
			var point = this,
19505
				options = merge(point.series.options.point, point.options),
19506
				events = options.events,
19507
				eventType;
19508
 
19509
			point.events = events;
19510
 
19511
			for (eventType in events) {
19512
				addEvent(point, eventType, events[eventType]);
19513
			}
19514
			this.hasImportedEvents = true;
19515
 
19516
		}
19517
	},
19518
 
19519
	/**
19520
	 * Set the point's state
19521
	 * @param {String} state
19522
	 */
19523
	setState: function (state, move) {
19524
		var point = this,
19525
			plotX = mathFloor(point.plotX), // #4586
19526
			plotY = point.plotY,
19527
			series = point.series,
19528
			stateOptions = series.options.states,
19529
			markerOptions = defaultPlotOptions[series.type].marker && series.options.marker,
19530
			normalDisabled = markerOptions && !markerOptions.enabled,
19531
			markerStateOptions = markerOptions && markerOptions.states[state],
19532
			stateDisabled = markerStateOptions && markerStateOptions.enabled === false,
19533
			stateMarkerGraphic = series.stateMarkerGraphic,
19534
			pointMarker = point.marker || {},
19535
			chart = series.chart,
19536
			radius,
19537
			halo = series.halo,
19538
			haloOptions,
19539
			newSymbol,
19540
			pointAttr;
19541
 
19542
		state = state || NORMAL_STATE; // empty string
19543
		pointAttr = point.pointAttr[state] || series.pointAttr[state];
19544
 
19545
		if (
19546
				// already has this state
19547
				(state === point.state && !move) ||
19548
				// selected points don't respond to hover
19549
				(point.selected && state !== SELECT_STATE) ||
19550
				// series' state options is disabled
19551
				(stateOptions[state] && stateOptions[state].enabled === false) ||
19552
				// general point marker's state options is disabled
19553
				(state && (stateDisabled || (normalDisabled && markerStateOptions.enabled === false))) ||
19554
				// individual point marker's state options is disabled
19555
				(state && pointMarker.states && pointMarker.states[state] && pointMarker.states[state].enabled === false) // #1610
19556
 
19557
			) {
19558
			return;
19559
		}
19560
 
19561
		// apply hover styles to the existing point
19562
		if (point.graphic) {
19563
			radius = markerOptions && point.graphic.symbolName && pointAttr.r;
19564
			point.graphic.attr(merge(
19565
				pointAttr,
19566
				radius ? { // new symbol attributes (#507, #612)
19567
					x: plotX - radius,
19568
					y: plotY - radius,
19569
					width: 2 * radius,
19570
					height: 2 * radius
19571
				} : {}
19572
			));
19573
 
19574
			// Zooming in from a range with no markers to a range with markers
19575
			if (stateMarkerGraphic) {
19576
				stateMarkerGraphic.hide();
19577
			}
19578
		} else {
19579
			// if a graphic is not applied to each point in the normal state, create a shared
19580
			// graphic for the hover state
19581
			if (state && markerStateOptions) {
19582
				radius = markerStateOptions.radius;
19583
				newSymbol = pointMarker.symbol || series.symbol;
19584
 
19585
				// If the point has another symbol than the previous one, throw away the
19586
				// state marker graphic and force a new one (#1459)
19587
				if (stateMarkerGraphic && stateMarkerGraphic.currentSymbol !== newSymbol) {
19588
					stateMarkerGraphic = stateMarkerGraphic.destroy();
19589
				}
19590
 
19591
				// Add a new state marker graphic
19592
				if (!stateMarkerGraphic) {
19593
					if (newSymbol) {
19594
						series.stateMarkerGraphic = stateMarkerGraphic = chart.renderer.symbol(
19595
							newSymbol,
19596
							plotX - radius,
19597
							plotY - radius,
19598
							2 * radius,
19599
							2 * radius
19600
						)
19601
						.attr(pointAttr)
19602
						.add(series.markerGroup);
19603
						stateMarkerGraphic.currentSymbol = newSymbol;
19604
					}
19605
 
19606
				// Move the existing graphic
19607
				} else {
19608
					stateMarkerGraphic[move ? 'animate' : 'attr']({ // #1054
19609
						x: plotX - radius,
19610
						y: plotY - radius
19611
					});
19612
				}
19613
			}
19614
 
19615
			if (stateMarkerGraphic) {
19616
				stateMarkerGraphic[state && chart.isInsidePlot(plotX, plotY, chart.inverted) ? 'show' : 'hide'](); // #2450
19617
				stateMarkerGraphic.element.point = point; // #4310
19618
			}
19619
		}
19620
 
19621
		// Show me your halo
19622
		haloOptions = stateOptions[state] && stateOptions[state].halo;
19623
		if (haloOptions && haloOptions.size) {
19624
			if (!halo) {
19625
				series.halo = halo = chart.renderer.path()
19626
					.add(chart.seriesGroup);
19627
			}
19628
			halo.attr(extend({
19629
				fill: Color(point.color || series.color).setOpacity(haloOptions.opacity).get()
19630
			}, haloOptions.attributes))[move ? 'animate' : 'attr']({
19631
				d: point.haloPath(haloOptions.size)
19632
			});
19633
		} else if (halo) {
19634
			halo.attr({ d: [] });
19635
		}
19636
 
19637
		point.state = state;
19638
	},
19639
 
19640
	haloPath: function (size) {
19641
		var series = this.series,
19642
			chart = series.chart,
19643
			plotBox = series.getPlotBox(),
19644
			inverted = chart.inverted;
19645
 
19646
		return chart.renderer.symbols.circle(
19647
			plotBox.translateX + (inverted ? series.yAxis.len - this.plotY : this.plotX) - size,
19648
			plotBox.translateY + (inverted ? series.xAxis.len - this.plotX : this.plotY) - size,
19649
			size * 2,
19650
			size * 2
19651
		);
19652
	}
19653
});
19654
 
19655
/*
19656
 * Extend the Series object with interaction
19657
 */
19658
 
19659
extend(Series.prototype, {
19660
	/**
19661
	 * Series mouse over handler
19662
	 */
19663
	onMouseOver: function () {
19664
		var series = this,
19665
			chart = series.chart,
19666
			hoverSeries = chart.hoverSeries;
19667
 
19668
		// set normal state to previous series
19669
		if (hoverSeries && hoverSeries !== series) {
19670
			hoverSeries.onMouseOut();
19671
		}
19672
 
19673
		// trigger the event, but to save processing time,
19674
		// only if defined
19675
		if (series.options.events.mouseOver) {
19676
			fireEvent(series, 'mouseOver');
19677
		}
19678
 
19679
		// hover this
19680
		series.setState(HOVER_STATE);
19681
		chart.hoverSeries = series;
19682
	},
19683
 
19684
	/**
19685
	 * Series mouse out handler
19686
	 */
19687
	onMouseOut: function () {
19688
		// trigger the event only if listeners exist
19689
		var series = this,
19690
			options = series.options,
19691
			chart = series.chart,
19692
			tooltip = chart.tooltip,
19693
			hoverPoint = chart.hoverPoint;
19694
 
19695
		chart.hoverSeries = null; // #182, set to null before the mouseOut event fires
19696
 
19697
		// trigger mouse out on the point, which must be in this series
19698
		if (hoverPoint) {
19699
			hoverPoint.onMouseOut();
19700
		}
19701
 
19702
		// fire the mouse out event
19703
		if (series && options.events.mouseOut) {
19704
			fireEvent(series, 'mouseOut');
19705
		}
19706
 
19707
 
19708
		// hide the tooltip
19709
		if (tooltip && !options.stickyTracking && (!tooltip.shared || series.noSharedTooltip)) {
19710
			tooltip.hide();
19711
		}
19712
 
19713
		// set normal state
19714
		series.setState();
19715
	},
19716
 
19717
	/**
19718
	 * Set the state of the graph
19719
	 */
19720
	setState: function (state) {
19721
		var series = this,
19722
			options = series.options,
19723
			graph = series.graph,
19724
			stateOptions = options.states,
19725
			lineWidth = options.lineWidth,
19726
			attribs,
19727
			i = 0;
19728
 
19729
		state = state || NORMAL_STATE;
19730
 
19731
		if (series.state !== state) {
19732
			series.state = state;
19733
 
19734
			if (stateOptions[state] && stateOptions[state].enabled === false) {
19735
				return;
19736
			}
19737
 
19738
			if (state) {
19739
				lineWidth = stateOptions[state].lineWidth || lineWidth + (stateOptions[state].lineWidthPlus || 0); // #4035
19740
			}
19741
 
19742
			if (graph && !graph.dashstyle) { // hover is turned off for dashed lines in VML
19743
				attribs = {
19744
					'stroke-width': lineWidth
19745
				};
19746
				// use attr because animate will cause any other animation on the graph to stop
19747
				graph.attr(attribs);
19748
				while (series['zoneGraph' + i]) {
19749
					series['zoneGraph' + i].attr(attribs);
19750
					i = i + 1;
19751
				}
19752
			}
19753
		}
19754
	},
19755
 
19756
	/**
19757
	 * Set the visibility of the graph
19758
	 *
19759
	 * @param vis {Boolean} True to show the series, false to hide. If UNDEFINED,
19760
	 *				the visibility is toggled.
19761
	 */
19762
	setVisible: function (vis, redraw) {
19763
		var series = this,
19764
			chart = series.chart,
19765
			legendItem = series.legendItem,
19766
			showOrHide,
19767
			ignoreHiddenSeries = chart.options.chart.ignoreHiddenSeries,
19768
			oldVisibility = series.visible;
19769
 
19770
		// if called without an argument, toggle visibility
19771
		series.visible = vis = series.userOptions.visible = vis === UNDEFINED ? !oldVisibility : vis;
19772
		showOrHide = vis ? 'show' : 'hide';
19773
 
19774
		// show or hide elements
19775
		each(['group', 'dataLabelsGroup', 'markerGroup', 'tracker'], function (key) {
19776
			if (series[key]) {
19777
				series[key][showOrHide]();
19778
			}
19779
		});
19780
 
19781
 
19782
		// hide tooltip (#1361)
19783
		if (chart.hoverSeries === series || (chart.hoverPoint && chart.hoverPoint.series) === series) {
19784
			series.onMouseOut();
19785
		}
19786
 
19787
 
19788
		if (legendItem) {
19789
			chart.legend.colorizeItem(series, vis);
19790
		}
19791
 
19792
 
19793
		// rescale or adapt to resized chart
19794
		series.isDirty = true;
19795
		// in a stack, all other series are affected
19796
		if (series.options.stacking) {
19797
			each(chart.series, function (otherSeries) {
19798
				if (otherSeries.options.stacking && otherSeries.visible) {
19799
					otherSeries.isDirty = true;
19800
				}
19801
			});
19802
		}
19803
 
19804
		// show or hide linked series
19805
		each(series.linkedSeries, function (otherSeries) {
19806
			otherSeries.setVisible(vis, false);
19807
		});
19808
 
19809
		if (ignoreHiddenSeries) {
19810
			chart.isDirtyBox = true;
19811
		}
19812
		if (redraw !== false) {
19813
			chart.redraw();
19814
		}
19815
 
19816
		fireEvent(series, showOrHide);
19817
	},
19818
 
19819
	/**
19820
	 * Show the graph
19821
	 */
19822
	show: function () {
19823
		this.setVisible(true);
19824
	},
19825
 
19826
	/**
19827
	 * Hide the graph
19828
	 */
19829
	hide: function () {
19830
		this.setVisible(false);
19831
	},
19832
 
19833
 
19834
	/**
19835
	 * Set the selected state of the graph
19836
	 *
19837
	 * @param selected {Boolean} True to select the series, false to unselect. If
19838
	 *				UNDEFINED, the selection state is toggled.
19839
	 */
19840
	select: function (selected) {
19841
		var series = this;
19842
		// if called without an argument, toggle
19843
		series.selected = selected = (selected === UNDEFINED) ? !series.selected : selected;
19844
 
19845
		if (series.checkbox) {
19846
			series.checkbox.checked = selected;
19847
		}
19848
 
19849
		fireEvent(series, selected ? 'select' : 'unselect');
19850
	},
19851
 
19852
	drawTracker: TrackerMixin.drawTrackerGraph
19853
});
19854
// global variables
19855
extend(Highcharts, {
19856
 
19857
	// Constructors
19858
	Color: Color,
19859
	Point: Point,
19860
	Tick: Tick,
19861
	Renderer: Renderer,
19862
	SVGElement: SVGElement,
19863
	SVGRenderer: SVGRenderer,
19864
 
19865
	// Various
19866
	arrayMin: arrayMin,
19867
	arrayMax: arrayMax,
19868
	charts: charts,
19869
	dateFormat: dateFormat,
19870
	error: error,
19871
	format: format,
19872
	pathAnim: pathAnim,
19873
	getOptions: getOptions,
19874
	hasBidiBug: hasBidiBug,
19875
	isTouchDevice: isTouchDevice,
19876
	setOptions: setOptions,
19877
	addEvent: addEvent,
19878
	removeEvent: removeEvent,
19879
	createElement: createElement,
19880
	discardElement: discardElement,
19881
	css: css,
19882
	each: each,
19883
	map: map,
19884
	merge: merge,
19885
	splat: splat,
19886
	extendClass: extendClass,
19887
	pInt: pInt,
19888
	svg: hasSVG,
19889
	canvas: useCanVG,
19890
	vml: !hasSVG && !useCanVG,
19891
	product: PRODUCT,
19892
	version: VERSION
19893
});
19894
 
19895
}());