UNPKG

910 kBJavaScriptView Raw
1/**
2 * @license Highcharts JS v5.0.5 (2016-11-29)
3 *
4 * (c) 2009-2016 Torstein Honsi
5 *
6 * License: www.highcharts.com/license
7 */
8'use strict';
9(function(root, factory) {
10 if (typeof module === 'object' && module.exports) {
11 module.exports = root.document ?
12 factory(root) :
13 factory;
14 } else {
15 root.Highcharts = factory(root);
16 }
17}(typeof window !== 'undefined' ? window : this, function(win) {
18 var Highcharts = (function() {
19 /**
20 * (c) 2010-2016 Torstein Honsi
21 *
22 * License: www.highcharts.com/license
23 */
24 'use strict';
25 /* global window */
26 var win = window,
27 doc = win.document;
28
29 var SVG_NS = 'http://www.w3.org/2000/svg',
30 userAgent = (win.navigator && win.navigator.userAgent) || '',
31 svg = doc && doc.createElementNS && !!doc.createElementNS(SVG_NS, 'svg').createSVGRect,
32 isMS = /(edge|msie|trident)/i.test(userAgent) && !window.opera,
33 vml = !svg,
34 isFirefox = /Firefox/.test(userAgent),
35 hasBidiBug = isFirefox && parseInt(userAgent.split('Firefox/')[1], 10) < 4; // issue #38
36
37 var Highcharts = win.Highcharts ? win.Highcharts.error(16, true) : {
38 product: 'Highcharts',
39 version: '5.0.5',
40 deg2rad: Math.PI * 2 / 360,
41 doc: doc,
42 hasBidiBug: hasBidiBug,
43 hasTouch: doc && doc.documentElement.ontouchstart !== undefined,
44 isMS: isMS,
45 isWebKit: /AppleWebKit/.test(userAgent),
46 isFirefox: isFirefox,
47 isTouchDevice: /(Mobile|Android|Windows Phone)/.test(userAgent),
48 SVG_NS: SVG_NS,
49 chartCount: 0,
50 seriesTypes: {},
51 symbolSizes: {},
52 svg: svg,
53 vml: vml,
54 win: win,
55 charts: [],
56 marginNames: ['plotTop', 'marginRight', 'marginBottom', 'plotLeft'],
57 noop: function() {
58 return undefined;
59 }
60 };
61 return Highcharts;
62 }());
63 (function(H) {
64 /**
65 * (c) 2010-2016 Torstein Honsi
66 *
67 * License: www.highcharts.com/license
68 */
69 /* eslint max-len: ["warn", 80, 4] */
70 'use strict';
71
72 /**
73 * The Highcharts object is the placeholder for all other members, and various
74 * utility functions.
75 * @namespace Highcharts
76 */
77
78 var timers = [];
79
80 var charts = H.charts,
81 doc = H.doc,
82 win = H.win;
83
84 /**
85 * Provide error messages for debugging, with links to online explanation. This
86 * function can be overridden to provide custom error handling.
87 *
88 * @function #error
89 * @memberOf Highcharts
90 * @param {Number} code - The error code. See [errors.xml]{@link
91 * https://github.com/highcharts/highcharts/blob/master/errors/errors.xml}
92 * for available codes.
93 * @param {Boolean} [stop=false] - Whether to throw an error or just log a
94 * warning in the console.
95 */
96 H.error = function(code, stop) {
97 var msg = 'Highcharts error #' + code + ': www.highcharts.com/errors/' +
98 code;
99 if (stop) {
100 throw new Error(msg);
101 }
102 // else ...
103 if (win.console) {
104 console.log(msg); // eslint-disable-line no-console
105 }
106 };
107
108 /**
109 * An animator object. One instance applies to one property (attribute or style
110 * prop) on one element.
111 *
112 * @constructor Fx
113 * @memberOf Highcharts
114 * @param {HTMLDOMElement|SVGElement} elem - The element to animate.
115 * @param {AnimationOptions} options - Animation options.
116 * @param {string} prop - The single attribute or CSS property to animate.
117 */
118 H.Fx = function(elem, options, prop) {
119 this.options = options;
120 this.elem = elem;
121 this.prop = prop;
122 };
123 H.Fx.prototype = {
124
125 /**
126 * Set the current step of a path definition on SVGElement.
127 *
128 * @function #dSetter
129 * @memberOf Highcharts.Fx
130 */
131 dSetter: function() {
132 var start = this.paths[0],
133 end = this.paths[1],
134 ret = [],
135 now = this.now,
136 i = start.length,
137 startVal;
138
139 // Land on the final path without adjustment points appended in the ends
140 if (now === 1) {
141 ret = this.toD;
142
143 } else if (i === end.length && now < 1) {
144 while (i--) {
145 startVal = parseFloat(start[i]);
146 ret[i] =
147 isNaN(startVal) ? // a letter instruction like M or L
148 start[i] :
149 now * (parseFloat(end[i] - startVal)) + startVal;
150
151 }
152 // If animation is finished or length not matching, land on right value
153 } else {
154 ret = end;
155 }
156 this.elem.attr('d', ret, null, true);
157 },
158
159 /**
160 * Update the element with the current animation step.
161 *
162 * @function #update
163 * @memberOf Highcharts.Fx
164 */
165 update: function() {
166 var elem = this.elem,
167 prop = this.prop, // if destroyed, it is null
168 now = this.now,
169 step = this.options.step;
170
171 // Animation setter defined from outside
172 if (this[prop + 'Setter']) {
173 this[prop + 'Setter']();
174
175 // Other animations on SVGElement
176 } else if (elem.attr) {
177 if (elem.element) {
178 elem.attr(prop, now, null, true);
179 }
180
181 // HTML styles, raw HTML content like container size
182 } else {
183 elem.style[prop] = now + this.unit;
184 }
185
186 if (step) {
187 step.call(elem, now, this);
188 }
189
190 },
191
192 /**
193 * Run an animation.
194 *
195 * @function #run
196 * @memberOf Highcharts.Fx
197 * @param {Number} from - The current value, value to start from.
198 * @param {Number} to - The end value, value to land on.
199 * @param {String} [unit] - The property unit, for example `px`.
200 * @returns {void}
201 */
202 run: function(from, to, unit) {
203 var self = this,
204 timer = function(gotoEnd) {
205 return timer.stopped ? false : self.step(gotoEnd);
206 },
207 i;
208
209 this.startTime = +new Date();
210 this.start = from;
211 this.end = to;
212 this.unit = unit;
213 this.now = this.start;
214 this.pos = 0;
215
216 timer.elem = this.elem;
217 timer.prop = this.prop;
218
219 if (timer() && timers.push(timer) === 1) {
220 timer.timerId = setInterval(function() {
221
222 for (i = 0; i < timers.length; i++) {
223 if (!timers[i]()) {
224 timers.splice(i--, 1);
225 }
226 }
227
228 if (!timers.length) {
229 clearInterval(timer.timerId);
230 }
231 }, 13);
232 }
233 },
234
235 /**
236 * Run a single step in the animation.
237 *
238 * @function #step
239 * @memberOf Highcharts.Fx
240 * @param {Boolean} [gotoEnd] - Whether to go to the endpoint of the
241 * animation after abort.
242 * @returns {Boolean} Returns `true` if animation continues.
243 */
244 step: function(gotoEnd) {
245 var t = +new Date(),
246 ret,
247 done,
248 options = this.options,
249 elem = this.elem,
250 complete = options.complete,
251 duration = options.duration,
252 curAnim = options.curAnim,
253 i;
254
255 if (elem.attr && !elem.element) { // #2616, element is destroyed
256 ret = false;
257
258 } else if (gotoEnd || t >= duration + this.startTime) {
259 this.now = this.end;
260 this.pos = 1;
261 this.update();
262
263 curAnim[this.prop] = true;
264
265 done = true;
266 for (i in curAnim) {
267 if (curAnim[i] !== true) {
268 done = false;
269 }
270 }
271
272 if (done && complete) {
273 complete.call(elem);
274 }
275 ret = false;
276
277 } else {
278 this.pos = options.easing((t - this.startTime) / duration);
279 this.now = this.start + ((this.end - this.start) * this.pos);
280 this.update();
281 ret = true;
282 }
283 return ret;
284 },
285
286 /**
287 * Prepare start and end values so that the path can be animated one to one.
288 *
289 * @function #initPath
290 * @memberOf Highcharts.Fx
291 * @param {SVGElement} elem - The SVGElement item.
292 * @param {String} fromD - Starting path definition.
293 * @param {Array} toD - Ending path definition.
294 * @returns {Array} An array containing start and end paths in array form
295 * so that they can be animated in parallel.
296 */
297 initPath: function(elem, fromD, toD) {
298 fromD = fromD || '';
299 var shift,
300 startX = elem.startX,
301 endX = elem.endX,
302 bezier = fromD.indexOf('C') > -1,
303 numParams = bezier ? 7 : 3,
304 fullLength,
305 slice,
306 i,
307 start = fromD.split(' '),
308 end = toD.slice(), // copy
309 isArea = elem.isArea,
310 positionFactor = isArea ? 2 : 1,
311 reverse;
312
313 /**
314 * In splines make moveTo and lineTo points have six parameters like
315 * bezier curves, to allow animation one-to-one.
316 */
317 function sixify(arr) {
318 var isOperator,
319 nextIsOperator;
320 i = arr.length;
321 while (i--) {
322
323 // Fill in dummy coordinates only if the next operator comes
324 // three places behind (#5788)
325 isOperator = arr[i] === 'M' || arr[i] === 'L';
326 nextIsOperator = /[a-zA-Z]/.test(arr[i + 3]);
327 if (isOperator && nextIsOperator) {
328 arr.splice(
329 i + 1, 0,
330 arr[i + 1], arr[i + 2],
331 arr[i + 1], arr[i + 2]
332 );
333 }
334 }
335 }
336
337 /**
338 * Insert an array at the given position of another array
339 */
340 function insertSlice(arr, subArr, index) {
341 [].splice.apply(
342 arr, [index, 0].concat(subArr)
343 );
344 }
345
346 /**
347 * If shifting points, prepend a dummy point to the end path.
348 */
349 function prepend(arr, other) {
350 while (arr.length < fullLength) {
351
352 // Move to, line to or curve to?
353 arr[0] = other[fullLength - arr.length];
354
355 // Prepend a copy of the first point
356 insertSlice(arr, arr.slice(0, numParams), 0);
357
358 // For areas, the bottom path goes back again to the left, so we
359 // need to append a copy of the last point.
360 if (isArea) {
361 insertSlice(
362 arr,
363 arr.slice(arr.length - numParams), arr.length
364 );
365 i--;
366 }
367 }
368 arr[0] = 'M';
369 }
370
371 /**
372 * Copy and append last point until the length matches the end length
373 */
374 function append(arr, other) {
375 var i = (fullLength - arr.length) / numParams;
376 while (i > 0 && i--) {
377
378 // Pull out the slice that is going to be appended or inserted.
379 // In a line graph, the positionFactor is 1, and the last point
380 // is sliced out. In an area graph, the positionFactor is 2,
381 // causing the middle two points to be sliced out, since an area
382 // path starts at left, follows the upper path then turns and
383 // follows the bottom back.
384 slice = arr.slice().splice(
385 (arr.length / positionFactor) - numParams,
386 numParams * positionFactor
387 );
388
389 // Move to, line to or curve to?
390 slice[0] = other[fullLength - numParams - (i * numParams)];
391
392 // Disable first control point
393 if (bezier) {
394 slice[numParams - 6] = slice[numParams - 2];
395 slice[numParams - 5] = slice[numParams - 1];
396 }
397
398 // Now insert the slice, either in the middle (for areas) or at
399 // the end (for lines)
400 insertSlice(arr, slice, arr.length / positionFactor);
401
402 if (isArea) {
403 i--;
404 }
405 }
406 }
407
408 if (bezier) {
409 sixify(start);
410 sixify(end);
411 }
412
413 // For sideways animation, find out how much we need to shift to get the
414 // start path Xs to match the end path Xs.
415 if (startX && endX) {
416 for (i = 0; i < startX.length; i++) {
417 // Moving left, new points coming in on right
418 if (startX[i] === endX[0]) {
419 shift = i;
420 break;
421 // Moving right
422 } else if (startX[0] ===
423 endX[endX.length - startX.length + i]) {
424 shift = i;
425 reverse = true;
426 break;
427 }
428 }
429 if (shift === undefined) {
430 start = [];
431 }
432 }
433
434 if (start.length) {
435
436 // The common target length for the start and end array, where both
437 // arrays are padded in opposite ends
438 fullLength = end.length + (shift || 0) * positionFactor * numParams;
439
440 if (!reverse) {
441 prepend(end, start);
442 append(start, end);
443 } else {
444 prepend(start, end);
445 append(end, start);
446 }
447 }
448
449 return [start, end];
450 }
451 }; // End of Fx prototype
452
453
454 /**
455 * Utility function to extend an object with the members of another.
456 *
457 * @function #extend
458 * @memberOf Highcharts
459 * @param {Object} a - The object to be extended.
460 * @param {Object} b - The object to add to the first one.
461 * @returns {Object} Object a, the original object.
462 */
463 H.extend = function(a, b) {
464 var n;
465 if (!a) {
466 a = {};
467 }
468 for (n in b) {
469 a[n] = b[n];
470 }
471 return a;
472 };
473
474 /**
475 * Utility function to deep merge two or more objects and return a third object.
476 * If the first argument is true, the contents of the second object is copied
477 * into the first object. The merge function can also be used with a single
478 * object argument to create a deep copy of an object.
479 *
480 * @function #merge
481 * @memberOf Highcharts
482 * @param {Boolean} [extend] - Whether to extend the left-side object (a) or
483 return a whole new object.
484 * @param {Object} a - The first object to extend. When only this is given, the
485 function returns a deep copy.
486 * @param {...Object} [n] - An object to merge into the previous one.
487 * @returns {Object} - The merged object. If the first argument is true, the
488 * return is the same as the second argument.
489 */
490 H.merge = function() {
491 var i,
492 args = arguments,
493 len,
494 ret = {},
495 doCopy = function(copy, original) {
496 var value, key;
497
498 // An object is replacing a primitive
499 if (typeof copy !== 'object') {
500 copy = {};
501 }
502
503 for (key in original) {
504 if (original.hasOwnProperty(key)) {
505 value = original[key];
506
507 // Copy the contents of objects, but not arrays or DOM nodes
508 if (H.isObject(value, true) &&
509 key !== 'renderTo' &&
510 typeof value.nodeType !== 'number') {
511 copy[key] = doCopy(copy[key] || {}, value);
512
513 // Primitives and arrays are copied over directly
514 } else {
515 copy[key] = original[key];
516 }
517 }
518 }
519 return copy;
520 };
521
522 // If first argument is true, copy into the existing object. Used in
523 // setOptions.
524 if (args[0] === true) {
525 ret = args[1];
526 args = Array.prototype.slice.call(args, 2);
527 }
528
529 // For each argument, extend the return
530 len = args.length;
531 for (i = 0; i < len; i++) {
532 ret = doCopy(ret, args[i]);
533 }
534
535 return ret;
536 };
537
538 /**
539 * Shortcut for parseInt
540 * @ignore
541 * @param {Object} s
542 * @param {Number} mag Magnitude
543 */
544 H.pInt = function(s, mag) {
545 return parseInt(s, mag || 10);
546 };
547
548 /**
549 * Utility function to check for string type.
550 *
551 * @function #isString
552 * @memberOf Highcharts
553 * @param {Object} s - The item to check.
554 * @returns {Boolean} - True if the argument is a string.
555 */
556 H.isString = function(s) {
557 return typeof s === 'string';
558 };
559
560 /**
561 * Utility function to check if an item is an array.
562 *
563 * @function #isArray
564 * @memberOf Highcharts
565 * @param {Object} obj - The item to check.
566 * @returns {Boolean} - True if the argument is an array.
567 */
568 H.isArray = function(obj) {
569 var str = Object.prototype.toString.call(obj);
570 return str === '[object Array]' || str === '[object Array Iterator]';
571 };
572
573 /**
574 * Utility function to check if an item is of type object.
575 *
576 * @function #isObject
577 * @memberOf Highcharts
578 * @param {Object} obj - The item to check.
579 * @param {Boolean} [strict=false] - Also checks that the object is not an
580 * array.
581 * @returns {Boolean} - True if the argument is an object.
582 */
583 H.isObject = function(obj, strict) {
584 return obj && typeof obj === 'object' && (!strict || !H.isArray(obj));
585 };
586
587 /**
588 * Utility function to check if an item is of type number.
589 *
590 * @function #isNumber
591 * @memberOf Highcharts
592 * @param {Object} n - The item to check.
593 * @returns {Boolean} - True if the item is a number and is not NaN.
594 */
595 H.isNumber = function(n) {
596 return typeof n === 'number' && !isNaN(n);
597 };
598
599 /**
600 * Remove the last occurence of an item from an array.
601 *
602 * @function #erase
603 * @memberOf Highcharts
604 * @param {Array} arr - The array.
605 * @param {*} item - The item to remove.
606 */
607 H.erase = function(arr, item) {
608 var i = arr.length;
609 while (i--) {
610 if (arr[i] === item) {
611 arr.splice(i, 1);
612 break;
613 }
614 }
615 };
616
617 /**
618 * Check if an object is null or undefined.
619 *
620 * @function #defined
621 * @memberOf Highcharts
622 * @param {Object} obj - The object to check.
623 * @returns {Boolean} - False if the object is null or undefined, otherwise
624 * true.
625 */
626 H.defined = function(obj) {
627 return obj !== undefined && obj !== null;
628 };
629
630 /**
631 * Set or get an attribute or an object of attributes. To use as a setter, pass
632 * a key and a value, or let the second argument be a collection of keys and
633 * values. To use as a getter, pass only a string as the second argument.
634 *
635 * @function #attr
636 * @memberOf Highcharts
637 * @param {Object} elem - The DOM element to receive the attribute(s).
638 * @param {String|Object} [prop] - The property or an object of key-value pairs.
639 * @param {String} [value] - The value if a single property is set.
640 * @returns {*} When used as a getter, return the value.
641 */
642 H.attr = function(elem, prop, value) {
643 var key,
644 ret;
645
646 // if the prop is a string
647 if (H.isString(prop)) {
648 // set the value
649 if (H.defined(value)) {
650 elem.setAttribute(prop, value);
651
652 // get the value
653 } else if (elem && elem.getAttribute) {
654 ret = elem.getAttribute(prop);
655 }
656
657 // else if prop is defined, it is a hash of key/value pairs
658 } else if (H.defined(prop) && H.isObject(prop)) {
659 for (key in prop) {
660 elem.setAttribute(key, prop[key]);
661 }
662 }
663 return ret;
664 };
665
666 /**
667 * Check if an element is an array, and if not, make it into an array.
668 *
669 * @function #splat
670 * @memberOf Highcharts
671 * @param obj {*} - The object to splat.
672 * @returns {Array} The produced or original array.
673 */
674 H.splat = function(obj) {
675 return H.isArray(obj) ? obj : [obj];
676 };
677
678 /**
679 * Set a timeout if the delay is given, otherwise perform the function
680 * synchronously.
681 *
682 * @function #syncTimeout
683 * @memberOf Highcharts
684 * @param {Function} fn - The function callback.
685 * @param {Number} delay - Delay in milliseconds.
686 * @param {Object} [context] - The context.
687 * @returns {Number} An identifier for the timeout that can later be cleared
688 * with clearTimeout.
689 */
690 H.syncTimeout = function(fn, delay, context) {
691 if (delay) {
692 return setTimeout(fn, delay, context);
693 }
694 fn.call(0, context);
695 };
696
697
698 /**
699 * Return the first value that is not null or undefined.
700 *
701 * @function #pick
702 * @memberOf Highcharts
703 * @param {...*} items - Variable number of arguments to inspect.
704 * @returns {*} The value of the first argument that is not null or undefined.
705 */
706 H.pick = function() {
707 var args = arguments,
708 i,
709 arg,
710 length = args.length;
711 for (i = 0; i < length; i++) {
712 arg = args[i];
713 if (arg !== undefined && arg !== null) {
714 return arg;
715 }
716 }
717 };
718
719 /**
720 * @typedef {Object} CSSObject - A style object with camel case property names.
721 * The properties can be whatever styles are supported on the given SVG or HTML
722 * element.
723 * @example
724 * {
725 * fontFamily: 'monospace',
726 * fontSize: '1.2em'
727 * }
728 */
729 /**
730 * Set CSS on a given element.
731 *
732 * @function #css
733 * @memberOf Highcharts
734 * @param {HTMLDOMElement} el - A HTML DOM element.
735 * @param {CSSObject} styles - Style object with camel case property names.
736 * @returns {void}
737 */
738 H.css = function(el, styles) {
739 if (H.isMS && !H.svg) { // #2686
740 if (styles && styles.opacity !== undefined) {
741 styles.filter = 'alpha(opacity=' + (styles.opacity * 100) + ')';
742 }
743 }
744 H.extend(el.style, styles);
745 };
746
747 /**
748 * A HTML DOM element.
749 * @typedef {Object} HTMLDOMElement
750 */
751
752 /**
753 * Utility function to create an HTML element with attributes and styles.
754 *
755 * @function #createElement
756 * @memberOf Highcharts
757 * @param {String} tag - The HTML tag.
758 * @param {Object} [attribs] - Attributes as an object of key-value pairs.
759 * @param {CSSObject} [styles] - Styles as an object of key-value pairs.
760 * @param {Object} [parent] - The parent HTML object.
761 * @param {Boolean} [nopad=false] - If true, remove all padding, border and
762 * margin.
763 * @returns {HTMLDOMElement} The created DOM element.
764 */
765 H.createElement = function(tag, attribs, styles, parent, nopad) {
766 var el = doc.createElement(tag),
767 css = H.css;
768 if (attribs) {
769 H.extend(el, attribs);
770 }
771 if (nopad) {
772 css(el, {
773 padding: 0,
774 border: 'none',
775 margin: 0
776 });
777 }
778 if (styles) {
779 css(el, styles);
780 }
781 if (parent) {
782 parent.appendChild(el);
783 }
784 return el;
785 };
786
787 /**
788 * Extend a prototyped class by new members.
789 *
790 * @function #extendClass
791 * @memberOf Highcharts
792 * @param {Object} parent - The parent prototype to inherit.
793 * @param {Object} members - A collection of prototype members to add or
794 * override compared to the parent prototype.
795 * @returns {Object} A new prototype.
796 */
797 H.extendClass = function(parent, members) {
798 var object = function() {};
799 object.prototype = new parent(); // eslint-disable-line new-cap
800 H.extend(object.prototype, members);
801 return object;
802 };
803
804 /**
805 * Left-pad a string to a given length by adding a character repetetively.
806 *
807 * @function #pad
808 * @memberOf Highcharts
809 * @param {Number} number - The input string or number.
810 * @param {Number} length - The desired string length.
811 * @param {String} [padder=0] - The character to pad with.
812 * @returns {String} The padded string.
813 */
814 H.pad = function(number, length, padder) {
815 return new Array((length || 2) + 1 -
816 String(number).length).join(padder || 0) + number;
817 };
818
819 /**
820 * @typedef {Number|String} RelativeSize - If a number is given, it defines the
821 * pixel length. If a percentage string is given, like for example `'50%'`,
822 * the setting defines a length relative to a base size, for example the size
823 * of a container.
824 */
825 /**
826 * Return a length based on either the integer value, or a percentage of a base.
827 *
828 * @function #relativeLength
829 * @memberOf Highcharts
830 * @param {RelativeSize} value - A percentage string or a number.
831 * @param {Number} base - The full length that represents 100%.
832 * @returns {Number} The computed length.
833 */
834 H.relativeLength = function(value, base) {
835 return (/%$/).test(value) ?
836 base * parseFloat(value) / 100 :
837 parseFloat(value);
838 };
839
840 /**
841 * Wrap a method with extended functionality, preserving the original function.
842 *
843 * @function #wrap
844 * @memberOf Highcharts
845 * @param {Object} obj - The context object that the method belongs to. In real
846 * cases, this is often a prototype.
847 * @param {String} method - The name of the method to extend.
848 * @param {Function} func - A wrapper function callback. This function is called
849 * with the same arguments as the original function, except that the
850 * original function is unshifted and passed as the first argument.
851 * @returns {void}
852 */
853 H.wrap = function(obj, method, func) {
854 var proceed = obj[method];
855 obj[method] = function() {
856 var args = Array.prototype.slice.call(arguments),
857 outerArgs = arguments,
858 ctx = this,
859 ret;
860 ctx.proceed = function() {
861 proceed.apply(ctx, arguments.length ? arguments : outerArgs);
862 };
863 args.unshift(proceed);
864 ret = func.apply(this, args);
865 ctx.proceed = null;
866 return ret;
867 };
868 };
869
870 /**
871 * Get the time zone offset based on the current timezone information as set in
872 * the global options.
873 *
874 * @function #getTZOffset
875 * @memberOf Highcharts
876 * @param {Number} timestamp - The JavaScript timestamp to inspect.
877 * @return {Number} - The timezone offset in minutes compared to UTC.
878 */
879 H.getTZOffset = function(timestamp) {
880 var d = H.Date;
881 return ((d.hcGetTimezoneOffset && d.hcGetTimezoneOffset(timestamp)) ||
882 d.hcTimezoneOffset || 0) * 60000;
883 };
884
885 /**
886 * Format a date, based on the syntax for PHP's [strftime]{@link
887 * http://www.php.net/manual/en/function.strftime.php} function.
888 *
889 * @function #dateFormat
890 * @memberOf Highcharts
891 * @param {String} format - The desired format where various time
892 * representations are prefixed with %.
893 * @param {Number} timestamp - The JavaScript timestamp.
894 * @param {Boolean} [capitalize=false] - Upper case first letter in the return.
895 * @returns {String} The formatted date.
896 */
897 H.dateFormat = function(format, timestamp, capitalize) {
898 if (!H.defined(timestamp) || isNaN(timestamp)) {
899 return H.defaultOptions.lang.invalidDate || '';
900 }
901 format = H.pick(format, '%Y-%m-%d %H:%M:%S');
902
903 var D = H.Date,
904 date = new D(timestamp - H.getTZOffset(timestamp)),
905 key, // used in for constuct below
906 // get the basic time values
907 hours = date[D.hcGetHours](),
908 day = date[D.hcGetDay](),
909 dayOfMonth = date[D.hcGetDate](),
910 month = date[D.hcGetMonth](),
911 fullYear = date[D.hcGetFullYear](),
912 lang = H.defaultOptions.lang,
913 langWeekdays = lang.weekdays,
914 shortWeekdays = lang.shortWeekdays,
915 pad = H.pad,
916
917 // List all format keys. Custom formats can be added from the outside.
918 replacements = H.extend({
919
920 //-- Day
921 // Short weekday, like 'Mon'
922 'a': shortWeekdays ?
923 shortWeekdays[day] : langWeekdays[day].substr(0, 3),
924 // Long weekday, like 'Monday'
925 'A': langWeekdays[day],
926 // Two digit day of the month, 01 to 31
927 'd': pad(dayOfMonth),
928 // Day of the month, 1 through 31
929 'e': pad(dayOfMonth, 2, ' '),
930 'w': day,
931
932 // Week (none implemented)
933 //'W': weekNumber(),
934
935 //-- Month
936 // Short month, like 'Jan'
937 'b': lang.shortMonths[month],
938 // Long month, like 'January'
939 'B': lang.months[month],
940 // Two digit month number, 01 through 12
941 'm': pad(month + 1),
942
943 //-- Year
944 // Two digits year, like 09 for 2009
945 'y': fullYear.toString().substr(2, 2),
946 // Four digits year, like 2009
947 'Y': fullYear,
948
949 //-- Time
950 // Two digits hours in 24h format, 00 through 23
951 'H': pad(hours),
952 // Hours in 24h format, 0 through 23
953 'k': hours,
954 // Two digits hours in 12h format, 00 through 11
955 'I': pad((hours % 12) || 12),
956 // Hours in 12h format, 1 through 12
957 'l': (hours % 12) || 12,
958 // Two digits minutes, 00 through 59
959 'M': pad(date[D.hcGetMinutes]()),
960 // Upper case AM or PM
961 'p': hours < 12 ? 'AM' : 'PM',
962 // Lower case AM or PM
963 'P': hours < 12 ? 'am' : 'pm',
964 // Two digits seconds, 00 through 59
965 'S': pad(date.getSeconds()),
966 // Milliseconds (naming from Ruby)
967 'L': pad(Math.round(timestamp % 1000), 3)
968 }, H.dateFormats);
969
970
971 // Do the replaces
972 for (key in replacements) {
973 // Regex would do it in one line, but this is faster
974 while (format.indexOf('%' + key) !== -1) {
975 format = format.replace(
976 '%' + key,
977 typeof replacements[key] === 'function' ?
978 replacements[key](timestamp) :
979 replacements[key]
980 );
981 }
982 }
983
984 // Optionally capitalize the string and return
985 return capitalize ?
986 format.substr(0, 1).toUpperCase() + format.substr(1) :
987 format;
988 };
989
990 /**
991 * Format a single variable. Similar to sprintf, without the % prefix.
992 *
993 * @example
994 * formatSingle('.2f', 5); // => '5.00'.
995 *
996 * @function #formatSingle
997 * @memberOf Highcharts
998 * @param {String} format The format string.
999 * @param {*} val The value.
1000 * @returns {String} The formatted representation of the value.
1001 */
1002 H.formatSingle = function(format, val) {
1003 var floatRegex = /f$/,
1004 decRegex = /\.([0-9])/,
1005 lang = H.defaultOptions.lang,
1006 decimals;
1007
1008 if (floatRegex.test(format)) { // float
1009 decimals = format.match(decRegex);
1010 decimals = decimals ? decimals[1] : -1;
1011 if (val !== null) {
1012 val = H.numberFormat(
1013 val,
1014 decimals,
1015 lang.decimalPoint,
1016 format.indexOf(',') > -1 ? lang.thousandsSep : ''
1017 );
1018 }
1019 } else {
1020 val = H.dateFormat(format, val);
1021 }
1022 return val;
1023 };
1024
1025 /**
1026 * Format a string according to a subset of the rules of Python's String.format
1027 * method.
1028 *
1029 * @function #format
1030 * @memberOf Highcharts
1031 * @param {String} str The string to format.
1032 * @param {Object} ctx The context, a collection of key-value pairs where each
1033 * key is replaced by its value.
1034 * @returns {String} The formatted string.
1035 *
1036 * @example
1037 * var s = Highcharts.format(
1038 * 'The {color} fox was {len:.2f} feet long',
1039 * { color: 'red', len: Math.PI }
1040 * );
1041 * // => The red fox was 3.14 feet long
1042 */
1043 H.format = function(str, ctx) {
1044 var splitter = '{',
1045 isInside = false,
1046 segment,
1047 valueAndFormat,
1048 path,
1049 i,
1050 len,
1051 ret = [],
1052 val,
1053 index;
1054
1055 while (str) {
1056 index = str.indexOf(splitter);
1057 if (index === -1) {
1058 break;
1059 }
1060
1061 segment = str.slice(0, index);
1062 if (isInside) { // we're on the closing bracket looking back
1063
1064 valueAndFormat = segment.split(':');
1065 path = valueAndFormat.shift().split('.'); // get first and leave
1066 len = path.length;
1067 val = ctx;
1068
1069 // Assign deeper paths
1070 for (i = 0; i < len; i++) {
1071 val = val[path[i]];
1072 }
1073
1074 // Format the replacement
1075 if (valueAndFormat.length) {
1076 val = H.formatSingle(valueAndFormat.join(':'), val);
1077 }
1078
1079 // Push the result and advance the cursor
1080 ret.push(val);
1081
1082 } else {
1083 ret.push(segment);
1084
1085 }
1086 str = str.slice(index + 1); // the rest
1087 isInside = !isInside; // toggle
1088 splitter = isInside ? '}' : '{'; // now look for next matching bracket
1089 }
1090 ret.push(str);
1091 return ret.join('');
1092 };
1093
1094 /**
1095 * Get the magnitude of a number.
1096 *
1097 * @function #getMagnitude
1098 * @memberOf Highcharts
1099 * @param {Number} number The number.
1100 * @returns {Number} The magnitude, where 1-9 are magnitude 1, 10-99 magnitude 2
1101 * etc.
1102 */
1103 H.getMagnitude = function(num) {
1104 return Math.pow(10, Math.floor(Math.log(num) / Math.LN10));
1105 };
1106
1107 /**
1108 * Take an interval and normalize it to multiples of round numbers.
1109 *
1110 * @todo Move this function to the Axis prototype. It is here only for
1111 * historical reasons.
1112 * @function #normalizeTickInterval
1113 * @memberOf Highcharts
1114 * @param {Number} interval - The raw, un-rounded interval.
1115 * @param {Array} [multiples] - Allowed multiples.
1116 * @param {Number} [magnitude] - The magnitude of the number.
1117 * @param {Boolean} [allowDecimals] - Whether to allow decimals.
1118 * @param {Boolean} [hasTickAmount] - If it has tickAmount, avoid landing
1119 * on tick intervals lower than original.
1120 * @returns {Number} The normalized interval.
1121 */
1122 H.normalizeTickInterval = function(interval, multiples, magnitude,
1123 allowDecimals, hasTickAmount) {
1124 var normalized,
1125 i,
1126 retInterval = interval;
1127
1128 // round to a tenfold of 1, 2, 2.5 or 5
1129 magnitude = H.pick(magnitude, 1);
1130 normalized = interval / magnitude;
1131
1132 // multiples for a linear scale
1133 if (!multiples) {
1134 multiples = hasTickAmount ?
1135 // Finer grained ticks when the tick amount is hard set, including
1136 // when alignTicks is true on multiple axes (#4580).
1137 [1, 1.2, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10] :
1138
1139 // Else, let ticks fall on rounder numbers
1140 [1, 2, 2.5, 5, 10];
1141
1142
1143 // the allowDecimals option
1144 if (allowDecimals === false) {
1145 if (magnitude === 1) {
1146 multiples = H.grep(multiples, function(num) {
1147 return num % 1 === 0;
1148 });
1149 } else if (magnitude <= 0.1) {
1150 multiples = [1 / magnitude];
1151 }
1152 }
1153 }
1154
1155 // normalize the interval to the nearest multiple
1156 for (i = 0; i < multiples.length; i++) {
1157 retInterval = multiples[i];
1158 // only allow tick amounts smaller than natural
1159 if ((hasTickAmount && retInterval * magnitude >= interval) ||
1160 (!hasTickAmount && (normalized <= (multiples[i] +
1161 (multiples[i + 1] || multiples[i])) / 2))) {
1162 break;
1163 }
1164 }
1165
1166 // multiply back to the correct magnitude
1167 retInterval *= magnitude;
1168
1169 return retInterval;
1170 };
1171
1172
1173 /**
1174 * Sort an object array and keep the order of equal items. The ECMAScript
1175 * standard does not specify the behaviour when items are equal.
1176 *
1177 * @function #stableSort
1178 * @memberOf Highcharts
1179 * @param {Array} arr - The array to sort.
1180 * @param {Function} sortFunction - The function to sort it with, like with
1181 * regular Array.prototype.sort.
1182 * @returns {void}
1183 */
1184 H.stableSort = function(arr, sortFunction) {
1185 var length = arr.length,
1186 sortValue,
1187 i;
1188
1189 // Add index to each item
1190 for (i = 0; i < length; i++) {
1191 arr[i].safeI = i; // stable sort index
1192 }
1193
1194 arr.sort(function(a, b) {
1195 sortValue = sortFunction(a, b);
1196 return sortValue === 0 ? a.safeI - b.safeI : sortValue;
1197 });
1198
1199 // Remove index from items
1200 for (i = 0; i < length; i++) {
1201 delete arr[i].safeI; // stable sort index
1202 }
1203 };
1204
1205 /**
1206 * Non-recursive method to find the lowest member of an array. `Math.min` raises
1207 * a maximum call stack size exceeded error in Chrome when trying to apply more
1208 * than 150.000 points. This method is slightly slower, but safe.
1209 *
1210 * @function #arrayMin
1211 * @memberOf Highcharts
1212 * @param {Array} data An array of numbers.
1213 * @returns {Number} The lowest number.
1214 */
1215 H.arrayMin = function(data) {
1216 var i = data.length,
1217 min = data[0];
1218
1219 while (i--) {
1220 if (data[i] < min) {
1221 min = data[i];
1222 }
1223 }
1224 return min;
1225 };
1226
1227 /**
1228 * Non-recursive method to find the lowest member of an array. `Math.max` raises
1229 * a maximum call stack size exceeded error in Chrome when trying to apply more
1230 * than 150.000 points. This method is slightly slower, but safe.
1231 *
1232 * @function #arrayMax
1233 * @memberOf Highcharts
1234 * @param {Array} data - An array of numbers.
1235 * @returns {Number} The highest number.
1236 */
1237 H.arrayMax = function(data) {
1238 var i = data.length,
1239 max = data[0];
1240
1241 while (i--) {
1242 if (data[i] > max) {
1243 max = data[i];
1244 }
1245 }
1246 return max;
1247 };
1248
1249 /**
1250 * Utility method that destroys any SVGElement instances that are properties on
1251 * the given object. It loops all properties and invokes destroy if there is a
1252 * destroy method. The property is then delete.
1253 *
1254 * @function #destroyObjectProperties
1255 * @memberOf Highcharts
1256 * @param {Object} obj - The object to destroy properties on.
1257 * @param {Object} [except] - Exception, do not destroy this property, only
1258 * delete it.
1259 * @returns {void}
1260 */
1261 H.destroyObjectProperties = function(obj, except) {
1262 var n;
1263 for (n in obj) {
1264 // If the object is non-null and destroy is defined
1265 if (obj[n] && obj[n] !== except && obj[n].destroy) {
1266 // Invoke the destroy
1267 obj[n].destroy();
1268 }
1269
1270 // Delete the property from the object.
1271 delete obj[n];
1272 }
1273 };
1274
1275
1276 /**
1277 * Discard a HTML element by moving it to the bin and delete.
1278 *
1279 * @function #discardElement
1280 * @memberOf Highcharts
1281 * @param {HTMLDOMElement} element - The HTML node to discard.
1282 * @returns {void}
1283 */
1284 H.discardElement = function(element) {
1285 var garbageBin = H.garbageBin;
1286 // create a garbage bin element, not part of the DOM
1287 if (!garbageBin) {
1288 garbageBin = H.createElement('div');
1289 }
1290
1291 // move the node and empty bin
1292 if (element) {
1293 garbageBin.appendChild(element);
1294 }
1295 garbageBin.innerHTML = '';
1296 };
1297
1298 /**
1299 * Fix JS round off float errors.
1300 *
1301 * @function #correctFloat
1302 * @memberOf Highcharts
1303 * @param {Number} num - A float number to fix.
1304 * @param {Number} [prec=14] - The precision.
1305 * @returns {Number} The corrected float number.
1306 */
1307 H.correctFloat = function(num, prec) {
1308 return parseFloat(
1309 num.toPrecision(prec || 14)
1310 );
1311 };
1312
1313 /**
1314 * Set the global animation to either a given value, or fall back to the given
1315 * chart's animation option.
1316 *
1317 * @function #setAnimation
1318 * @memberOf Highcharts
1319 * @param {Boolean|Animation} animation - The animation object.
1320 * @param {Object} chart - The chart instance.
1321 * @returns {void}
1322 * @todo This function always relates to a chart, and sets a property on the
1323 * renderer, so it should be moved to the SVGRenderer.
1324 */
1325 H.setAnimation = function(animation, chart) {
1326 chart.renderer.globalAnimation = H.pick(
1327 animation,
1328 chart.options.chart.animation,
1329 true
1330 );
1331 };
1332
1333 /**
1334 * Get the animation in object form, where a disabled animation is always
1335 * returned as `{ duration: 0 }`.
1336 *
1337 * @function #animObject
1338 * @memberOf Highcharts
1339 * @param {Boolean|AnimationOptions} animation - An animation setting. Can be an
1340 * object with duration, complete and easing properties, or a boolean to
1341 * enable or disable.
1342 * @returns {AnimationOptions} An object with at least a duration property.
1343 */
1344 H.animObject = function(animation) {
1345 return H.isObject(animation) ?
1346 H.merge(animation) : {
1347 duration: animation ? 500 : 0
1348 };
1349 };
1350
1351 /**
1352 * The time unit lookup
1353 */
1354 H.timeUnits = {
1355 millisecond: 1,
1356 second: 1000,
1357 minute: 60000,
1358 hour: 3600000,
1359 day: 24 * 3600000,
1360 week: 7 * 24 * 3600000,
1361 month: 28 * 24 * 3600000,
1362 year: 364 * 24 * 3600000
1363 };
1364
1365 /**
1366 * Format a number and return a string based on input settings.
1367 *
1368 * @function #numberFormat
1369 * @memberOf Highcharts
1370 * @param {Number} number - The input number to format.
1371 * @param {Number} decimals - The amount of decimals.
1372 * @param {String} [decimalPoint] - The decimal point, defaults to the one given
1373 * in the lang options.
1374 * @param {String} [thousandsSep] - The thousands separator, defaults to the one
1375 * given in the lang options.
1376 * @returns {String} The formatted number.
1377 */
1378 H.numberFormat = function(number, decimals, decimalPoint, thousandsSep) {
1379
1380 number = +number || 0;
1381 decimals = +decimals;
1382
1383 var lang = H.defaultOptions.lang,
1384 origDec = (number.toString().split('.')[1] || '').length,
1385 decimalComponent,
1386 strinteger,
1387 thousands,
1388 absNumber = Math.abs(number),
1389 ret;
1390
1391 if (decimals === -1) {
1392 // Preserve decimals. Not huge numbers (#3793).
1393 decimals = Math.min(origDec, 20);
1394 } else if (!H.isNumber(decimals)) {
1395 decimals = 2;
1396 }
1397
1398 // A string containing the positive integer component of the number
1399 strinteger = String(H.pInt(absNumber.toFixed(decimals)));
1400
1401 // Leftover after grouping into thousands. Can be 0, 1 or 3.
1402 thousands = strinteger.length > 3 ? strinteger.length % 3 : 0;
1403
1404 // Language
1405 decimalPoint = H.pick(decimalPoint, lang.decimalPoint);
1406 thousandsSep = H.pick(thousandsSep, lang.thousandsSep);
1407
1408 // Start building the return
1409 ret = number < 0 ? '-' : '';
1410
1411 // Add the leftover after grouping into thousands. For example, in the
1412 // number 42 000 000, this line adds 42.
1413 ret += thousands ? strinteger.substr(0, thousands) + thousandsSep : '';
1414
1415 // Add the remaining thousands groups, joined by the thousands separator
1416 ret += strinteger
1417 .substr(thousands)
1418 .replace(/(\d{3})(?=\d)/g, '$1' + thousandsSep);
1419
1420 // Add the decimal point and the decimal component
1421 if (decimals) {
1422 // Get the decimal component, and add power to avoid rounding errors
1423 // with float numbers (#4573)
1424 decimalComponent = Math.abs(absNumber - strinteger +
1425 Math.pow(10, -Math.max(decimals, origDec) - 1));
1426 ret += decimalPoint + decimalComponent.toFixed(decimals).slice(2);
1427 }
1428
1429 return ret;
1430 };
1431
1432 /**
1433 * Easing definition
1434 * @ignore
1435 * @param {Number} pos Current position, ranging from 0 to 1.
1436 */
1437 Math.easeInOutSine = function(pos) {
1438 return -0.5 * (Math.cos(Math.PI * pos) - 1);
1439 };
1440
1441 /**
1442 * Get the computed CSS value for given element and property, only for numerical
1443 * properties. For width and height, the dimension of the inner box (excluding
1444 * padding) is returned. Used for fitting the chart within the container.
1445 *
1446 * @function #getStyle
1447 * @memberOf Highcharts
1448 * @param {HTMLDOMElement} el - A HTML element.
1449 * @param {String} prop - The property name.
1450 * @returns {Number} - The numeric value.
1451 */
1452 H.getStyle = function(el, prop) {
1453
1454 var style;
1455
1456 // For width and height, return the actual inner pixel size (#4913)
1457 if (prop === 'width') {
1458 return Math.min(el.offsetWidth, el.scrollWidth) -
1459 H.getStyle(el, 'padding-left') -
1460 H.getStyle(el, 'padding-right');
1461 } else if (prop === 'height') {
1462 return Math.min(el.offsetHeight, el.scrollHeight) -
1463 H.getStyle(el, 'padding-top') -
1464 H.getStyle(el, 'padding-bottom');
1465 }
1466
1467 // Otherwise, get the computed style
1468 style = win.getComputedStyle(el, undefined);
1469 return style && H.pInt(style.getPropertyValue(prop));
1470 };
1471
1472 /**
1473 * Search for an item in an array.
1474 *
1475 * @function #inArray
1476 * @memberOf Highcharts
1477 * @param {*} item - The item to search for.
1478 * @param {arr} arr - The array or node collection to search in.
1479 * @returns {Number} - The index within the array, or -1 if not found.
1480 */
1481 H.inArray = function(item, arr) {
1482 return arr.indexOf ? arr.indexOf(item) : [].indexOf.call(arr, item);
1483 };
1484
1485 /**
1486 * Filter an array by a callback.
1487 *
1488 * @function #grep
1489 * @memberOf Highcharts
1490 * @param {Array} arr - The array to filter.
1491 * @param {Function} callback - The callback function. The function receives the
1492 * item as the first argument. Return `true` if the item is to be
1493 * preserved.
1494 * @returns {Array} - A new, filtered array.
1495 */
1496 H.grep = function(arr, callback) {
1497 return [].filter.call(arr, callback);
1498 };
1499
1500 /**
1501 * Map an array by a callback.
1502 *
1503 * @function #map
1504 * @memberOf Highcharts
1505 * @param {Array} arr - The array to map.
1506 * @param {Function} fn - The callback function. Return the new value for the
1507 * new array.
1508 * @returns {Array} - A new array item with modified items.
1509 */
1510 H.map = function(arr, fn) {
1511 var results = [],
1512 i = 0,
1513 len = arr.length;
1514
1515 for (; i < len; i++) {
1516 results[i] = fn.call(arr[i], arr[i], i, arr);
1517 }
1518
1519 return results;
1520 };
1521
1522 /**
1523 * Get the element's offset position, corrected for `overflow: auto`.
1524 *
1525 * @function #offset
1526 * @memberOf Highcharts
1527 * @param {HTMLDOMElement} el - The HTML element.
1528 * @returns {Object} An object containing `left` and `top` properties for the
1529 * position in the page.
1530 */
1531 H.offset = function(el) {
1532 var docElem = doc.documentElement,
1533 box = el.getBoundingClientRect();
1534
1535 return {
1536 top: box.top + (win.pageYOffset || docElem.scrollTop) -
1537 (docElem.clientTop || 0),
1538 left: box.left + (win.pageXOffset || docElem.scrollLeft) -
1539 (docElem.clientLeft || 0)
1540 };
1541 };
1542
1543 /**
1544 * Stop running animation.
1545 *
1546 * @todo A possible extension to this would be to stop a single property, when
1547 * we want to continue animating others. Then assign the prop to the timer
1548 * in the Fx.run method, and check for the prop here. This would be an
1549 * improvement in all cases where we stop the animation from .attr. Instead of
1550 * stopping everything, we can just stop the actual attributes we're setting.
1551 *
1552 * @function #stop
1553 * @memberOf Highcharts
1554 * @param {SVGElement} el - The SVGElement to stop animation on.
1555 * @param {string} [prop] - The property to stop animating. If given, the stop
1556 * method will stop a single property from animating, while others continue.
1557 * @returns {void}
1558 */
1559 H.stop = function(el, prop) {
1560
1561 var i = timers.length;
1562
1563 // Remove timers related to this element (#4519)
1564 while (i--) {
1565 if (timers[i].elem === el && (!prop || prop === timers[i].prop)) {
1566 timers[i].stopped = true; // #4667
1567 }
1568 }
1569 };
1570
1571 /**
1572 * Iterate over an array.
1573 *
1574 * @function #each
1575 * @memberOf Highcharts
1576 * @param {Array} arr - The array to iterate over.
1577 * @param {Function} fn - The iterator callback. It passes two arguments:
1578 * * item - The array item.
1579 * * index - The item's index in the array.
1580 * @param {Object} [ctx] The context.
1581 */
1582 H.each = function(arr, fn, ctx) { // modern browsers
1583 return Array.prototype.forEach.call(arr, fn, ctx);
1584 };
1585
1586 /**
1587 * Add an event listener.
1588 *
1589 * @function #addEvent
1590 * @memberOf Highcharts
1591 * @param {Object} el - The element or object to add a listener to. It can be a
1592 * {@link HTMLDOMElement}, an {@link SVGElement} or any other object.
1593 * @param {String} type - The event type.
1594 * @param {Function} fn - The function callback to execute when the event is
1595 * fired.
1596 * @returns {Function} A callback function to remove the added event.
1597 */
1598 H.addEvent = function(el, type, fn) {
1599
1600 var events = el.hcEvents = el.hcEvents || {};
1601
1602 function wrappedFn(e) {
1603 e.target = e.srcElement || win; // #2820
1604 fn.call(el, e);
1605 }
1606
1607 // Handle DOM events in modern browsers
1608 if (el.addEventListener) {
1609 el.addEventListener(type, fn, false);
1610
1611 // Handle old IE implementation
1612 } else if (el.attachEvent) {
1613
1614 if (!el.hcEventsIE) {
1615 el.hcEventsIE = {};
1616 }
1617
1618 // Link wrapped fn with original fn, so we can get this in removeEvent
1619 el.hcEventsIE[fn.toString()] = wrappedFn;
1620
1621 el.attachEvent('on' + type, wrappedFn);
1622 }
1623
1624 if (!events[type]) {
1625 events[type] = [];
1626 }
1627
1628 events[type].push(fn);
1629
1630 // Return a function that can be called to remove this event.
1631 return function() {
1632 H.removeEvent(el, type, fn);
1633 };
1634 };
1635
1636 /**
1637 * Remove an event that was added with {@link Highcharts#addEvent}.
1638 *
1639 * @function #removeEvent
1640 * @memberOf Highcharts
1641 * @param {Object} el - The element to remove events on.
1642 * @param {String} [type] - The type of events to remove. If undefined, all
1643 * events are removed from the element.
1644 * @param {Function} [fn] - The specific callback to remove. If undefined, all
1645 * events that match the element and optionally the type are removed.
1646 * @returns {void}
1647 */
1648 H.removeEvent = function(el, type, fn) {
1649
1650 var events,
1651 hcEvents = el.hcEvents,
1652 index;
1653
1654 function removeOneEvent(type, fn) {
1655 if (el.removeEventListener) {
1656 el.removeEventListener(type, fn, false);
1657 } else if (el.attachEvent) {
1658 fn = el.hcEventsIE[fn.toString()];
1659 el.detachEvent('on' + type, fn);
1660 }
1661 }
1662
1663 function removeAllEvents() {
1664 var types,
1665 len,
1666 n;
1667
1668 if (!el.nodeName) {
1669 return; // break on non-DOM events
1670 }
1671
1672 if (type) {
1673 types = {};
1674 types[type] = true;
1675 } else {
1676 types = hcEvents;
1677 }
1678
1679 for (n in types) {
1680 if (hcEvents[n]) {
1681 len = hcEvents[n].length;
1682 while (len--) {
1683 removeOneEvent(n, hcEvents[n][len]);
1684 }
1685 }
1686 }
1687 }
1688
1689 if (hcEvents) {
1690 if (type) {
1691 events = hcEvents[type] || [];
1692 if (fn) {
1693 index = H.inArray(fn, events);
1694 if (index > -1) {
1695 events.splice(index, 1);
1696 hcEvents[type] = events;
1697 }
1698 removeOneEvent(type, fn);
1699
1700 } else {
1701 removeAllEvents();
1702 hcEvents[type] = [];
1703 }
1704 } else {
1705 removeAllEvents();
1706 el.hcEvents = {};
1707 }
1708 }
1709 };
1710
1711 /**
1712 * Fire an event that was registered with {@link Highcharts#addEvent}.
1713 *
1714 * @function #fireEvent
1715 * @memberOf Highcharts
1716 * @param {Object} el - The object to fire the event on. It can be a
1717 * {@link HTMLDOMElement}, an {@link SVGElement} or any other object.
1718 * @param {String} type - The type of event.
1719 * @param {Object} [eventArguments] - Custom event arguments that are passed on
1720 * as an argument to the event handler.
1721 * @param {Function} [defaultFunction] - The default function to execute if the
1722 * other listeners haven't returned false.
1723 * @returns {void}
1724 */
1725 H.fireEvent = function(el, type, eventArguments, defaultFunction) {
1726 var e,
1727 hcEvents = el.hcEvents,
1728 events,
1729 len,
1730 i,
1731 fn;
1732
1733 eventArguments = eventArguments || {};
1734
1735 if (doc.createEvent && (el.dispatchEvent || el.fireEvent)) {
1736 e = doc.createEvent('Events');
1737 e.initEvent(type, true, true);
1738 //e.target = el;
1739
1740 H.extend(e, eventArguments);
1741
1742 if (el.dispatchEvent) {
1743 el.dispatchEvent(e);
1744 } else {
1745 el.fireEvent(type, e);
1746 }
1747
1748 } else if (hcEvents) {
1749
1750 events = hcEvents[type] || [];
1751 len = events.length;
1752
1753 if (!eventArguments.target) { // We're running a custom event
1754
1755 H.extend(eventArguments, {
1756 // Attach a simple preventDefault function to skip default
1757 // handler if called. The built-in defaultPrevented property is
1758 // not overwritable (#5112)
1759 preventDefault: function() {
1760 eventArguments.defaultPrevented = true;
1761 },
1762 // Setting target to native events fails with clicking the
1763 // zoom-out button in Chrome.
1764 target: el,
1765 // If the type is not set, we're running a custom event (#2297).
1766 // If it is set, we're running a browser event, and setting it
1767 // will cause en error in IE8 (#2465).
1768 type: type
1769 });
1770 }
1771
1772
1773 for (i = 0; i < len; i++) {
1774 fn = events[i];
1775
1776 // If the event handler return false, prevent the default handler
1777 // from executing
1778 if (fn && fn.call(el, eventArguments) === false) {
1779 eventArguments.preventDefault();
1780 }
1781 }
1782 }
1783
1784 // Run the default if not prevented
1785 if (defaultFunction && !eventArguments.defaultPrevented) {
1786 defaultFunction(eventArguments);
1787 }
1788 };
1789
1790 /**
1791 * An animation configuration. Animation configurations can also be defined as
1792 * booleans, where `false` turns off animation and `true` defaults to a duration
1793 * of 500ms.
1794 * @typedef {Object} AnimationOptions
1795 * @property {Number} duration - The animation duration in milliseconds.
1796 * @property {String} [easing] - The name of an easing function as defined on
1797 * the `Math` object.
1798 * @property {Function} [complete] - A callback function to exectute when the
1799 * animation finishes.
1800 * @property {Function} [step] - A callback function to execute on each step of
1801 * each attribute or CSS property that's being animated. The first argument
1802 * contains information about the animation and progress.
1803 */
1804
1805
1806 /**
1807 * The global animate method, which uses Fx to create individual animators.
1808 *
1809 * @function #animate
1810 * @memberOf Highcharts
1811 * @param {HTMLDOMElement|SVGElement} el - The element to animate.
1812 * @param {Object} params - An object containing key-value pairs of the
1813 * properties to animate. Supports numeric as pixel-based CSS properties
1814 * for HTML objects and attributes for SVGElements.
1815 * @param {AnimationOptions} [opt] - Animation options.
1816 */
1817 H.animate = function(el, params, opt) {
1818 var start,
1819 unit = '',
1820 end,
1821 fx,
1822 args,
1823 prop;
1824
1825 if (!H.isObject(opt)) { // Number or undefined/null
1826 args = arguments;
1827 opt = {
1828 duration: args[2],
1829 easing: args[3],
1830 complete: args[4]
1831 };
1832 }
1833 if (!H.isNumber(opt.duration)) {
1834 opt.duration = 400;
1835 }
1836 opt.easing = typeof opt.easing === 'function' ?
1837 opt.easing :
1838 (Math[opt.easing] || Math.easeInOutSine);
1839 opt.curAnim = H.merge(params);
1840
1841 for (prop in params) {
1842
1843 // Stop current running animation of this property
1844 H.stop(el, prop);
1845
1846 fx = new H.Fx(el, opt, prop);
1847 end = null;
1848
1849 if (prop === 'd') {
1850 fx.paths = fx.initPath(
1851 el,
1852 el.d,
1853 params.d
1854 );
1855 fx.toD = params.d;
1856 start = 0;
1857 end = 1;
1858 } else if (el.attr) {
1859 start = el.attr(prop);
1860 } else {
1861 start = parseFloat(H.getStyle(el, prop)) || 0;
1862 if (prop !== 'opacity') {
1863 unit = 'px';
1864 }
1865 }
1866
1867 if (!end) {
1868 end = params[prop];
1869 }
1870 if (end.match && end.match('px')) {
1871 end = end.replace(/px/g, ''); // #4351
1872 }
1873 fx.run(start, end, unit);
1874 }
1875 };
1876
1877 /**
1878 * Factory to create new series prototypes.
1879 *
1880 * @function #seriesType
1881 * @memberOf Highcharts
1882 *
1883 * @param {String} type - The series type name.
1884 * @param {String} parent - The parent series type name. Use `line` to inherit
1885 * from the basic {@link Series} object.
1886 * @param {Object} options - The additional default options that is merged with
1887 * the parent's options.
1888 * @param {Object} props - The properties (functions and primitives) to set on
1889 * the new prototype.
1890 * @param {Object} [pointProps] - Members for a series-specific extension of the
1891 * {@link Point} prototype if needed.
1892 * @returns {*} - The newly created prototype as extended from {@link Series}
1893 * or its derivatives.
1894 */
1895 // docs: add to API + extending Highcharts
1896 H.seriesType = function(type, parent, options, props, pointProps) {
1897 var defaultOptions = H.getOptions(),
1898 seriesTypes = H.seriesTypes;
1899
1900 // Merge the options
1901 defaultOptions.plotOptions[type] = H.merge(
1902 defaultOptions.plotOptions[parent],
1903 options
1904 );
1905
1906 // Create the class
1907 seriesTypes[type] = H.extendClass(seriesTypes[parent] ||
1908 function() {}, props);
1909 seriesTypes[type].prototype.type = type;
1910
1911 // Create the point class if needed
1912 if (pointProps) {
1913 seriesTypes[type].prototype.pointClass =
1914 H.extendClass(H.Point, pointProps);
1915 }
1916
1917 return seriesTypes[type];
1918 };
1919
1920 /**
1921 * Get a unique key for using in internal element id's and pointers. The key
1922 * is composed of a random hash specific to this Highcharts instance, and a
1923 * counter.
1924 * @function #uniqueKey
1925 * @memberOf Highcharts
1926 * @return {string} The key.
1927 * @example
1928 * var id = H.uniqueKey(); // => 'highcharts-x45f6hp-0'
1929 */
1930 H.uniqueKey = (function() {
1931
1932 var uniqueKeyHash = Math.random().toString(36).substring(2, 9),
1933 idCounter = 0;
1934
1935 return function() {
1936 return 'highcharts-' + uniqueKeyHash + '-' + idCounter++;
1937 };
1938 }());
1939
1940 /**
1941 * Register Highcharts as a plugin in jQuery
1942 */
1943 if (win.jQuery) {
1944 win.jQuery.fn.highcharts = function() {
1945 var args = [].slice.call(arguments);
1946
1947 if (this[0]) { // this[0] is the renderTo div
1948
1949 // Create the chart
1950 if (args[0]) {
1951 new H[ // eslint-disable-line no-new
1952 // Constructor defaults to Chart
1953 H.isString(args[0]) ? args.shift() : 'Chart'
1954 ](this[0], args[0], args[1]);
1955 return this;
1956 }
1957
1958 // When called without parameters or with the return argument,
1959 // return an existing chart
1960 return charts[H.attr(this[0], 'data-highcharts-chart')];
1961 }
1962 };
1963 }
1964
1965
1966 /**
1967 * Compatibility section to add support for legacy IE. This can be removed if
1968 * old IE support is not needed.
1969 */
1970 if (doc && !doc.defaultView) {
1971 H.getStyle = function(el, prop) {
1972 var val,
1973 alias = {
1974 width: 'clientWidth',
1975 height: 'clientHeight'
1976 }[prop];
1977
1978 if (el.style[prop]) {
1979 return H.pInt(el.style[prop]);
1980 }
1981 if (prop === 'opacity') {
1982 prop = 'filter';
1983 }
1984
1985 // Getting the rendered width and height
1986 if (alias) {
1987 el.style.zoom = 1;
1988 return Math.max(el[alias] - 2 * H.getStyle(el, 'padding'), 0);
1989 }
1990
1991 val = el.currentStyle[prop.replace(/\-(\w)/g, function(a, b) {
1992 return b.toUpperCase();
1993 })];
1994 if (prop === 'filter') {
1995 val = val.replace(
1996 /alpha\(opacity=([0-9]+)\)/,
1997 function(a, b) {
1998 return b / 100;
1999 }
2000 );
2001 }
2002
2003 return val === '' ? 1 : H.pInt(val);
2004 };
2005 }
2006
2007 if (!Array.prototype.forEach) {
2008 H.each = function(arr, fn, ctx) { // legacy
2009 var i = 0,
2010 len = arr.length;
2011 for (; i < len; i++) {
2012 if (fn.call(ctx, arr[i], i, arr) === false) {
2013 return i;
2014 }
2015 }
2016 };
2017 }
2018
2019 if (!Array.prototype.indexOf) {
2020 H.inArray = function(item, arr) {
2021 var len,
2022 i = 0;
2023
2024 if (arr) {
2025 len = arr.length;
2026
2027 for (; i < len; i++) {
2028 if (arr[i] === item) {
2029 return i;
2030 }
2031 }
2032 }
2033
2034 return -1;
2035 };
2036 }
2037
2038 if (!Array.prototype.filter) {
2039 H.grep = function(elements, fn) {
2040 var ret = [],
2041 i = 0,
2042 length = elements.length;
2043
2044 for (; i < length; i++) {
2045 if (fn(elements[i], i)) {
2046 ret.push(elements[i]);
2047 }
2048 }
2049
2050 return ret;
2051 };
2052 }
2053
2054 //--- End compatibility section ---
2055
2056 }(Highcharts));
2057 (function(H) {
2058 /**
2059 * (c) 2010-2016 Torstein Honsi
2060 *
2061 * License: www.highcharts.com/license
2062 */
2063 'use strict';
2064 var each = H.each,
2065 isNumber = H.isNumber,
2066 map = H.map,
2067 merge = H.merge,
2068 pInt = H.pInt;
2069
2070 /**
2071 * @typedef {string} ColorString
2072 * A valid color to be parsed and handled by Highcharts. Highcharts internally
2073 * supports hex colors like `#ffffff`, rgb colors like `rgb(255,255,255)` and
2074 * rgba colors like `rgba(255,255,255,1)`. Other colors may be supported by the
2075 * browsers and displayed correctly, but Highcharts is not able to process them
2076 * and apply concepts like opacity and brightening.
2077 */
2078 /**
2079 * Handle color operations. The object methods are chainable.
2080 * @param {String} input The input color in either rbga or hex format
2081 */
2082 H.Color = function(input) {
2083 // Backwards compatibility, allow instanciation without new
2084 if (!(this instanceof H.Color)) {
2085 return new H.Color(input);
2086 }
2087 // Initialize
2088 this.init(input);
2089 };
2090 H.Color.prototype = {
2091
2092 // Collection of parsers. This can be extended from the outside by pushing parsers
2093 // to Highcharts.Color.prototype.parsers.
2094 parsers: [{
2095 // RGBA color
2096 regex: /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*\)/,
2097 parse: function(result) {
2098 return [pInt(result[1]), pInt(result[2]), pInt(result[3]), parseFloat(result[4], 10)];
2099 }
2100 }, {
2101 // HEX color
2102 regex: /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/,
2103 parse: function(result) {
2104 return [pInt(result[1], 16), pInt(result[2], 16), pInt(result[3], 16), 1];
2105 }
2106 }, {
2107 // RGB color
2108 regex: /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/,
2109 parse: function(result) {
2110 return [pInt(result[1]), pInt(result[2]), pInt(result[3]), 1];
2111 }
2112 }],
2113
2114 // Collection of named colors. Can be extended from the outside by adding colors
2115 // to Highcharts.Color.prototype.names.
2116 names: {
2117 white: '#ffffff',
2118 black: '#000000'
2119 },
2120
2121 /**
2122 * Parse the input color to rgba array
2123 * @param {String} input
2124 */
2125 init: function(input) {
2126 var result,
2127 rgba,
2128 i,
2129 parser;
2130
2131 this.input = input = this.names[input] || input;
2132
2133 // Gradients
2134 if (input && input.stops) {
2135 this.stops = map(input.stops, function(stop) {
2136 return new H.Color(stop[1]);
2137 });
2138
2139 // Solid colors
2140 } else {
2141 i = this.parsers.length;
2142 while (i-- && !rgba) {
2143 parser = this.parsers[i];
2144 result = parser.regex.exec(input);
2145 if (result) {
2146 rgba = parser.parse(result);
2147 }
2148 }
2149 }
2150 this.rgba = rgba || [];
2151 },
2152
2153 /**
2154 * Return the color a specified format
2155 * @param {String} format
2156 */
2157 get: function(format) {
2158 var input = this.input,
2159 rgba = this.rgba,
2160 ret;
2161
2162 if (this.stops) {
2163 ret = merge(input);
2164 ret.stops = [].concat(ret.stops);
2165 each(this.stops, function(stop, i) {
2166 ret.stops[i] = [ret.stops[i][0], stop.get(format)];
2167 });
2168
2169 // it's NaN if gradient colors on a column chart
2170 } else if (rgba && isNumber(rgba[0])) {
2171 if (format === 'rgb' || (!format && rgba[3] === 1)) {
2172 ret = 'rgb(' + rgba[0] + ',' + rgba[1] + ',' + rgba[2] + ')';
2173 } else if (format === 'a') {
2174 ret = rgba[3];
2175 } else {
2176 ret = 'rgba(' + rgba.join(',') + ')';
2177 }
2178 } else {
2179 ret = input;
2180 }
2181 return ret;
2182 },
2183
2184 /**
2185 * Brighten the color
2186 * @param {Number} alpha
2187 */
2188 brighten: function(alpha) {
2189 var i,
2190 rgba = this.rgba;
2191
2192 if (this.stops) {
2193 each(this.stops, function(stop) {
2194 stop.brighten(alpha);
2195 });
2196
2197 } else if (isNumber(alpha) && alpha !== 0) {
2198 for (i = 0; i < 3; i++) {
2199 rgba[i] += pInt(alpha * 255);
2200
2201 if (rgba[i] < 0) {
2202 rgba[i] = 0;
2203 }
2204 if (rgba[i] > 255) {
2205 rgba[i] = 255;
2206 }
2207 }
2208 }
2209 return this;
2210 },
2211
2212 /**
2213 * Set the color's opacity to a given alpha value
2214 * @param {Number} alpha
2215 */
2216 setOpacity: function(alpha) {
2217 this.rgba[3] = alpha;
2218 return this;
2219 }
2220 };
2221 H.color = function(input) {
2222 return new H.Color(input);
2223 };
2224
2225 }(Highcharts));
2226 (function(H) {
2227 /**
2228 * (c) 2010-2016 Torstein Honsi
2229 *
2230 * License: www.highcharts.com/license
2231 */
2232 'use strict';
2233 var SVGElement,
2234 SVGRenderer,
2235
2236 addEvent = H.addEvent,
2237 animate = H.animate,
2238 attr = H.attr,
2239 charts = H.charts,
2240 color = H.color,
2241 css = H.css,
2242 createElement = H.createElement,
2243 defined = H.defined,
2244 deg2rad = H.deg2rad,
2245 destroyObjectProperties = H.destroyObjectProperties,
2246 doc = H.doc,
2247 each = H.each,
2248 extend = H.extend,
2249 erase = H.erase,
2250 grep = H.grep,
2251 hasTouch = H.hasTouch,
2252 isArray = H.isArray,
2253 isFirefox = H.isFirefox,
2254 isMS = H.isMS,
2255 isObject = H.isObject,
2256 isString = H.isString,
2257 isWebKit = H.isWebKit,
2258 merge = H.merge,
2259 noop = H.noop,
2260 pick = H.pick,
2261 pInt = H.pInt,
2262 removeEvent = H.removeEvent,
2263 splat = H.splat,
2264 stop = H.stop,
2265 svg = H.svg,
2266 SVG_NS = H.SVG_NS,
2267 symbolSizes = H.symbolSizes,
2268 win = H.win;
2269
2270 /**
2271 * @typedef {Object} SVGDOMElement - An SVG DOM element.
2272 */
2273 /**
2274 * The SVGElement prototype is a JavaScript wrapper for SVG elements used in the
2275 * rendering layer of Highcharts. Combined with the {@link SVGRenderer} object,
2276 * these prototypes allow freeform annotation in the charts or even in HTML
2277 * pages without instanciating a chart. The SVGElement can also wrap HTML
2278 * labels, when `text` or `label` elements are created with the `useHTML`
2279 * parameter.
2280 *
2281 * The SVGElement instances are created through factory functions on the
2282 * {@link SVGRenderer} object, like [rect]{@link SVGRenderer#rect},
2283 * [path]{@link SVGRenderer#path}, [text]{@link SVGRenderer#text}, [label]{@link
2284 * SVGRenderer#label}, [g]{@link SVGRenderer#g} and more.
2285 *
2286 * @class
2287 */
2288 SVGElement = H.SVGElement = function() {
2289 return this;
2290 };
2291 SVGElement.prototype = {
2292
2293 // Default base for animation
2294 opacity: 1,
2295 SVG_NS: SVG_NS,
2296
2297 /**
2298 * For labels, these CSS properties are applied to the `text` node directly.
2299 * @type {Array.<string>}
2300 */
2301 textProps: ['direction', 'fontSize', 'fontWeight', 'fontFamily',
2302 'fontStyle', 'color', 'lineHeight', 'width', 'textDecoration',
2303 'textOverflow', 'textOutline'
2304 ],
2305
2306 /**
2307 * Initialize the SVG renderer. This function only exists to make the
2308 * initiation process overridable. It should not be called directly.
2309 *
2310 * @param {SVGRenderer} renderer The SVGRenderer instance to initialize to.
2311 * @param {String} nodeName The SVG node name.
2312 * @returns {void}
2313 */
2314 init: function(renderer, nodeName) {
2315
2316 /**
2317 * The DOM node. Each SVGRenderer instance wraps a main DOM node, but
2318 * may also represent more nodes.
2319 * @type {SVGDOMNode|HTMLDOMNode}
2320 */
2321 this.element = nodeName === 'span' ?
2322 createElement(nodeName) :
2323 doc.createElementNS(this.SVG_NS, nodeName);
2324
2325 /**
2326 * The renderer that the SVGElement belongs to.
2327 * @type {SVGRenderer}
2328 */
2329 this.renderer = renderer;
2330 },
2331
2332 /**
2333 * Animate to given attributes or CSS properties.
2334 *
2335 * @param {SVGAttributes} params SVG attributes or CSS to animate.
2336 * @param {AnimationOptions} [options] Animation options.
2337 * @param {Function} [complete] Function to perform at the end of animation.
2338 * @returns {SVGElement} Returns the SVGElement for chaining.
2339 */
2340 animate: function(params, options, complete) {
2341 var animOptions = pick(options, this.renderer.globalAnimation, true);
2342 if (animOptions) {
2343 if (complete) { // allows using a callback with the global animation without overwriting it
2344 animOptions.complete = complete;
2345 }
2346 animate(this, params, animOptions);
2347 } else {
2348 this.attr(params, null, complete);
2349 }
2350 return this;
2351 },
2352
2353 /**
2354 * @typedef {Object} GradientOptions
2355 * @property {Object} linearGradient Holds an object that defines the start
2356 * position and the end position relative to the shape.
2357 * @property {Number} linearGradient.x1 Start horizontal position of the
2358 * gradient. Ranges 0-1.
2359 * @property {Number} linearGradient.x2 End horizontal position of the
2360 * gradient. Ranges 0-1.
2361 * @property {Number} linearGradient.y1 Start vertical position of the
2362 * gradient. Ranges 0-1.
2363 * @property {Number} linearGradient.y2 End vertical position of the
2364 * gradient. Ranges 0-1.
2365 * @property {Object} radialGradient Holds an object that defines the center
2366 * position and the radius.
2367 * @property {Number} radialGradient.cx Center horizontal position relative
2368 * to the shape. Ranges 0-1.
2369 * @property {Number} radialGradient.cy Center vertical position relative
2370 * to the shape. Ranges 0-1.
2371 * @property {Number} radialGradient.r Radius relative to the shape. Ranges
2372 * 0-1.
2373 * @property {Array.<Array>} stops The first item in each tuple is the
2374 * position in the gradient, where 0 is the start of the gradient and 1
2375 * is the end of the gradient. Multiple stops can be applied. The second
2376 * item is the color for each stop. This color can also be given in the
2377 * rgba format.
2378 *
2379 * @example
2380 * // Linear gradient used as a color option
2381 * color: {
2382 * linearGradient: { x1: 0, x2: 0, y1: 0, y2: 1 },
2383 * stops: [
2384 * [0, '#003399'], // start
2385 * [0.5, '#ffffff'], // middle
2386 * [1, '#3366AA'] // end
2387 * ]
2388 * }
2389 * }
2390 */
2391 /**
2392 * Build and apply an SVG gradient out of a common JavaScript configuration
2393 * object. This function is called from the attribute setters.
2394 *
2395 * @private
2396 * @param {GradientOptions} color The gradient options structure.
2397 * @param {string} prop The property to apply, can either be `fill` or
2398 * `stroke`.
2399 * @param {SVGDOMElement} elem SVG DOM element to apply the gradient on.
2400 */
2401 colorGradient: function(color, prop, elem) {
2402 var renderer = this.renderer,
2403 colorObject,
2404 gradName,
2405 gradAttr,
2406 radAttr,
2407 gradients,
2408 gradientObject,
2409 stops,
2410 stopColor,
2411 stopOpacity,
2412 radialReference,
2413 n,
2414 id,
2415 key = [],
2416 value;
2417
2418 // Apply linear or radial gradients
2419 if (color.linearGradient) {
2420 gradName = 'linearGradient';
2421 } else if (color.radialGradient) {
2422 gradName = 'radialGradient';
2423 }
2424
2425 if (gradName) {
2426 gradAttr = color[gradName];
2427 gradients = renderer.gradients;
2428 stops = color.stops;
2429 radialReference = elem.radialReference;
2430
2431 // Keep < 2.2 kompatibility
2432 if (isArray(gradAttr)) {
2433 color[gradName] = gradAttr = {
2434 x1: gradAttr[0],
2435 y1: gradAttr[1],
2436 x2: gradAttr[2],
2437 y2: gradAttr[3],
2438 gradientUnits: 'userSpaceOnUse'
2439 };
2440 }
2441
2442 // Correct the radial gradient for the radial reference system
2443 if (gradName === 'radialGradient' && radialReference && !defined(gradAttr.gradientUnits)) {
2444 radAttr = gradAttr; // Save the radial attributes for updating
2445 gradAttr = merge(gradAttr,
2446 renderer.getRadialAttr(radialReference, radAttr), {
2447 gradientUnits: 'userSpaceOnUse'
2448 }
2449 );
2450 }
2451
2452 // Build the unique key to detect whether we need to create a new element (#1282)
2453 for (n in gradAttr) {
2454 if (n !== 'id') {
2455 key.push(n, gradAttr[n]);
2456 }
2457 }
2458 for (n in stops) {
2459 key.push(stops[n]);
2460 }
2461 key = key.join(',');
2462
2463 // Check if a gradient object with the same config object is created within this renderer
2464 if (gradients[key]) {
2465 id = gradients[key].attr('id');
2466
2467 } else {
2468
2469 // Set the id and create the element
2470 gradAttr.id = id = H.uniqueKey();
2471 gradients[key] = gradientObject = renderer.createElement(gradName)
2472 .attr(gradAttr)
2473 .add(renderer.defs);
2474
2475 gradientObject.radAttr = radAttr;
2476
2477 // The gradient needs to keep a list of stops to be able to destroy them
2478 gradientObject.stops = [];
2479 each(stops, function(stop) {
2480 var stopObject;
2481 if (stop[1].indexOf('rgba') === 0) {
2482 colorObject = H.color(stop[1]);
2483 stopColor = colorObject.get('rgb');
2484 stopOpacity = colorObject.get('a');
2485 } else {
2486 stopColor = stop[1];
2487 stopOpacity = 1;
2488 }
2489 stopObject = renderer.createElement('stop').attr({
2490 offset: stop[0],
2491 'stop-color': stopColor,
2492 'stop-opacity': stopOpacity
2493 }).add(gradientObject);
2494
2495 // Add the stop element to the gradient
2496 gradientObject.stops.push(stopObject);
2497 });
2498 }
2499
2500 // Set the reference to the gradient object
2501 value = 'url(' + renderer.url + '#' + id + ')';
2502 elem.setAttribute(prop, value);
2503 elem.gradient = key;
2504
2505 // Allow the color to be concatenated into tooltips formatters etc. (#2995)
2506 color.toString = function() {
2507 return value;
2508 };
2509 }
2510 },
2511
2512 /**
2513 * Apply a text outline through a custom CSS property, by copying the text
2514 * element and apply stroke to the copy. Used internally. Contrast checks
2515 * at http://jsfiddle.net/highcharts/43soe9m1/2/ .
2516 *
2517 * @private
2518 * @param {String} textOutline A custom CSS `text-outline` setting, defined
2519 * by `width color`.
2520 * @example
2521 * // Specific color
2522 * text.css({
2523 * textOutline: '1px black'
2524 * });
2525 * // Automatic contrast
2526 * text.css({
2527 * color: '#000000', // black text
2528 * textOutline: '1px contrast' // => white outline
2529 * });
2530 */
2531 applyTextOutline: function(textOutline) {
2532 var elem = this.element,
2533 tspans,
2534 hasContrast = textOutline.indexOf('contrast') !== -1,
2535 styles = {},
2536 color,
2537 strokeWidth;
2538
2539 // When the text shadow is set to contrast, use dark stroke for light
2540 // text and vice versa.
2541 if (hasContrast) {
2542 styles.textOutline = textOutline = textOutline.replace(
2543 /contrast/g,
2544 this.renderer.getContrast(elem.style.fill)
2545 );
2546 }
2547
2548 this.fakeTS = true; // Fake text shadow
2549
2550 // In order to get the right y position of the clone,
2551 // copy over the y setter
2552 this.ySetter = this.xSetter;
2553
2554 tspans = [].slice.call(elem.getElementsByTagName('tspan'));
2555
2556 // Extract the stroke width and color
2557 textOutline = textOutline.split(' ');
2558 color = textOutline[textOutline.length - 1];
2559 strokeWidth = textOutline[0];
2560
2561 if (strokeWidth && strokeWidth !== 'none') {
2562
2563 // Since the stroke is applied on center of the actual outline, we
2564 // need to double it to get the correct stroke-width outside the
2565 // glyphs.
2566 strokeWidth = strokeWidth.replace(
2567 /(^[\d\.]+)(.*?)$/g,
2568 function(match, digit, unit) {
2569 return (2 * digit) + unit;
2570 }
2571 );
2572
2573 // Remove shadows from previous runs
2574 each(tspans, function(tspan) {
2575 if (tspan.getAttribute('class') === 'highcharts-text-outline') {
2576 // Remove then erase
2577 erase(tspans, elem.removeChild(tspan));
2578 }
2579 });
2580
2581 // For each of the tspans, create a stroked copy behind it.
2582 each(tspans, function(tspan, y) {
2583 var clone;
2584
2585 // Let the first line start at the correct X position
2586 if (y === 0) {
2587 tspan.setAttribute('x', elem.getAttribute('x'));
2588 y = elem.getAttribute('y');
2589 tspan.setAttribute('y', y || 0);
2590 if (y === null) {
2591 elem.setAttribute('y', 0);
2592 }
2593 }
2594
2595 // Create the clone and apply outline properties
2596 clone = tspan.cloneNode(1);
2597 attr(clone, {
2598 'class': 'highcharts-text-outline',
2599 'fill': color,
2600 'stroke': color,
2601 'stroke-width': strokeWidth,
2602 'stroke-linejoin': 'round'
2603 });
2604 elem.insertBefore(clone, elem.firstChild);
2605 });
2606 }
2607 },
2608
2609 /**
2610 *
2611 * @typedef {Object} SVGAttributes An object of key-value pairs for SVG
2612 * attributes. Attributes in Highcharts elements for the most parts
2613 * correspond to SVG, but some are specific to Highcharts, like `zIndex`,
2614 * `rotation`, `translateX`, `translateY`, `scaleX` and `scaleY`. SVG
2615 * attributes containing a hyphen are _not_ camel-cased, they should be
2616 * quoted to preserve the hyphen.
2617 * @example
2618 * {
2619 * 'stroke': '#ff0000', // basic
2620 * 'stroke-width': 2, // hyphenated
2621 * 'rotation': 45 // custom
2622 * 'd': ['M', 10, 10, 'L', 30, 30, 'z'] // path definition, note format
2623 * }
2624 */
2625 /**
2626 * Apply native and custom attributes to the SVG elements.
2627 *
2628 * In order to set the rotation center for rotation, set x and y to 0 and
2629 * use `translateX` and `translateY` attributes to position the element
2630 * instead.
2631 *
2632 * Attributes frequently used in Highcharts are `fill`, `stroke`,
2633 * `stroke-width`.
2634 *
2635 * @param {SVGAttributes|String} hash - The native and custom SVG
2636 * attributes.
2637 * @param {string} [val] - If the type of the first argument is `string`,
2638 * the second can be a value, which will serve as a single attribute
2639 * setter. If the first argument is a string and the second is undefined,
2640 * the function serves as a getter and the current value of the property
2641 * is returned.
2642 * @param {Function} complete - A callback function to execute after setting
2643 * the attributes. This makes the function compliant and interchangeable
2644 * with the {@link SVGElement#animate} function.
2645 * @param {boolean} continueAnimation - Used internally when `.attr` is
2646 * called as part of an animation step. Otherwise, calling `.attr` for an
2647 * attribute will stop animation for that attribute.
2648 *
2649 * @returns {SVGElement|string|number} If used as a setter, it returns the
2650 * current {@link SVGElement} so the calls can be chained. If used as a
2651 * getter, the current value of the attribute is returned.
2652 *
2653 * @example
2654 * // Set multiple attributes
2655 * element.attr({
2656 * stroke: 'red',
2657 * fill: 'blue',
2658 * x: 10,
2659 * y: 10
2660 * });
2661 *
2662 * // Set a single attribute
2663 * element.attr('stroke', 'red');
2664 *
2665 * // Get an attribute
2666 * element.attr('stroke'); // => 'red'
2667 *
2668 */
2669 attr: function(hash, val, complete, continueAnimation) {
2670 var key,
2671 value,
2672 element = this.element,
2673 hasSetSymbolSize,
2674 ret = this,
2675 skipAttr,
2676 setter;
2677
2678 // single key-value pair
2679 if (typeof hash === 'string' && val !== undefined) {
2680 key = hash;
2681 hash = {};
2682 hash[key] = val;
2683 }
2684
2685 // used as a getter: first argument is a string, second is undefined
2686 if (typeof hash === 'string') {
2687 ret = (this[hash + 'Getter'] || this._defaultGetter).call(this, hash, element);
2688
2689 // setter
2690 } else {
2691
2692 for (key in hash) {
2693 value = hash[key];
2694 skipAttr = false;
2695
2696 // Unless .attr is from the animator update, stop current
2697 // running animation of this property
2698 if (!continueAnimation) {
2699 stop(this, key);
2700 }
2701
2702 if (this.symbolName && /^(x|y|width|height|r|start|end|innerR|anchorX|anchorY)/.test(key)) {
2703 if (!hasSetSymbolSize) {
2704 this.symbolAttr(hash);
2705 hasSetSymbolSize = true;
2706 }
2707 skipAttr = true;
2708 }
2709
2710 if (this.rotation && (key === 'x' || key === 'y')) {
2711 this.doTransform = true;
2712 }
2713
2714 if (!skipAttr) {
2715 setter = this[key + 'Setter'] || this._defaultSetter;
2716 setter.call(this, value, key, element);
2717
2718
2719 // Let the shadow follow the main element
2720 if (this.shadows && /^(width|height|visibility|x|y|d|transform|cx|cy|r)$/.test(key)) {
2721 this.updateShadows(key, value, setter);
2722 }
2723
2724 }
2725 }
2726
2727 // Update transform. Do this outside the loop to prevent redundant updating for batch setting
2728 // of attributes.
2729 if (this.doTransform) {
2730 this.updateTransform();
2731 this.doTransform = false;
2732 }
2733
2734 }
2735
2736 // In accordance with animate, run a complete callback
2737 if (complete) {
2738 complete();
2739 }
2740
2741 return ret;
2742 },
2743
2744
2745 /**
2746 * Update the shadow elements with new attributes.
2747 *
2748 * @private
2749 * @param {String} key - The attribute name.
2750 * @param {String|Number} value - The value of the attribute.
2751 * @param {Function} setter - The setter function, inherited from the
2752 * parent wrapper
2753 * @returns {void}
2754 */
2755 updateShadows: function(key, value, setter) {
2756 var shadows = this.shadows,
2757 i = shadows.length;
2758
2759 while (i--) {
2760 setter.call(
2761 shadows[i],
2762 key === 'height' ?
2763 Math.max(value - (shadows[i].cutHeight || 0), 0) :
2764 key === 'd' ? this.d : value,
2765 key,
2766 shadows[i]
2767 );
2768 }
2769 },
2770
2771
2772 /**
2773 * Add a class name to an element.
2774 *
2775 * @param {string} className - The new class name to add.
2776 * @param {boolean} [replace=false] - When true, the existing class name(s)
2777 * will be overwritten with the new one. When false, the new one is
2778 * added.
2779 * @returns {SVGElement} Return the SVG element for chainability.
2780 */
2781 addClass: function(className, replace) {
2782 var currentClassName = this.attr('class') || '';
2783
2784 if (currentClassName.indexOf(className) === -1) {
2785 if (!replace) {
2786 className =
2787 (currentClassName + (currentClassName ? ' ' : '') +
2788 className).replace(' ', ' ');
2789 }
2790 this.attr('class', className);
2791 }
2792 return this;
2793 },
2794
2795 /**
2796 * Check if an element has the given class name.
2797 * @param {string} className - The class name to check for.
2798 * @return {Boolean}
2799 */
2800 hasClass: function(className) {
2801 return attr(this.element, 'class').indexOf(className) !== -1;
2802 },
2803
2804 /**
2805 * Remove a class name from the element.
2806 * @param {string} className The class name to remove.
2807 * @return {SVGElement} Returns the SVG element for chainability.
2808 */
2809 removeClass: function(className) {
2810 attr(this.element, 'class', (attr(this.element, 'class') || '').replace(className, ''));
2811 return this;
2812 },
2813
2814 /**
2815 * If one of the symbol size affecting parameters are changed,
2816 * check all the others only once for each call to an element's
2817 * .attr() method
2818 * @param {Object} hash - The attributes to set.
2819 * @private
2820 */
2821 symbolAttr: function(hash) {
2822 var wrapper = this;
2823
2824 each(['x', 'y', 'r', 'start', 'end', 'width', 'height', 'innerR', 'anchorX', 'anchorY'], function(key) {
2825 wrapper[key] = pick(hash[key], wrapper[key]);
2826 });
2827
2828 wrapper.attr({
2829 d: wrapper.renderer.symbols[wrapper.symbolName](
2830 wrapper.x,
2831 wrapper.y,
2832 wrapper.width,
2833 wrapper.height,
2834 wrapper
2835 )
2836 });
2837 },
2838
2839 /**
2840 * Apply a clipping rectangle to this element.
2841 *
2842 * @param {ClipRect} [clipRect] - The clipping rectangle. If skipped, the
2843 * current clip is removed.
2844 * @returns {SVGElement} Returns the SVG element to allow chaining.
2845 */
2846 clip: function(clipRect) {
2847 return this.attr(
2848 'clip-path',
2849 clipRect ?
2850 'url(' + this.renderer.url + '#' + clipRect.id + ')' :
2851 'none'
2852 );
2853 },
2854
2855 /**
2856 * Calculate the coordinates needed for drawing a rectangle crisply and
2857 * return the calculated attributes.
2858 *
2859 * @param {Object} rect - A rectangle.
2860 * @param {number} rect.x - The x position.
2861 * @param {number} rect.y - The y position.
2862 * @param {number} rect.width - The width.
2863 * @param {number} rect.height - The height.
2864 * @param {number} [strokeWidth] - The stroke width to consider when
2865 * computing crisp positioning. It can also be set directly on the rect
2866 * parameter.
2867 *
2868 * @returns {{x: Number, y: Number, width: Number, height: Number}} The
2869 * modified rectangle arguments.
2870 */
2871 crisp: function(rect, strokeWidth) {
2872
2873 var wrapper = this,
2874 key,
2875 attribs = {},
2876 normalizer;
2877
2878 strokeWidth = strokeWidth || rect.strokeWidth || 0;
2879 normalizer = Math.round(strokeWidth) % 2 / 2; // Math.round because strokeWidth can sometimes have roundoff errors
2880
2881 // normalize for crisp edges
2882 rect.x = Math.floor(rect.x || wrapper.x || 0) + normalizer;
2883 rect.y = Math.floor(rect.y || wrapper.y || 0) + normalizer;
2884 rect.width = Math.floor((rect.width || wrapper.width || 0) - 2 * normalizer);
2885 rect.height = Math.floor((rect.height || wrapper.height || 0) - 2 * normalizer);
2886 if (defined(rect.strokeWidth)) {
2887 rect.strokeWidth = strokeWidth;
2888 }
2889
2890 for (key in rect) {
2891 if (wrapper[key] !== rect[key]) { // only set attribute if changed
2892 wrapper[key] = attribs[key] = rect[key];
2893 }
2894 }
2895
2896 return attribs;
2897 },
2898
2899 /**
2900 * Set styles for the element. In addition to CSS styles supported by
2901 * native SVG and HTML elements, there are also some custom made for
2902 * Highcharts, like `width`, `ellipsis` and `textOverflow` for SVG text
2903 * elements.
2904 * @param {CSSObject} styles The new CSS styles.
2905 * @returns {SVGElement} Return the SVG element for chaining.
2906 */
2907 css: function(styles) {
2908 var elemWrapper = this,
2909 oldStyles = elemWrapper.styles,
2910 newStyles = {},
2911 elem = elemWrapper.element,
2912 textWidth,
2913 n,
2914 serializedCss = '',
2915 hyphenate,
2916 hasNew = !oldStyles;
2917
2918 // convert legacy
2919 if (styles && styles.color) {
2920 styles.fill = styles.color;
2921 }
2922
2923 // Filter out existing styles to increase performance (#2640)
2924 if (oldStyles) {
2925 for (n in styles) {
2926 if (styles[n] !== oldStyles[n]) {
2927 newStyles[n] = styles[n];
2928 hasNew = true;
2929 }
2930 }
2931 }
2932 if (hasNew) {
2933 textWidth = elemWrapper.textWidth =
2934 (styles && styles.width && elem.nodeName.toLowerCase() === 'text' && pInt(styles.width)) ||
2935 elemWrapper.textWidth; // #3501
2936
2937 // Merge the new styles with the old ones
2938 if (oldStyles) {
2939 styles = extend(
2940 oldStyles,
2941 newStyles
2942 );
2943 }
2944
2945 // store object
2946 elemWrapper.styles = styles;
2947
2948 if (textWidth && (!svg && elemWrapper.renderer.forExport)) {
2949 delete styles.width;
2950 }
2951
2952 // serialize and set style attribute
2953 if (isMS && !svg) {
2954 css(elemWrapper.element, styles);
2955 } else {
2956 hyphenate = function(a, b) {
2957 return '-' + b.toLowerCase();
2958 };
2959 for (n in styles) {
2960 serializedCss += n.replace(/([A-Z])/g, hyphenate) + ':' + styles[n] + ';';
2961 }
2962 attr(elem, 'style', serializedCss); // #1881
2963 }
2964
2965
2966 if (elemWrapper.added) {
2967 // Rebuild text after added
2968 if (textWidth) {
2969 elemWrapper.renderer.buildText(elemWrapper);
2970 }
2971
2972 // Apply text outline after added
2973 if (styles && styles.textOutline) {
2974 elemWrapper.applyTextOutline(styles.textOutline);
2975 }
2976 }
2977 }
2978
2979 return elemWrapper;
2980 },
2981
2982
2983 /**
2984 * Get the current stroke width. In classic mode, the setter registers it
2985 * directly on the element.
2986 * @returns {number} The stroke width in pixels.
2987 * @ignore
2988 */
2989 strokeWidth: function() {
2990 return this['stroke-width'] || 0;
2991 },
2992
2993
2994 /**
2995 * Add an event listener. This is a simple setter that replaces all other
2996 * events of the same type, opposed to the {@link Highcharts#addEvent}
2997 * function.
2998 * @param {string} eventType - The event type. If the type is `click`,
2999 * Highcharts will internally translate it to a `touchstart` event on
3000 * touch devices, to prevent the browser from waiting for a click event
3001 * from firing.
3002 * @param {Function} handler - The handler callback.
3003 * @returns {SVGElement} The SVGElement for chaining.
3004 */
3005 on: function(eventType, handler) {
3006 var svgElement = this,
3007 element = svgElement.element;
3008
3009 // touch
3010 if (hasTouch && eventType === 'click') {
3011 element.ontouchstart = function(e) {
3012 svgElement.touchEventFired = Date.now(); // #2269
3013 e.preventDefault();
3014 handler.call(element, e);
3015 };
3016 element.onclick = function(e) {
3017 if (win.navigator.userAgent.indexOf('Android') === -1 ||
3018 Date.now() - (svgElement.touchEventFired || 0) > 1100) {
3019 handler.call(element, e);
3020 }
3021 };
3022 } else {
3023 // simplest possible event model for internal use
3024 element['on' + eventType] = handler;
3025 }
3026 return this;
3027 },
3028
3029 /**
3030 * Set the coordinates needed to draw a consistent radial gradient across
3031 * a shape regardless of positioning inside the chart. Used on pie slices
3032 * to make all the slices have the same radial reference point.
3033 *
3034 * @param {Array} coordinates The center reference. The format is
3035 * `[centerX, centerY, diameter]` in pixels.
3036 * @returns {SVGElement} Returns the SVGElement for chaining.
3037 */
3038 setRadialReference: function(coordinates) {
3039 var existingGradient = this.renderer.gradients[this.element.gradient];
3040
3041 this.element.radialReference = coordinates;
3042
3043 // On redrawing objects with an existing gradient, the gradient needs
3044 // to be repositioned (#3801)
3045 if (existingGradient && existingGradient.radAttr) {
3046 existingGradient.animate(
3047 this.renderer.getRadialAttr(
3048 coordinates,
3049 existingGradient.radAttr
3050 )
3051 );
3052 }
3053
3054 return this;
3055 },
3056
3057 /**
3058 * Move an object and its children by x and y values.
3059 *
3060 * @param {number} x - The x value.
3061 * @param {number} y - The y value.
3062 */
3063 translate: function(x, y) {
3064 return this.attr({
3065 translateX: x,
3066 translateY: y
3067 });
3068 },
3069
3070 /**
3071 * Invert a group, rotate and flip. This is used internally on inverted
3072 * charts, where the points and graphs are drawn as if not inverted, then
3073 * the series group elements are inverted.
3074 *
3075 * @param {boolean} inverted - Whether to invert or not. An inverted shape
3076 * can be un-inverted by setting it to false.
3077 * @returns {SVGElement} Return the SVGElement for chaining.
3078 */
3079 invert: function(inverted) {
3080 var wrapper = this;
3081 wrapper.inverted = inverted;
3082 wrapper.updateTransform();
3083 return wrapper;
3084 },
3085
3086 /**
3087 * Update the transform attribute based on internal properties. Deals with
3088 * the custom `translateX`, `translateY`, `rotation`, `scaleX` and `scaleY`
3089 * attributes and updates the SVG `transform` attribute.
3090 * @private
3091 * @returns {void}
3092 */
3093 updateTransform: function() {
3094 var wrapper = this,
3095 translateX = wrapper.translateX || 0,
3096 translateY = wrapper.translateY || 0,
3097 scaleX = wrapper.scaleX,
3098 scaleY = wrapper.scaleY,
3099 inverted = wrapper.inverted,
3100 rotation = wrapper.rotation,
3101 element = wrapper.element,
3102 transform;
3103
3104 // flipping affects translate as adjustment for flipping around the group's axis
3105 if (inverted) {
3106 translateX += wrapper.attr('width');
3107 translateY += wrapper.attr('height');
3108 }
3109
3110 // Apply translate. Nearly all transformed elements have translation, so instead
3111 // of checking for translate = 0, do it always (#1767, #1846).
3112 transform = ['translate(' + translateX + ',' + translateY + ')'];
3113
3114 // apply rotation
3115 if (inverted) {
3116 transform.push('rotate(90) scale(-1,1)');
3117 } else if (rotation) { // text rotation
3118 transform.push('rotate(' + rotation + ' ' + (element.getAttribute('x') || 0) + ' ' + (element.getAttribute('y') || 0) + ')');
3119
3120 // Delete bBox memo when the rotation changes
3121 //delete wrapper.bBox;
3122 }
3123
3124 // apply scale
3125 if (defined(scaleX) || defined(scaleY)) {
3126 transform.push('scale(' + pick(scaleX, 1) + ' ' + pick(scaleY, 1) + ')');
3127 }
3128
3129 if (transform.length) {
3130 element.setAttribute('transform', transform.join(' '));
3131 }
3132 },
3133
3134 /**
3135 * Bring the element to the front.
3136 *
3137 * @returns {SVGElement} Returns the SVGElement for chaining.
3138 */
3139 toFront: function() {
3140 var element = this.element;
3141 element.parentNode.appendChild(element);
3142 return this;
3143 },
3144
3145
3146 /**
3147 * Align the element relative to the chart or another box.
3148 * ß
3149 * @param {Object} [alignOptions] The alignment options. The function can be
3150 * called without this parameter in order to re-align an element after the
3151 * box has been updated.
3152 * @param {string} [alignOptions.align=left] Horizontal alignment. Can be
3153 * one of `left`, `center` and `right`.
3154 * @param {string} [alignOptions.verticalAlign=top] Vertical alignment. Can
3155 * be one of `top`, `middle` and `bottom`.
3156 * @param {number} [alignOptions.x=0] Horizontal pixel offset from
3157 * alignment.
3158 * @param {number} [alignOptions.y=0] Vertical pixel offset from alignment.
3159 * @param {Boolean} [alignByTranslate=false] Use the `transform` attribute
3160 * with translateX and translateY custom attributes to align this elements
3161 * rather than `x` and `y` attributes.
3162 * @param {String|Object} box The box to align to, needs a width and height.
3163 * When the box is a string, it refers to an object in the Renderer. For
3164 * example, when box is `spacingBox`, it refers to `Renderer.spacingBox`
3165 * which holds `width`, `height`, `x` and `y` properties.
3166 * @returns {SVGElement} Returns the SVGElement for chaining.
3167 */
3168 align: function(alignOptions, alignByTranslate, box) {
3169 var align,
3170 vAlign,
3171 x,
3172 y,
3173 attribs = {},
3174 alignTo,
3175 renderer = this.renderer,
3176 alignedObjects = renderer.alignedObjects,
3177 alignFactor,
3178 vAlignFactor;
3179
3180 // First call on instanciate
3181 if (alignOptions) {
3182 this.alignOptions = alignOptions;
3183 this.alignByTranslate = alignByTranslate;
3184 if (!box || isString(box)) { // boxes other than renderer handle this internally
3185 this.alignTo = alignTo = box || 'renderer';
3186 erase(alignedObjects, this); // prevent duplicates, like legendGroup after resize
3187 alignedObjects.push(this);
3188 box = null; // reassign it below
3189 }
3190
3191 // When called on resize, no arguments are supplied
3192 } else {
3193 alignOptions = this.alignOptions;
3194 alignByTranslate = this.alignByTranslate;
3195 alignTo = this.alignTo;
3196 }
3197
3198 box = pick(box, renderer[alignTo], renderer);
3199
3200 // Assign variables
3201 align = alignOptions.align;
3202 vAlign = alignOptions.verticalAlign;
3203 x = (box.x || 0) + (alignOptions.x || 0); // default: left align
3204 y = (box.y || 0) + (alignOptions.y || 0); // default: top align
3205
3206 // Align
3207 if (align === 'right') {
3208 alignFactor = 1;
3209 } else if (align === 'center') {
3210 alignFactor = 2;
3211 }
3212 if (alignFactor) {
3213 x += (box.width - (alignOptions.width || 0)) / alignFactor;
3214 }
3215 attribs[alignByTranslate ? 'translateX' : 'x'] = Math.round(x);
3216
3217
3218 // Vertical align
3219 if (vAlign === 'bottom') {
3220 vAlignFactor = 1;
3221 } else if (vAlign === 'middle') {
3222 vAlignFactor = 2;
3223 }
3224 if (vAlignFactor) {
3225 y += (box.height - (alignOptions.height || 0)) / vAlignFactor;
3226 }
3227 attribs[alignByTranslate ? 'translateY' : 'y'] = Math.round(y);
3228
3229 // Animate only if already placed
3230 this[this.placed ? 'animate' : 'attr'](attribs);
3231 this.placed = true;
3232 this.alignAttr = attribs;
3233
3234 return this;
3235 },
3236
3237 /**
3238 * Get the bounding box (width, height, x and y) for the element. Generally
3239 * used to get rendered text size. Since this is called a lot in charts,
3240 * the results are cached based on text properties, in order to save DOM
3241 * traffic. The returned bounding box includes the rotation, so for example
3242 * a single text line of rotation 90 will report a greater height, and a
3243 * width corresponding to the line-height.
3244 *
3245 * @param {boolean} [reload] Skip the cache and get the updated DOM bouding
3246 * box.
3247 * @param {number} [rot] Override the element's rotation. This is internally
3248 * used on axis labels with a value of 0 to find out what the bounding box
3249 * would be have been if it were not rotated.
3250 * @returns {Object} The bounding box with `x`, `y`, `width` and `height`
3251 * properties.
3252 */
3253 getBBox: function(reload, rot) {
3254 var wrapper = this,
3255 bBox, // = wrapper.bBox,
3256 renderer = wrapper.renderer,
3257 width,
3258 height,
3259 rotation,
3260 rad,
3261 element = wrapper.element,
3262 styles = wrapper.styles,
3263 fontSize,
3264 textStr = wrapper.textStr,
3265 toggleTextShadowShim,
3266 cache = renderer.cache,
3267 cacheKeys = renderer.cacheKeys,
3268 cacheKey;
3269
3270 rotation = pick(rot, wrapper.rotation);
3271 rad = rotation * deg2rad;
3272
3273
3274 fontSize = styles && styles.fontSize;
3275
3276
3277 if (textStr !== undefined) {
3278
3279 cacheKey = textStr.toString();
3280
3281 // Since numbers are monospaced, and numerical labels appear a lot
3282 // in a chart, we assume that a label of n characters has the same
3283 // bounding box as others of the same length. Unless there is inner
3284 // HTML in the label. In that case, leave the numbers as is (#5899).
3285 if (cacheKey.indexOf('<') === -1) {
3286 cacheKey = cacheKey.replace(/[0-9]/g, '0');
3287 }
3288
3289 // Properties that affect bounding box
3290 cacheKey += [
3291 '',
3292 rotation || 0,
3293 fontSize,
3294 element.style.width,
3295 element.style['text-overflow'] // #5968
3296 ]
3297 .join(',');
3298
3299 }
3300
3301 if (cacheKey && !reload) {
3302 bBox = cache[cacheKey];
3303 }
3304
3305 // No cache found
3306 if (!bBox) {
3307
3308 // SVG elements
3309 if (element.namespaceURI === wrapper.SVG_NS || renderer.forExport) {
3310 try { // Fails in Firefox if the container has display: none.
3311
3312 // When the text shadow shim is used, we need to hide the fake shadows
3313 // to get the correct bounding box (#3872)
3314 toggleTextShadowShim = this.fakeTS && function(display) {
3315 each(element.querySelectorAll('.highcharts-text-outline'), function(tspan) {
3316 tspan.style.display = display;
3317 });
3318 };
3319
3320 // Workaround for #3842, Firefox reporting wrong bounding box for shadows
3321 if (toggleTextShadowShim) {
3322 toggleTextShadowShim('none');
3323 }
3324
3325 bBox = element.getBBox ?
3326 // SVG: use extend because IE9 is not allowed to change width and height in case
3327 // of rotation (below)
3328 extend({}, element.getBBox()) :
3329 // Legacy IE in export mode
3330 {
3331 width: element.offsetWidth,
3332 height: element.offsetHeight
3333 };
3334
3335 // #3842
3336 if (toggleTextShadowShim) {
3337 toggleTextShadowShim('');
3338 }
3339 } catch (e) {}
3340
3341 // If the bBox is not set, the try-catch block above failed. The other condition
3342 // is for Opera that returns a width of -Infinity on hidden elements.
3343 if (!bBox || bBox.width < 0) {
3344 bBox = {
3345 width: 0,
3346 height: 0
3347 };
3348 }
3349
3350
3351 // VML Renderer or useHTML within SVG
3352 } else {
3353
3354 bBox = wrapper.htmlGetBBox();
3355
3356 }
3357
3358 // True SVG elements as well as HTML elements in modern browsers using the .useHTML option
3359 // need to compensated for rotation
3360 if (renderer.isSVG) {
3361 width = bBox.width;
3362 height = bBox.height;
3363
3364 // Workaround for wrong bounding box in IE9 and IE10 (#1101, #1505, #1669, #2568)
3365 if (isMS && styles && styles.fontSize === '11px' && height.toPrecision(3) === '16.9') {
3366 bBox.height = height = 14;
3367 }
3368
3369 // Adjust for rotated text
3370 if (rotation) {
3371 bBox.width = Math.abs(height * Math.sin(rad)) + Math.abs(width * Math.cos(rad));
3372 bBox.height = Math.abs(height * Math.cos(rad)) + Math.abs(width * Math.sin(rad));
3373 }
3374 }
3375
3376 // Cache it. When loading a chart in a hidden iframe in Firefox and IE/Edge, the
3377 // bounding box height is 0, so don't cache it (#5620).
3378 if (cacheKey && bBox.height > 0) {
3379
3380 // Rotate (#4681)
3381 while (cacheKeys.length > 250) {
3382 delete cache[cacheKeys.shift()];
3383 }
3384
3385 if (!cache[cacheKey]) {
3386 cacheKeys.push(cacheKey);
3387 }
3388 cache[cacheKey] = bBox;
3389 }
3390 }
3391 return bBox;
3392 },
3393
3394 /**
3395 * Show the element after it has been hidden.
3396 *
3397 * @param {boolean} [inherit=false] Set the visibility attribute to
3398 * `inherit` rather than `visible`. The difference is that an element with
3399 * `visibility="visible"` will be visible even if the parent is hidden.
3400 *
3401 * @returns {SVGElement} Returns the SVGElement for chaining.
3402 */
3403 show: function(inherit) {
3404 return this.attr({
3405 visibility: inherit ? 'inherit' : 'visible'
3406 });
3407 },
3408
3409 /**
3410 * Hide the element, equivalent to setting the `visibility` attribute to
3411 * `hidden`.
3412 *
3413 * @returns {SVGElement} Returns the SVGElement for chaining.
3414 */
3415 hide: function() {
3416 return this.attr({
3417 visibility: 'hidden'
3418 });
3419 },
3420
3421 /**
3422 * Fade out an element by animating its opacity down to 0, and hide it on
3423 * complete. Used internally for the tooltip.
3424 *
3425 * @param {number} [duration=150] The fade duration in milliseconds.
3426 */
3427 fadeOut: function(duration) {
3428 var elemWrapper = this;
3429 elemWrapper.animate({
3430 opacity: 0
3431 }, {
3432 duration: duration || 150,
3433 complete: function() {
3434 elemWrapper.attr({
3435 y: -9999
3436 }); // #3088, assuming we're only using this for tooltips
3437 }
3438 });
3439 },
3440
3441 /**
3442 * Add the element to the DOM. All elements must be added this way.
3443 *
3444 * @param {SVGElement|SVGDOMElement} [parent] The parent item to add it to.
3445 * If undefined, the element is added to the {@link SVGRenderer.box}.
3446 *
3447 * @returns {SVGElement} Returns the SVGElement for chaining.
3448 *
3449 * @sample highcharts/members/renderer-g - Elements added to a group
3450 */
3451 add: function(parent) {
3452
3453 var renderer = this.renderer,
3454 element = this.element,
3455 inserted;
3456
3457 if (parent) {
3458 this.parentGroup = parent;
3459 }
3460
3461 // mark as inverted
3462 this.parentInverted = parent && parent.inverted;
3463
3464 // build formatted text
3465 if (this.textStr !== undefined) {
3466 renderer.buildText(this);
3467 }
3468
3469 // Mark as added
3470 this.added = true;
3471
3472 // If we're adding to renderer root, or other elements in the group
3473 // have a z index, we need to handle it
3474 if (!parent || parent.handleZ || this.zIndex) {
3475 inserted = this.zIndexSetter();
3476 }
3477
3478 // If zIndex is not handled, append at the end
3479 if (!inserted) {
3480 (parent ? parent.element : renderer.box).appendChild(element);
3481 }
3482
3483 // fire an event for internal hooks
3484 if (this.onAdd) {
3485 this.onAdd();
3486 }
3487
3488 return this;
3489 },
3490
3491 /**
3492 * Removes an element from the DOM.
3493 *
3494 * @private
3495 * @param {SVGDOMElement|HTMLDOMElement} element The DOM node to remove.
3496 */
3497 safeRemoveChild: function(element) {
3498 var parentNode = element.parentNode;
3499 if (parentNode) {
3500 parentNode.removeChild(element);
3501 }
3502 },
3503
3504 /**
3505 * Destroy the element and element wrapper and clear up the DOM and event
3506 * hooks.
3507 *
3508 * @returns {void}
3509 */
3510 destroy: function() {
3511 var wrapper = this,
3512 element = wrapper.element || {},
3513 parentToClean = wrapper.renderer.isSVG && element.nodeName === 'SPAN' && wrapper.parentGroup,
3514 grandParent,
3515 key,
3516 i;
3517
3518 // remove events
3519 element.onclick = element.onmouseout = element.onmouseover = element.onmousemove = element.point = null;
3520 stop(wrapper); // stop running animations
3521
3522 if (wrapper.clipPath) {
3523 wrapper.clipPath = wrapper.clipPath.destroy();
3524 }
3525
3526 // Destroy stops in case this is a gradient object
3527 if (wrapper.stops) {
3528 for (i = 0; i < wrapper.stops.length; i++) {
3529 wrapper.stops[i] = wrapper.stops[i].destroy();
3530 }
3531 wrapper.stops = null;
3532 }
3533
3534 // remove element
3535 wrapper.safeRemoveChild(element);
3536
3537
3538 wrapper.destroyShadows();
3539
3540
3541 // In case of useHTML, clean up empty containers emulating SVG groups (#1960, #2393, #2697).
3542 while (parentToClean && parentToClean.div && parentToClean.div.childNodes.length === 0) {
3543 grandParent = parentToClean.parentGroup;
3544 wrapper.safeRemoveChild(parentToClean.div);
3545 delete parentToClean.div;
3546 parentToClean = grandParent;
3547 }
3548
3549 // remove from alignObjects
3550 if (wrapper.alignTo) {
3551 erase(wrapper.renderer.alignedObjects, wrapper);
3552 }
3553
3554 for (key in wrapper) {
3555 delete wrapper[key];
3556 }
3557
3558 return null;
3559 },
3560
3561
3562 /**
3563 * @typedef {Object} ShadowOptions
3564 * @property {string} [color=#000000] The shadow color.
3565 * @property {number} [offsetX=1] The horizontal offset from the element.
3566 * @property {number} [offsetY=1] The vertical offset from the element.
3567 * @property {number} [opacity=0.15] The shadow opacity.
3568 * @property {number} [width=3] The shadow width or distance from the
3569 * element.
3570 */
3571 /**
3572 * Add a shadow to the element. Must be called after the element is added to
3573 * the DOM. In styled mode, this method is not used, instead use `defs` and
3574 * filters.
3575 *
3576 * @param {boolean|ShadowOptions} shadowOptions The shadow options. If
3577 * `true`, the default options are applied. If `false`, the current
3578 * shadow will be removed.
3579 * @param {SVGElement} [group] The SVG group element where the shadows will
3580 * be applied. The default is to add it to the same parent as the current
3581 * element. Internally, this is ised for pie slices, where all the
3582 * shadows are added to an element behind all the slices.
3583 * @param {boolean} [cutOff] Used internally for column shadows.
3584 *
3585 * @returns {SVGElement} Returns the SVGElement for chaining.
3586 *
3587 * @example
3588 * renderer.rect(10, 100, 100, 100)
3589 * .attr({ fill: 'red' })
3590 * .shadow(true);
3591 */
3592 shadow: function(shadowOptions, group, cutOff) {
3593 var shadows = [],
3594 i,
3595 shadow,
3596 element = this.element,
3597 strokeWidth,
3598 shadowWidth,
3599 shadowElementOpacity,
3600
3601 // compensate for inverted plot area
3602 transform;
3603
3604 if (!shadowOptions) {
3605 this.destroyShadows();
3606
3607 } else if (!this.shadows) {
3608 shadowWidth = pick(shadowOptions.width, 3);
3609 shadowElementOpacity = (shadowOptions.opacity || 0.15) / shadowWidth;
3610 transform = this.parentInverted ?
3611 '(-1,-1)' :
3612 '(' + pick(shadowOptions.offsetX, 1) + ', ' + pick(shadowOptions.offsetY, 1) + ')';
3613 for (i = 1; i <= shadowWidth; i++) {
3614 shadow = element.cloneNode(0);
3615 strokeWidth = (shadowWidth * 2) + 1 - (2 * i);
3616 attr(shadow, {
3617 'isShadow': 'true',
3618 'stroke': shadowOptions.color || '#000000',
3619 'stroke-opacity': shadowElementOpacity * i,
3620 'stroke-width': strokeWidth,
3621 'transform': 'translate' + transform,
3622 'fill': 'none'
3623 });
3624 if (cutOff) {
3625 attr(shadow, 'height', Math.max(attr(shadow, 'height') - strokeWidth, 0));
3626 shadow.cutHeight = strokeWidth;
3627 }
3628
3629 if (group) {
3630 group.element.appendChild(shadow);
3631 } else {
3632 element.parentNode.insertBefore(shadow, element);
3633 }
3634
3635 shadows.push(shadow);
3636 }
3637
3638 this.shadows = shadows;
3639 }
3640 return this;
3641
3642 },
3643
3644 /**
3645 * Destroy shadows on the element.
3646 * @private
3647 */
3648 destroyShadows: function() {
3649 each(this.shadows || [], function(shadow) {
3650 this.safeRemoveChild(shadow);
3651 }, this);
3652 this.shadows = undefined;
3653 },
3654
3655
3656
3657 xGetter: function(key) {
3658 if (this.element.nodeName === 'circle') {
3659 if (key === 'x') {
3660 key = 'cx';
3661 } else if (key === 'y') {
3662 key = 'cy';
3663 }
3664 }
3665 return this._defaultGetter(key);
3666 },
3667
3668 /**
3669 * Get the current value of an attribute or pseudo attribute, used mainly
3670 * for animation. Called internally from the {@link SVGRenderer#attr}
3671 * function.
3672 *
3673 * @private
3674 */
3675 _defaultGetter: function(key) {
3676 var ret = pick(this[key], this.element ? this.element.getAttribute(key) : null, 0);
3677
3678 if (/^[\-0-9\.]+$/.test(ret)) { // is numerical
3679 ret = parseFloat(ret);
3680 }
3681 return ret;
3682 },
3683
3684
3685 dSetter: function(value, key, element) {
3686 if (value && value.join) { // join path
3687 value = value.join(' ');
3688 }
3689 if (/(NaN| {2}|^$)/.test(value)) {
3690 value = 'M 0 0';
3691 }
3692 element.setAttribute(key, value);
3693
3694 this[key] = value;
3695 },
3696
3697 dashstyleSetter: function(value) {
3698 var i,
3699 strokeWidth = this['stroke-width'];
3700
3701 // If "inherit", like maps in IE, assume 1 (#4981). With HC5 and the new strokeWidth
3702 // function, we should be able to use that instead.
3703 if (strokeWidth === 'inherit') {
3704 strokeWidth = 1;
3705 }
3706 value = value && value.toLowerCase();
3707 if (value) {
3708 value = value
3709 .replace('shortdashdotdot', '3,1,1,1,1,1,')
3710 .replace('shortdashdot', '3,1,1,1')
3711 .replace('shortdot', '1,1,')
3712 .replace('shortdash', '3,1,')
3713 .replace('longdash', '8,3,')
3714 .replace(/dot/g, '1,3,')
3715 .replace('dash', '4,3,')
3716 .replace(/,$/, '')
3717 .split(','); // ending comma
3718
3719 i = value.length;
3720 while (i--) {
3721 value[i] = pInt(value[i]) * strokeWidth;
3722 }
3723 value = value.join(',')
3724 .replace(/NaN/g, 'none'); // #3226
3725 this.element.setAttribute('stroke-dasharray', value);
3726 }
3727 },
3728
3729 alignSetter: function(value) {
3730 var convert = {
3731 left: 'start',
3732 center: 'middle',
3733 right: 'end'
3734 };
3735 this.element.setAttribute('text-anchor', convert[value]);
3736 },
3737 opacitySetter: function(value, key, element) {
3738 this[key] = value;
3739 element.setAttribute(key, value);
3740 },
3741 titleSetter: function(value) {
3742 var titleNode = this.element.getElementsByTagName('title')[0];
3743 if (!titleNode) {
3744 titleNode = doc.createElementNS(this.SVG_NS, 'title');
3745 this.element.appendChild(titleNode);
3746 }
3747
3748 // Remove text content if it exists
3749 if (titleNode.firstChild) {
3750 titleNode.removeChild(titleNode.firstChild);
3751 }
3752
3753 titleNode.appendChild(
3754 doc.createTextNode(
3755 (String(pick(value), '')).replace(/<[^>]*>/g, '') // #3276, #3895
3756 )
3757 );
3758 },
3759 textSetter: function(value) {
3760 if (value !== this.textStr) {
3761 // Delete bBox memo when the text changes
3762 delete this.bBox;
3763
3764 this.textStr = value;
3765 if (this.added) {
3766 this.renderer.buildText(this);
3767 }
3768 }
3769 },
3770 fillSetter: function(value, key, element) {
3771 if (typeof value === 'string') {
3772 element.setAttribute(key, value);
3773 } else if (value) {
3774 this.colorGradient(value, key, element);
3775 }
3776 },
3777 visibilitySetter: function(value, key, element) {
3778 // IE9-11 doesn't handle visibilty:inherit well, so we remove the attribute instead (#2881, #3909)
3779 if (value === 'inherit') {
3780 element.removeAttribute(key);
3781 } else {
3782 element.setAttribute(key, value);
3783 }
3784 },
3785 zIndexSetter: function(value, key) {
3786 var renderer = this.renderer,
3787 parentGroup = this.parentGroup,
3788 parentWrapper = parentGroup || renderer,
3789 parentNode = parentWrapper.element || renderer.box,
3790 childNodes,
3791 otherElement,
3792 otherZIndex,
3793 element = this.element,
3794 inserted,
3795 run = this.added,
3796 i;
3797
3798 if (defined(value)) {
3799 element.zIndex = value; // So we can read it for other elements in the group
3800 value = +value;
3801 if (this[key] === value) { // Only update when needed (#3865)
3802 run = false;
3803 }
3804 this[key] = value;
3805 }
3806
3807 // Insert according to this and other elements' zIndex. Before .add() is called,
3808 // nothing is done. Then on add, or by later calls to zIndexSetter, the node
3809 // is placed on the right place in the DOM.
3810 if (run) {
3811 value = this.zIndex;
3812
3813 if (value && parentGroup) {
3814 parentGroup.handleZ = true;
3815 }
3816
3817 childNodes = parentNode.childNodes;
3818 for (i = 0; i < childNodes.length && !inserted; i++) {
3819 otherElement = childNodes[i];
3820 otherZIndex = otherElement.zIndex;
3821 if (otherElement !== element && (
3822 // Insert before the first element with a higher zIndex
3823 pInt(otherZIndex) > value ||
3824 // If no zIndex given, insert before the first element with a zIndex
3825 (!defined(value) && defined(otherZIndex)) ||
3826 // Negative zIndex versus no zIndex:
3827 // On all levels except the highest. If the parent is <svg>,
3828 // then we don't want to put items before <desc> or <defs>
3829 (value < 0 && !defined(otherZIndex) && parentNode !== renderer.box)
3830
3831 )) {
3832 parentNode.insertBefore(element, otherElement);
3833 inserted = true;
3834 }
3835 }
3836 if (!inserted) {
3837 parentNode.appendChild(element);
3838 }
3839 }
3840 return inserted;
3841 },
3842 _defaultSetter: function(value, key, element) {
3843 element.setAttribute(key, value);
3844 }
3845 };
3846
3847 // Some shared setters and getters
3848 SVGElement.prototype.yGetter = SVGElement.prototype.xGetter;
3849 SVGElement.prototype.translateXSetter = SVGElement.prototype.translateYSetter =
3850 SVGElement.prototype.rotationSetter = SVGElement.prototype.verticalAlignSetter =
3851 SVGElement.prototype.scaleXSetter = SVGElement.prototype.scaleYSetter = function(value, key) {
3852 this[key] = value;
3853 this.doTransform = true;
3854 };
3855
3856
3857 // WebKit and Batik have problems with a stroke-width of zero, so in this case we remove the
3858 // stroke attribute altogether. #1270, #1369, #3065, #3072.
3859 SVGElement.prototype['stroke-widthSetter'] = SVGElement.prototype.strokeSetter = function(value, key, element) {
3860 this[key] = value;
3861 // Only apply the stroke attribute if the stroke width is defined and larger than 0
3862 if (this.stroke && this['stroke-width']) {
3863 SVGElement.prototype.fillSetter.call(this, this.stroke, 'stroke', element); // use prototype as instance may be overridden
3864 element.setAttribute('stroke-width', this['stroke-width']);
3865 this.hasStroke = true;
3866 } else if (key === 'stroke-width' && value === 0 && this.hasStroke) {
3867 element.removeAttribute('stroke');
3868 this.hasStroke = false;
3869 }
3870 };
3871
3872
3873 /**
3874 * Allows direct access to the Highcharts rendering layer in order to draw
3875 * primitive shapes like circles, rectangles, paths or text directly on a chart,
3876 * or independent from any chart. The SVGRenderer represents a wrapper object
3877 * for SVGin modern browsers and through the VMLRenderer, for VML in IE < 8.
3878 *
3879 * An existing chart's renderer can be accessed through {@link Chart#renderer}.
3880 * The renderer can also be used completely decoupled from a chart.
3881 *
3882 * @param {HTMLDOMElement} container - Where to put the SVG in the web page.
3883 * @param {number} width - The width of the SVG.
3884 * @param {number} height - The height of the SVG.
3885 * @param {boolean} [forExport=false] - Whether the rendered content is intended
3886 * for export.
3887 * @param {boolean} [allowHTML=true] - Whether the renderer is allowed to
3888 * include HTML text, which will be projected on top of the SVG.
3889 *
3890 * @example
3891 * // Use directly without a chart object.
3892 * var renderer = new Highcharts.Renderer(parentNode, 600, 400);
3893 *
3894 * @sample highcharts/members/renderer-on-chart - Annotating a chart programmatically.
3895 * @sample highcharts/members/renderer-basic - Independedt SVG drawing.
3896 *
3897 * @class
3898 */
3899 SVGRenderer = H.SVGRenderer = function() {
3900 this.init.apply(this, arguments);
3901 };
3902 SVGRenderer.prototype = {
3903 /**
3904 * A pointer to the renderer's associated Element class. The VMLRenderer
3905 * will have a pointer to VMLElement here.
3906 * @type {SVGElement}
3907 */
3908 Element: SVGElement,
3909 SVG_NS: SVG_NS,
3910 /**
3911 * Initialize the SVGRenderer. Overridable initiator function that takes
3912 * the same parameters as the constructor.
3913 */
3914 init: function(container, width, height, style, forExport, allowHTML) {
3915 var renderer = this,
3916 boxWrapper,
3917 element,
3918 desc;
3919
3920 boxWrapper = renderer.createElement('svg')
3921 .attr({
3922 'version': '1.1',
3923 'class': 'highcharts-root'
3924 })
3925
3926 .css(this.getStyle(style));
3927 element = boxWrapper.element;
3928 container.appendChild(element);
3929
3930 // For browsers other than IE, add the namespace attribute (#1978)
3931 if (container.innerHTML.indexOf('xmlns') === -1) {
3932 attr(element, 'xmlns', this.SVG_NS);
3933 }
3934
3935 // object properties
3936 renderer.isSVG = true;
3937
3938 /**
3939 * The root `svg` node of the renderer.
3940 * @type {SVGDOMElement}
3941 */
3942 this.box = element;
3943 /**
3944 * The wrapper for the root `svg` node of the renderer.
3945 * @type {SVGElement}
3946 */
3947 this.boxWrapper = boxWrapper;
3948 renderer.alignedObjects = [];
3949
3950 /**
3951 * Page url used for internal references.
3952 * @type {string}
3953 */
3954 // #24, #672, #1070
3955 this.url = (isFirefox || isWebKit) && doc.getElementsByTagName('base').length ?
3956 win.location.href
3957 .replace(/#.*?$/, '') // remove the hash
3958 .replace(/([\('\)])/g, '\\$1') // escape parantheses and quotes
3959 .replace(/ /g, '%20') : // replace spaces (needed for Safari only)
3960 '';
3961
3962 // Add description
3963 desc = this.createElement('desc').add();
3964 desc.element.appendChild(doc.createTextNode('Created with Highcharts 5.0.5'));
3965
3966
3967 renderer.defs = this.createElement('defs').add();
3968 renderer.allowHTML = allowHTML;
3969 renderer.forExport = forExport;
3970 renderer.gradients = {}; // Object where gradient SvgElements are stored
3971 renderer.cache = {}; // Cache for numerical bounding boxes
3972 renderer.cacheKeys = [];
3973 renderer.imgCount = 0;
3974
3975 renderer.setSize(width, height, false);
3976
3977
3978
3979 // Issue 110 workaround:
3980 // In Firefox, if a div is positioned by percentage, its pixel position may land
3981 // between pixels. The container itself doesn't display this, but an SVG element
3982 // inside this container will be drawn at subpixel precision. In order to draw
3983 // sharp lines, this must be compensated for. This doesn't seem to work inside
3984 // iframes though (like in jsFiddle).
3985 var subPixelFix, rect;
3986 if (isFirefox && container.getBoundingClientRect) {
3987 subPixelFix = function() {
3988 css(container, {
3989 left: 0,
3990 top: 0
3991 });
3992 rect = container.getBoundingClientRect();
3993 css(container, {
3994 left: (Math.ceil(rect.left) - rect.left) + 'px',
3995 top: (Math.ceil(rect.top) - rect.top) + 'px'
3996 });
3997 };
3998
3999 // run the fix now
4000 subPixelFix();
4001
4002 // run it on resize
4003 renderer.unSubPixelFix = addEvent(win, 'resize', subPixelFix);
4004 }
4005 },
4006
4007
4008
4009 /**
4010 * Get the global style setting for the renderer.
4011 * @private
4012 * @param {CSSObject} style - Style settings.
4013 * @return {CSSObject} The style settings mixed with defaults.
4014 */
4015 getStyle: function(style) {
4016 this.style = extend({
4017
4018 fontFamily: '"Lucida Grande", "Lucida Sans Unicode", Arial, Helvetica, sans-serif', // default font
4019 fontSize: '12px'
4020
4021 }, style);
4022 return this.style;
4023 },
4024 /**
4025 * Apply the global style on the renderer, mixed with the default styles.
4026 * @param {CSSObject} style - CSS to apply.
4027 */
4028 setStyle: function(style) {
4029 this.boxWrapper.css(this.getStyle(style));
4030 },
4031
4032
4033 /**
4034 * Detect whether the renderer is hidden. This happens when one of the
4035 * parent elements has display: none. Used internally to detect when we need
4036 * to render preliminarily in another div to get the text bounding boxes
4037 * right.
4038 *
4039 * @returns {boolean} True if it is hidden.
4040 */
4041 isHidden: function() { // #608
4042 return !this.boxWrapper.getBBox().width;
4043 },
4044
4045 /**
4046 * Destroys the renderer and its allocated members.
4047 */
4048 destroy: function() {
4049 var renderer = this,
4050 rendererDefs = renderer.defs;
4051 renderer.box = null;
4052 renderer.boxWrapper = renderer.boxWrapper.destroy();
4053
4054 // Call destroy on all gradient elements
4055 destroyObjectProperties(renderer.gradients || {});
4056 renderer.gradients = null;
4057
4058 // Defs are null in VMLRenderer
4059 // Otherwise, destroy them here.
4060 if (rendererDefs) {
4061 renderer.defs = rendererDefs.destroy();
4062 }
4063
4064 // Remove sub pixel fix handler (#982)
4065 if (renderer.unSubPixelFix) {
4066 renderer.unSubPixelFix();
4067 }
4068
4069 renderer.alignedObjects = null;
4070
4071 return null;
4072 },
4073
4074 /**
4075 * Create a wrapper for an SVG element. Serves as a factory for
4076 * {@link SVGElement}, but this function is itself mostly called from
4077 * primitive factories like {@link SVGRenderer#path}, {@link
4078 * SVGRenderer#rect} or {@link SVGRenderer#text}.
4079 *
4080 * @param {string} nodeName - The node name, for example `rect`, `g` etc.
4081 * @returns {SVGElement} The generated SVGElement.
4082 */
4083 createElement: function(nodeName) {
4084 var wrapper = new this.Element();
4085 wrapper.init(this, nodeName);
4086 return wrapper;
4087 },
4088
4089 /**
4090 * Dummy function for plugins, called every time the renderer is updated.
4091 * Prior to Highcharts 5, this was used for the canvg renderer.
4092 * @function
4093 */
4094 draw: noop,
4095
4096 /**
4097 * Get converted radial gradient attributes according to the radial
4098 * reference. Used internally from the {@link SVGElement#colorGradient}
4099 * function.
4100 *
4101 * @private
4102 */
4103 getRadialAttr: function(radialReference, gradAttr) {
4104 return {
4105 cx: (radialReference[0] - radialReference[2] / 2) + gradAttr.cx * radialReference[2],
4106 cy: (radialReference[1] - radialReference[2] / 2) + gradAttr.cy * radialReference[2],
4107 r: gradAttr.r * radialReference[2]
4108 };
4109 },
4110
4111 /**
4112 * Parse a simple HTML string into SVG tspans. Called internally when text
4113 * is set on an SVGElement. The function supports a subset of HTML tags,
4114 * CSS text features like `width`, `text-overflow`, `white-space`, and
4115 * also attributes like `href` and `style`.
4116 * @private
4117 * @param {SVGElement} wrapper The parent SVGElement.
4118 */
4119 buildText: function(wrapper) {
4120 var textNode = wrapper.element,
4121 renderer = this,
4122 forExport = renderer.forExport,
4123 textStr = pick(wrapper.textStr, '').toString(),
4124 hasMarkup = textStr.indexOf('<') !== -1,
4125 lines,
4126 childNodes = textNode.childNodes,
4127 clsRegex,
4128 styleRegex,
4129 hrefRegex,
4130 wasTooLong,
4131 parentX = attr(textNode, 'x'),
4132 textStyles = wrapper.styles,
4133 width = wrapper.textWidth,
4134 textLineHeight = textStyles && textStyles.lineHeight,
4135 textOutline = textStyles && textStyles.textOutline,
4136 ellipsis = textStyles && textStyles.textOverflow === 'ellipsis',
4137 i = childNodes.length,
4138 tempParent = width && !wrapper.added && this.box,
4139 getLineHeight = function(tspan) {
4140 var fontSizeStyle;
4141
4142 fontSizeStyle = /(px|em)$/.test(tspan && tspan.style.fontSize) ?
4143 tspan.style.fontSize :
4144 ((textStyles && textStyles.fontSize) || renderer.style.fontSize || 12);
4145
4146
4147 return textLineHeight ?
4148 pInt(textLineHeight) :
4149 renderer.fontMetrics(
4150 fontSizeStyle,
4151 // Get the computed size from parent if not explicit
4152 tspan.getAttribute('style') ? tspan : textNode
4153 ).h;
4154 },
4155 unescapeAngleBrackets = function(inputStr) {
4156 return inputStr.replace(/&lt;/g, '<').replace(/&gt;/g, '>');
4157 };
4158
4159 /// remove old text
4160 while (i--) {
4161 textNode.removeChild(childNodes[i]);
4162 }
4163
4164 // Skip tspans, add text directly to text node. The forceTSpan is a hook
4165 // used in text outline hack.
4166 if (!hasMarkup && !textOutline && !ellipsis && !width && textStr.indexOf(' ') === -1) {
4167 textNode.appendChild(doc.createTextNode(unescapeAngleBrackets(textStr)));
4168
4169 // Complex strings, add more logic
4170 } else {
4171
4172 clsRegex = /<.*class="([^"]+)".*>/;
4173 styleRegex = /<.*style="([^"]+)".*>/;
4174 hrefRegex = /<.*href="(http[^"]+)".*>/;
4175
4176 if (tempParent) {
4177 tempParent.appendChild(textNode); // attach it to the DOM to read offset width
4178 }
4179
4180 if (hasMarkup) {
4181 lines = textStr
4182 .replace(/<(b|strong)>/g, '<span style="font-weight:bold">')
4183 .replace(/<(i|em)>/g, '<span style="font-style:italic">')
4184 .replace(/<a/g, '<span')
4185 .replace(/<\/(b|strong|i|em|a)>/g, '</span>')
4186 .split(/<br.*?>/g);
4187
4188 } else {
4189 lines = [textStr];
4190 }
4191
4192
4193 // Trim empty lines (#5261)
4194 lines = grep(lines, function(line) {
4195 return line !== '';
4196 });
4197
4198
4199 // build the lines
4200 each(lines, function buildTextLines(line, lineNo) {
4201 var spans,
4202 spanNo = 0;
4203 line = line
4204 .replace(/^\s+|\s+$/g, '') // Trim to prevent useless/costly process on the spaces (#5258)
4205 .replace(/<span/g, '|||<span')
4206 .replace(/<\/span>/g, '</span>|||');
4207 spans = line.split('|||');
4208
4209 each(spans, function buildTextSpans(span) {
4210 if (span !== '' || spans.length === 1) {
4211 var attributes = {},
4212 tspan = doc.createElementNS(renderer.SVG_NS, 'tspan'),
4213 spanCls,
4214 spanStyle; // #390
4215 if (clsRegex.test(span)) {
4216 spanCls = span.match(clsRegex)[1];
4217 attr(tspan, 'class', spanCls);
4218 }
4219 if (styleRegex.test(span)) {
4220 spanStyle = span.match(styleRegex)[1].replace(/(;| |^)color([ :])/, '$1fill$2');
4221 attr(tspan, 'style', spanStyle);
4222 }
4223 if (hrefRegex.test(span) && !forExport) { // Not for export - #1529
4224 attr(tspan, 'onclick', 'location.href=\"' + span.match(hrefRegex)[1] + '\"');
4225 css(tspan, {
4226 cursor: 'pointer'
4227 });
4228 }
4229
4230 span = unescapeAngleBrackets(span.replace(/<(.|\n)*?>/g, '') || ' ');
4231
4232 // Nested tags aren't supported, and cause crash in Safari (#1596)
4233 if (span !== ' ') {
4234
4235 // add the text node
4236 tspan.appendChild(doc.createTextNode(span));
4237
4238 if (!spanNo) { // first span in a line, align it to the left
4239 if (lineNo && parentX !== null) {
4240 attributes.x = parentX;
4241 }
4242 } else {
4243 attributes.dx = 0; // #16
4244 }
4245
4246 // add attributes
4247 attr(tspan, attributes);
4248
4249 // Append it
4250 textNode.appendChild(tspan);
4251
4252 // first span on subsequent line, add the line height
4253 if (!spanNo && lineNo) {
4254
4255 // allow getting the right offset height in exporting in IE
4256 if (!svg && forExport) {
4257 css(tspan, {
4258 display: 'block'
4259 });
4260 }
4261
4262 // Set the line height based on the font size of either
4263 // the text element or the tspan element
4264 attr(
4265 tspan,
4266 'dy',
4267 getLineHeight(tspan)
4268 );
4269 }
4270
4271 /*if (width) {
4272 renderer.breakText(wrapper, width);
4273 }*/
4274
4275 // Check width and apply soft breaks or ellipsis
4276 if (width) {
4277 var words = span.replace(/([^\^])-/g, '$1- ').split(' '), // #1273
4278 noWrap = textStyles.whiteSpace === 'nowrap',
4279 hasWhiteSpace = spans.length > 1 || lineNo || (words.length > 1 && !noWrap),
4280 tooLong,
4281 actualWidth,
4282 rest = [],
4283 dy = getLineHeight(tspan),
4284 rotation = wrapper.rotation,
4285 wordStr = span, // for ellipsis
4286 cursor = wordStr.length, // binary search cursor
4287 bBox;
4288
4289 while ((hasWhiteSpace || ellipsis) && (words.length || rest.length)) {
4290 wrapper.rotation = 0; // discard rotation when computing box
4291 bBox = wrapper.getBBox(true);
4292 actualWidth = bBox.width;
4293
4294 // Old IE cannot measure the actualWidth for SVG elements (#2314)
4295 if (!svg && renderer.forExport) {
4296 actualWidth = renderer.measureSpanWidth(tspan.firstChild.data, wrapper.styles);
4297 }
4298
4299 tooLong = actualWidth > width;
4300
4301 // For ellipsis, do a binary search for the correct string length
4302 if (wasTooLong === undefined) {
4303 wasTooLong = tooLong; // First time
4304 }
4305 if (ellipsis && wasTooLong) {
4306 cursor /= 2;
4307
4308 if (wordStr === '' || (!tooLong && cursor < 0.5)) {
4309 words = []; // All ok, break out
4310 } else {
4311 wordStr = span.substring(0, wordStr.length + (tooLong ? -1 : 1) * Math.ceil(cursor));
4312 words = [wordStr + (width > 3 ? '\u2026' : '')];
4313 tspan.removeChild(tspan.firstChild);
4314 }
4315
4316 // Looping down, this is the first word sequence that is not too long,
4317 // so we can move on to build the next line.
4318 } else if (!tooLong || words.length === 1) {
4319 words = rest;
4320 rest = [];
4321
4322 if (words.length && !noWrap) {
4323 tspan = doc.createElementNS(SVG_NS, 'tspan');
4324 attr(tspan, {
4325 dy: dy,
4326 x: parentX
4327 });
4328 if (spanStyle) { // #390
4329 attr(tspan, 'style', spanStyle);
4330 }
4331 textNode.appendChild(tspan);
4332 }
4333 if (actualWidth > width) { // a single word is pressing it out
4334 width = actualWidth;
4335 }
4336 } else { // append to existing line tspan
4337 tspan.removeChild(tspan.firstChild);
4338 rest.unshift(words.pop());
4339 }
4340 if (words.length) {
4341 tspan.appendChild(doc.createTextNode(words.join(' ').replace(/- /g, '-')));
4342 }
4343 }
4344 wrapper.rotation = rotation;
4345 }
4346
4347 spanNo++;
4348 }
4349 }
4350 });
4351 });
4352
4353 if (wasTooLong) {
4354 wrapper.attr('title', wrapper.textStr);
4355 }
4356 if (tempParent) {
4357 tempParent.removeChild(textNode); // attach it to the DOM to read offset width
4358 }
4359
4360 // Apply the text outline
4361 if (textOutline && wrapper.applyTextOutline) {
4362 wrapper.applyTextOutline(textOutline);
4363 }
4364 }
4365 },
4366
4367
4368
4369 /*
4370 breakText: function (wrapper, width) {
4371 var bBox = wrapper.getBBox(),
4372 node = wrapper.element,
4373 textLength = node.textContent.length,
4374 pos = Math.round(width * textLength / bBox.width), // try this position first, based on average character width
4375 increment = 0,
4376 finalPos;
4377
4378 if (bBox.width > width) {
4379 while (finalPos === undefined) {
4380 textLength = node.getSubStringLength(0, pos);
4381
4382 if (textLength <= width) {
4383 if (increment === -1) {
4384 finalPos = pos;
4385 } else {
4386 increment = 1;
4387 }
4388 } else {
4389 if (increment === 1) {
4390 finalPos = pos - 1;
4391 } else {
4392 increment = -1;
4393 }
4394 }
4395 pos += increment;
4396 }
4397 }
4398 console.log('width', width, 'stringWidth', node.getSubStringLength(0, finalPos))
4399 },
4400 */
4401
4402 /**
4403 * Returns white for dark colors and black for bright colors.
4404 *
4405 * @param {ColorString} rgba - The color to get the contrast for.
4406 * @returns {string} The contrast color, either `#000000` or `#FFFFFF`.
4407 */
4408 getContrast: function(rgba) {
4409 rgba = color(rgba).rgba;
4410 return rgba[0] + rgba[1] + rgba[2] > 2 * 255 ? '#000000' : '#FFFFFF';
4411 },
4412
4413 /**
4414 * Create a button with preset states.
4415 * @param {string} text - The text or HTML to draw.
4416 * @param {number} x - The x position of the button's left side.
4417 * @param {number} y - The y position of the button's top side.
4418 * @param {Function} callback - The function to execute on button click or
4419 * touch.
4420 * @param {SVGAttributes} [normalState] - SVG attributes for the normal
4421 * state.
4422 * @param {SVGAttributes} [hoverState] - SVG attributes for the hover state.
4423 * @param {SVGAttributes} [pressedState] - SVG attributes for the pressed
4424 * state.
4425 * @param {SVGAttributes} [disabledState] - SVG attributes for the disabled
4426 * state.
4427 * @param {Symbol} [shape=rect] - The shape type.
4428 * @returns {SVGRenderer} The button element.
4429 */
4430 button: function(text, x, y, callback, normalState, hoverState, pressedState, disabledState, shape) {
4431 var label = this.label(text, x, y, shape, null, null, null, null, 'button'),
4432 curState = 0;
4433
4434 // Default, non-stylable attributes
4435 label.attr(merge({
4436 'padding': 8,
4437 'r': 2
4438 }, normalState));
4439
4440
4441 // Presentational
4442 var normalStyle,
4443 hoverStyle,
4444 pressedStyle,
4445 disabledStyle;
4446
4447 // Normal state - prepare the attributes
4448 normalState = merge({
4449 fill: '#f7f7f7',
4450 stroke: '#cccccc',
4451 'stroke-width': 1,
4452 style: {
4453 color: '#333333',
4454 cursor: 'pointer',
4455 fontWeight: 'normal'
4456 }
4457 }, normalState);
4458 normalStyle = normalState.style;
4459 delete normalState.style;
4460
4461 // Hover state
4462 hoverState = merge(normalState, {
4463 fill: '#e6e6e6'
4464 }, hoverState);
4465 hoverStyle = hoverState.style;
4466 delete hoverState.style;
4467
4468 // Pressed state
4469 pressedState = merge(normalState, {
4470 fill: '#e6ebf5',
4471 style: {
4472 color: '#000000',
4473 fontWeight: 'bold'
4474 }
4475 }, pressedState);
4476 pressedStyle = pressedState.style;
4477 delete pressedState.style;
4478
4479 // Disabled state
4480 disabledState = merge(normalState, {
4481 style: {
4482 color: '#cccccc'
4483 }
4484 }, disabledState);
4485 disabledStyle = disabledState.style;
4486 delete disabledState.style;
4487
4488
4489 // Add the events. IE9 and IE10 need mouseover and mouseout to funciton (#667).
4490 addEvent(label.element, isMS ? 'mouseover' : 'mouseenter', function() {
4491 if (curState !== 3) {
4492 label.setState(1);
4493 }
4494 });
4495 addEvent(label.element, isMS ? 'mouseout' : 'mouseleave', function() {
4496 if (curState !== 3) {
4497 label.setState(curState);
4498 }
4499 });
4500
4501 label.setState = function(state) {
4502 // Hover state is temporary, don't record it
4503 if (state !== 1) {
4504 label.state = curState = state;
4505 }
4506 // Update visuals
4507 label.removeClass(/highcharts-button-(normal|hover|pressed|disabled)/)
4508 .addClass('highcharts-button-' + ['normal', 'hover', 'pressed', 'disabled'][state || 0]);
4509
4510
4511 label.attr([normalState, hoverState, pressedState, disabledState][state || 0])
4512 .css([normalStyle, hoverStyle, pressedStyle, disabledStyle][state || 0]);
4513
4514 };
4515
4516
4517
4518 // Presentational attributes
4519 label
4520 .attr(normalState)
4521 .css(extend({
4522 cursor: 'default'
4523 }, normalStyle));
4524
4525
4526 return label
4527 .on('click', function(e) {
4528 if (curState !== 3) {
4529 callback.call(label, e);
4530 }
4531 });
4532 },
4533
4534 /**
4535 * Make a straight line crisper by not spilling out to neighbour pixels.
4536 *
4537 * @param {Array} points - The original points on the format `['M', 0, 0,
4538 * 'L', 100, 0]`.
4539 * @param {number} width - The width of the line.
4540 * @returns {Array} The original points array, but modified to render
4541 * crisply.
4542 */
4543 crispLine: function(points, width) {
4544 // normalize to a crisp line
4545 if (points[1] === points[4]) {
4546 // Substract due to #1129. Now bottom and left axis gridlines behave the same.
4547 points[1] = points[4] = Math.round(points[1]) - (width % 2 / 2);
4548 }
4549 if (points[2] === points[5]) {
4550 points[2] = points[5] = Math.round(points[2]) + (width % 2 / 2);
4551 }
4552 return points;
4553 },
4554
4555
4556 /**
4557 * Draw a path, wraps the SVG `path` element.
4558 *
4559 * @param {Array} [path] An SVG path definition in array form.
4560 *
4561 * @example
4562 * var path = renderer.path(['M', 10, 10, 'L', 30, 30, 'z'])
4563 * .attr({ stroke: '#ff00ff' })
4564 * .add();
4565 * @returns {SVGElement} The generated wrapper element.
4566 */
4567 /**
4568 * Draw a path, wraps the SVG `path` element.
4569 *
4570 * @param {SVGAttributes} [attribs] The initial attributes.
4571 * @returns {SVGElement} The generated wrapper element.
4572 */
4573 path: function(path) {
4574 var attribs = {
4575
4576 fill: 'none'
4577
4578 };
4579 if (isArray(path)) {
4580 attribs.d = path;
4581 } else if (isObject(path)) { // attributes
4582 extend(attribs, path);
4583 }
4584 return this.createElement('path').attr(attribs);
4585 },
4586
4587 /**
4588 * Draw a circle, wraps the SVG `circle` element.
4589 *
4590 * @param {number} [x] The center x position.
4591 * @param {number} [y] The center y position.
4592 * @param {number} [r] The radius.
4593 * @returns {SVGElement} The generated wrapper element.
4594 */
4595 /**
4596 * Draw a circle, wraps the SVG `circle` element.
4597 *
4598 * @param {SVGAttributes} [attribs] The initial attributes.
4599 * @returns {SVGElement} The generated wrapper element.
4600 */
4601 circle: function(x, y, r) {
4602 var attribs = isObject(x) ? x : {
4603 x: x,
4604 y: y,
4605 r: r
4606 },
4607 wrapper = this.createElement('circle');
4608
4609 // Setting x or y translates to cx and cy
4610 wrapper.xSetter = wrapper.ySetter = function(value, key, element) {
4611 element.setAttribute('c' + key, value);
4612 };
4613
4614 return wrapper.attr(attribs);
4615 },
4616
4617 /**
4618 * Draw and return an arc.
4619 * @param {number} [x=0] Center X position.
4620 * @param {number} [y=0] Center Y position.
4621 * @param {number} [r=0] The outer radius of the arc.
4622 * @param {number} [innerR=0] Inner radius like used in donut charts.
4623 * @param {number} [start=0] The starting angle of the arc in radians, where
4624 * 0 is to the right and `-Math.PI/2` is up.
4625 * @param {number} [end=0] The ending angle of the arc in radians, where 0
4626 * is to the right and `-Math.PI/2` is up.
4627 * @returns {SVGElement} The generated wrapper element.
4628 */
4629 /**
4630 * Draw and return an arc. Overloaded function that takes arguments object.
4631 * @param {SVGAttributes} attribs Initial SVG attributes.
4632 * @returns {SVGElement} The generated wrapper element.
4633 */
4634 arc: function(x, y, r, innerR, start, end) {
4635 var arc;
4636
4637 if (isObject(x)) {
4638 y = x.y;
4639 r = x.r;
4640 innerR = x.innerR;
4641 start = x.start;
4642 end = x.end;
4643 x = x.x;
4644 }
4645
4646 // Arcs are defined as symbols for the ability to set
4647 // attributes in attr and animate
4648 arc = this.symbol('arc', x || 0, y || 0, r || 0, r || 0, {
4649 innerR: innerR || 0,
4650 start: start || 0,
4651 end: end || 0
4652 });
4653 arc.r = r; // #959
4654 return arc;
4655 },
4656
4657 /**
4658 * Draw and return a rectangle.
4659 * @param {number} [x] Left position.
4660 * @param {number} [y] Top position.
4661 * @param {number} [width] Width of the rectangle.
4662 * @param {number} [height] Height of the rectangle.
4663 * @param {number} [r] Border corner radius.
4664 * @param {number} [strokeWidth] A stroke width can be supplied to allow
4665 * crisp drawing.
4666 * @returns {SVGElement} The generated wrapper element.
4667 */
4668 /**
4669 * Draw and return a rectangle.
4670 * @param {SVGAttributes} [attributes] General SVG attributes for the
4671 * rectangle.
4672 * @returns {SVGElement} The generated wrapper element.
4673 */
4674 rect: function(x, y, width, height, r, strokeWidth) {
4675
4676 r = isObject(x) ? x.r : r;
4677
4678 var wrapper = this.createElement('rect'),
4679 attribs = isObject(x) ? x : x === undefined ? {} : {
4680 x: x,
4681 y: y,
4682 width: Math.max(width, 0),
4683 height: Math.max(height, 0)
4684 };
4685
4686
4687 if (strokeWidth !== undefined) {
4688 attribs.strokeWidth = strokeWidth;
4689 attribs = wrapper.crisp(attribs);
4690 }
4691 attribs.fill = 'none';
4692
4693
4694 if (r) {
4695 attribs.r = r;
4696 }
4697
4698 wrapper.rSetter = function(value, key, element) {
4699 attr(element, {
4700 rx: value,
4701 ry: value
4702 });
4703 };
4704
4705 return wrapper.attr(attribs);
4706 },
4707
4708 /**
4709 * Resize the {@link SVGRenderer#box} and re-align all aligned child
4710 * elements.
4711 * @param {number} width The new pixel width.
4712 * @param {number} height The new pixel height.
4713 * @param {boolean} animate Whether to animate.
4714 */
4715 setSize: function(width, height, animate) {
4716 var renderer = this,
4717 alignedObjects = renderer.alignedObjects,
4718 i = alignedObjects.length;
4719
4720 renderer.width = width;
4721 renderer.height = height;
4722
4723 renderer.boxWrapper.animate({
4724 width: width,
4725 height: height
4726 }, {
4727 step: function() {
4728 this.attr({
4729 viewBox: '0 0 ' + this.attr('width') + ' ' + this.attr('height')
4730 });
4731 },
4732 duration: pick(animate, true) ? undefined : 0
4733 });
4734
4735 while (i--) {
4736 alignedObjects[i].align();
4737 }
4738 },
4739
4740 /**
4741 * Create and return an svg group element.
4742 *
4743 * @param {string} [name] The group will be given a class name of
4744 * `highcharts-{name}`. This can be used for styling and scripting.
4745 * @returns {SVGElement} The generated wrapper element.
4746 */
4747 g: function(name) {
4748 var elem = this.createElement('g');
4749 return name ? elem.attr({
4750 'class': 'highcharts-' + name
4751 }) : elem;
4752 },
4753
4754 /**
4755 * Display an image.
4756 * @param {string} src The image source.
4757 * @param {number} [x] The X position.
4758 * @param {number} [y] The Y position.
4759 * @param {number} [width] The image width. If omitted, it defaults to the
4760 * image file width.
4761 * @param {number} [height] The image height. If omitted it defaults to the
4762 * image file height.
4763 * @returns {SVGElement} The generated wrapper element.
4764 */
4765 image: function(src, x, y, width, height) {
4766 var attribs = {
4767 preserveAspectRatio: 'none'
4768 },
4769 elemWrapper;
4770
4771 // optional properties
4772 if (arguments.length > 1) {
4773 extend(attribs, {
4774 x: x,
4775 y: y,
4776 width: width,
4777 height: height
4778 });
4779 }
4780
4781 elemWrapper = this.createElement('image').attr(attribs);
4782
4783 // set the href in the xlink namespace
4784 if (elemWrapper.element.setAttributeNS) {
4785 elemWrapper.element.setAttributeNS('http://www.w3.org/1999/xlink',
4786 'href', src);
4787 } else {
4788 // could be exporting in IE
4789 // using href throws "not supported" in ie7 and under, requries regex shim to fix later
4790 elemWrapper.element.setAttribute('hc-svg-href', src);
4791 }
4792 return elemWrapper;
4793 },
4794
4795 /**
4796 * Draw a symbol out of pre-defined shape paths from {@SVGRenderer#symbols}.
4797 * It is used in Highcharts for point makers, which cake a `symbol` option,
4798 * and label and button backgrounds like in the tooltip and stock flags.
4799 *
4800 * @param {Symbol} symbol - The symbol name.
4801 * @param {number} x - The X coordinate for the top left position.
4802 * @param {number} y - The Y coordinate for the top left position.
4803 * @param {number} width - The pixel width.
4804 * @param {number} height - The pixel height.
4805 * @param {Object} [options] - Additional options, depending on the actual
4806 * symbol drawn.
4807 * @param {number} [options.anchorX] - The anchor X position for the
4808 * `callout` symbol. This is where the chevron points to.
4809 * @param {number} [options.anchorY] - The anchor Y position for the
4810 * `callout` symbol. This is where the chevron points to.
4811 * @param {number} [options.end] - The end angle of an `arc` symbol.
4812 * @param {boolean} [options.open] - Whether to draw `arc` symbol open or
4813 * closed.
4814 * @param {number} [options.r] - The radius of an `arc` symbol, or the
4815 * border radius for the `callout` symbol.
4816 * @param {number} [options.start] - The start angle of an `arc` symbol.
4817 */
4818 symbol: function(symbol, x, y, width, height, options) {
4819
4820 var ren = this,
4821 obj,
4822
4823 // get the symbol definition function
4824 symbolFn = this.symbols[symbol],
4825
4826 // check if there's a path defined for this symbol
4827 path = defined(x) && symbolFn && symbolFn(
4828 Math.round(x),
4829 Math.round(y),
4830 width,
4831 height,
4832 options
4833 ),
4834 imageRegex = /^url\((.*?)\)$/,
4835 imageSrc,
4836 centerImage;
4837
4838 if (symbolFn) {
4839 obj = this.path(path);
4840
4841
4842 obj.attr('fill', 'none');
4843
4844
4845 // expando properties for use in animate and attr
4846 extend(obj, {
4847 symbolName: symbol,
4848 x: x,
4849 y: y,
4850 width: width,
4851 height: height
4852 });
4853 if (options) {
4854 extend(obj, options);
4855 }
4856
4857
4858 // image symbols
4859 } else if (imageRegex.test(symbol)) {
4860
4861
4862 imageSrc = symbol.match(imageRegex)[1];
4863
4864 // Create the image synchronously, add attribs async
4865 obj = this.image(imageSrc);
4866
4867 // The image width is not always the same as the symbol width. The
4868 // image may be centered within the symbol, as is the case when
4869 // image shapes are used as label backgrounds, for example in flags.
4870 obj.imgwidth = pick(
4871 symbolSizes[imageSrc] && symbolSizes[imageSrc].width,
4872 options && options.width
4873 );
4874 obj.imgheight = pick(
4875 symbolSizes[imageSrc] && symbolSizes[imageSrc].height,
4876 options && options.height
4877 );
4878 /**
4879 * Set the size and position
4880 */
4881 centerImage = function() {
4882 obj.attr({
4883 width: obj.width,
4884 height: obj.height
4885 });
4886 };
4887
4888 /**
4889 * Width and height setters that take both the image's physical size
4890 * and the label size into consideration, and translates the image
4891 * to center within the label.
4892 */
4893 each(['width', 'height'], function(key) {
4894 obj[key + 'Setter'] = function(value, key) {
4895 var attribs = {},
4896 imgSize = this['img' + key],
4897 trans = key === 'width' ? 'translateX' : 'translateY';
4898 this[key] = value;
4899 if (defined(imgSize)) {
4900 if (this.element) {
4901 this.element.setAttribute(key, imgSize);
4902 }
4903 if (!this.alignByTranslate) {
4904 attribs[trans] = ((this[key] || 0) - imgSize) / 2;
4905 this.attr(attribs);
4906 }
4907 }
4908 };
4909 });
4910
4911
4912 if (defined(x)) {
4913 obj.attr({
4914 x: x,
4915 y: y
4916 });
4917 }
4918 obj.isImg = true;
4919
4920 if (defined(obj.imgwidth) && defined(obj.imgheight)) {
4921 centerImage();
4922 } else {
4923 // Initialize image to be 0 size so export will still function if there's no cached sizes.
4924 obj.attr({
4925 width: 0,
4926 height: 0
4927 });
4928
4929 // Create a dummy JavaScript image to get the width and height. Due to a bug in IE < 8,
4930 // the created element must be assigned to a variable in order to load (#292).
4931 createElement('img', {
4932 onload: function() {
4933
4934 var chart = charts[ren.chartIndex];
4935
4936 // Special case for SVGs on IE11, the width is not accessible until the image is
4937 // part of the DOM (#2854).
4938 if (this.width === 0) {
4939 css(this, {
4940 position: 'absolute',
4941 top: '-999em'
4942 });
4943 doc.body.appendChild(this);
4944 }
4945
4946 // Center the image
4947 symbolSizes[imageSrc] = { // Cache for next
4948 width: this.width,
4949 height: this.height
4950 };
4951 obj.imgwidth = this.width;
4952 obj.imgheight = this.height;
4953
4954 if (obj.element) {
4955 centerImage();
4956 }
4957
4958 // Clean up after #2854 workaround.
4959 if (this.parentNode) {
4960 this.parentNode.removeChild(this);
4961 }
4962
4963 // Fire the load event when all external images are loaded
4964 ren.imgCount--;
4965 if (!ren.imgCount && chart && chart.onload) {
4966 chart.onload();
4967 }
4968 },
4969 src: imageSrc
4970 });
4971 this.imgCount++;
4972 }
4973 }
4974
4975 return obj;
4976 },
4977
4978 /**
4979 * @typedef {string} Symbol
4980 *
4981 * Can be one of `arc`, `callout`, `circle`, `diamond`, `square`,
4982 * `triangle`, `triangle-down`. Symbols are used internally for point
4983 * markers, button and label borders and backgrounds, or custom shapes.
4984 * Extendable by adding to {@link SVGRenderer#symbols}.
4985 */
4986 /**
4987 * An extendable collection of functions for defining symbol paths.
4988 */
4989 symbols: {
4990 'circle': function(x, y, w, h) {
4991 var cpw = 0.166 * w;
4992 return [
4993 'M', x + w / 2, y,
4994 'C', x + w + cpw, y, x + w + cpw, y + h, x + w / 2, y + h,
4995 'C', x - cpw, y + h, x - cpw, y, x + w / 2, y,
4996 'Z'
4997 ];
4998 },
4999
5000 'square': function(x, y, w, h) {
5001 return [
5002 'M', x, y,
5003 'L', x + w, y,
5004 x + w, y + h,
5005 x, y + h,
5006 'Z'
5007 ];
5008 },
5009
5010 'triangle': function(x, y, w, h) {
5011 return [
5012 'M', x + w / 2, y,
5013 'L', x + w, y + h,
5014 x, y + h,
5015 'Z'
5016 ];
5017 },
5018
5019 'triangle-down': function(x, y, w, h) {
5020 return [
5021 'M', x, y,
5022 'L', x + w, y,
5023 x + w / 2, y + h,
5024 'Z'
5025 ];
5026 },
5027 'diamond': function(x, y, w, h) {
5028 return [
5029 'M', x + w / 2, y,
5030 'L', x + w, y + h / 2,
5031 x + w / 2, y + h,
5032 x, y + h / 2,
5033 'Z'
5034 ];
5035 },
5036 'arc': function(x, y, w, h, options) {
5037 var start = options.start,
5038 radius = options.r || w || h,
5039 end = options.end - 0.001, // to prevent cos and sin of start and end from becoming equal on 360 arcs (related: #1561)
5040 innerRadius = options.innerR,
5041 open = options.open,
5042 cosStart = Math.cos(start),
5043 sinStart = Math.sin(start),
5044 cosEnd = Math.cos(end),
5045 sinEnd = Math.sin(end),
5046 longArc = options.end - start < Math.PI ? 0 : 1;
5047
5048 return [
5049 'M',
5050 x + radius * cosStart,
5051 y + radius * sinStart,
5052 'A', // arcTo
5053 radius, // x radius
5054 radius, // y radius
5055 0, // slanting
5056 longArc, // long or short arc
5057 1, // clockwise
5058 x + radius * cosEnd,
5059 y + radius * sinEnd,
5060 open ? 'M' : 'L',
5061 x + innerRadius * cosEnd,
5062 y + innerRadius * sinEnd,
5063 'A', // arcTo
5064 innerRadius, // x radius
5065 innerRadius, // y radius
5066 0, // slanting
5067 longArc, // long or short arc
5068 0, // clockwise
5069 x + innerRadius * cosStart,
5070 y + innerRadius * sinStart,
5071
5072 open ? '' : 'Z' // close
5073 ];
5074 },
5075
5076 /**
5077 * Callout shape used for default tooltips, also used for rounded rectangles in VML
5078 */
5079 callout: function(x, y, w, h, options) {
5080 var arrowLength = 6,
5081 halfDistance = 6,
5082 r = Math.min((options && options.r) || 0, w, h),
5083 safeDistance = r + halfDistance,
5084 anchorX = options && options.anchorX,
5085 anchorY = options && options.anchorY,
5086 path;
5087
5088 path = [
5089 'M', x + r, y,
5090 'L', x + w - r, y, // top side
5091 'C', x + w, y, x + w, y, x + w, y + r, // top-right corner
5092 'L', x + w, y + h - r, // right side
5093 'C', x + w, y + h, x + w, y + h, x + w - r, y + h, // bottom-right corner
5094 'L', x + r, y + h, // bottom side
5095 'C', x, y + h, x, y + h, x, y + h - r, // bottom-left corner
5096 'L', x, y + r, // left side
5097 'C', x, y, x, y, x + r, y // top-left corner
5098 ];
5099
5100 // Anchor on right side
5101 if (anchorX && anchorX > w) {
5102
5103 // Chevron
5104 if (anchorY > y + safeDistance && anchorY < y + h - safeDistance) {
5105 path.splice(13, 3,
5106 'L', x + w, anchorY - halfDistance,
5107 x + w + arrowLength, anchorY,
5108 x + w, anchorY + halfDistance,
5109 x + w, y + h - r
5110 );
5111
5112 // Simple connector
5113 } else {
5114 path.splice(13, 3,
5115 'L', x + w, h / 2,
5116 anchorX, anchorY,
5117 x + w, h / 2,
5118 x + w, y + h - r
5119 );
5120 }
5121
5122 // Anchor on left side
5123 } else if (anchorX && anchorX < 0) {
5124
5125 // Chevron
5126 if (anchorY > y + safeDistance && anchorY < y + h - safeDistance) {
5127 path.splice(33, 3,
5128 'L', x, anchorY + halfDistance,
5129 x - arrowLength, anchorY,
5130 x, anchorY - halfDistance,
5131 x, y + r
5132 );
5133
5134 // Simple connector
5135 } else {
5136 path.splice(33, 3,
5137 'L', x, h / 2,
5138 anchorX, anchorY,
5139 x, h / 2,
5140 x, y + r
5141 );
5142 }
5143
5144 } else if (anchorY && anchorY > h && anchorX > x + safeDistance && anchorX < x + w - safeDistance) { // replace bottom
5145 path.splice(23, 3,
5146 'L', anchorX + halfDistance, y + h,
5147 anchorX, y + h + arrowLength,
5148 anchorX - halfDistance, y + h,
5149 x + r, y + h
5150 );
5151 } else if (anchorY && anchorY < 0 && anchorX > x + safeDistance && anchorX < x + w - safeDistance) { // replace top
5152 path.splice(3, 3,
5153 'L', anchorX - halfDistance, y,
5154 anchorX, y - arrowLength,
5155 anchorX + halfDistance, y,
5156 w - r, y
5157 );
5158 }
5159
5160 return path;
5161 }
5162 },
5163
5164 /**
5165 * @typedef {SVGElement} ClipRect - A clipping rectangle that can be applied
5166 * to one or more {@link SVGElement} instances. It is instanciated with the
5167 * {@link SVGRenderer#clipRect} function and applied with the {@link
5168 * SVGElement#clip} function.
5169 *
5170 * @example
5171 * var circle = renderer.circle(100, 100, 100)
5172 * .attr({ fill: 'red' })
5173 * .add();
5174 * var clipRect = renderer.clipRect(100, 100, 100, 100);
5175 *
5176 * // Leave only the lower right quarter visible
5177 * circle.clip(clipRect);
5178 */
5179 /**
5180 * Define a clipping rectangle
5181 * @param {String} id
5182 * @param {number} x
5183 * @param {number} y
5184 * @param {number} width
5185 * @param {number} height
5186 * @returns {ClipRect} A clipping rectangle.
5187 */
5188 clipRect: function(x, y, width, height) {
5189 var wrapper,
5190 id = H.uniqueKey(),
5191
5192 clipPath = this.createElement('clipPath').attr({
5193 id: id
5194 }).add(this.defs);
5195
5196 wrapper = this.rect(x, y, width, height, 0).add(clipPath);
5197 wrapper.id = id;
5198 wrapper.clipPath = clipPath;
5199 wrapper.count = 0;
5200
5201 return wrapper;
5202 },
5203
5204
5205
5206
5207
5208 /**
5209 * Add text to the SVG object
5210 * @param {String} str
5211 * @param {number} x Left position
5212 * @param {number} y Top position
5213 * @param {Boolean} useHTML Use HTML to render the text
5214 */
5215 text: function(str, x, y, useHTML) {
5216
5217 // declare variables
5218 var renderer = this,
5219 fakeSVG = !svg && renderer.forExport,
5220 wrapper,
5221 attribs = {};
5222
5223 if (useHTML && (renderer.allowHTML || !renderer.forExport)) {
5224 return renderer.html(str, x, y);
5225 }
5226
5227 attribs.x = Math.round(x || 0); // X is always needed for line-wrap logic
5228 if (y) {
5229 attribs.y = Math.round(y);
5230 }
5231 if (str || str === 0) {
5232 attribs.text = str;
5233 }
5234
5235 wrapper = renderer.createElement('text')
5236 .attr(attribs);
5237
5238 // Prevent wrapping from creating false offsetWidths in export in legacy IE (#1079, #1063)
5239 if (fakeSVG) {
5240 wrapper.css({
5241 position: 'absolute'
5242 });
5243 }
5244
5245 if (!useHTML) {
5246 wrapper.xSetter = function(value, key, element) {
5247 var tspans = element.getElementsByTagName('tspan'),
5248 tspan,
5249 parentVal = element.getAttribute(key),
5250 i;
5251 for (i = 0; i < tspans.length; i++) {
5252 tspan = tspans[i];
5253 // If the x values are equal, the tspan represents a linebreak
5254 if (tspan.getAttribute(key) === parentVal) {
5255 tspan.setAttribute(key, value);
5256 }
5257 }
5258 element.setAttribute(key, value);
5259 };
5260 }
5261
5262 return wrapper;
5263 },
5264
5265 /**
5266 * Utility to return the baseline offset and total line height from the font
5267 * size.
5268 *
5269 * @param {?string} fontSize The current font size to inspect. If not given,
5270 * the font size will be found from the DOM element.
5271 * @param {SVGElement|SVGDOMElement} [elem] The element to inspect for a
5272 * current font size.
5273 * @returns {Object} An object containing `h`: the line height, `b`: the
5274 * baseline relative to the top of the box, and `f`: the font size.
5275 */
5276 fontMetrics: function(fontSize, elem) {
5277 var lineHeight,
5278 baseline;
5279
5280
5281 fontSize = fontSize ||
5282 // When the elem is a DOM element (#5932)
5283 (elem && elem.style && elem.style.fontSize) ||
5284 // Fall back on the renderer style default
5285 (this.style && this.style.fontSize);
5286
5287
5288
5289 // Handle different units
5290 if (/px/.test(fontSize)) {
5291 fontSize = pInt(fontSize);
5292 } else if (/em/.test(fontSize)) {
5293 // The em unit depends on parent items
5294 fontSize = parseFloat(fontSize) *
5295 (elem ? this.fontMetrics(null, elem.parentNode).f : 16);
5296 } else {
5297 fontSize = 12;
5298 }
5299
5300 // Empirical values found by comparing font size and bounding box
5301 // height. Applies to the default font family.
5302 // http://jsfiddle.net/highcharts/7xvn7/
5303 lineHeight = fontSize < 24 ? fontSize + 3 : Math.round(fontSize * 1.2);
5304 baseline = Math.round(lineHeight * 0.8);
5305
5306 return {
5307 h: lineHeight,
5308 b: baseline,
5309 f: fontSize
5310 };
5311 },
5312
5313 /**
5314 * Correct X and Y positioning of a label for rotation (#1764)
5315 */
5316 rotCorr: function(baseline, rotation, alterY) {
5317 var y = baseline;
5318 if (rotation && alterY) {
5319 y = Math.max(y * Math.cos(rotation * deg2rad), 4);
5320 }
5321 return {
5322 x: (-baseline / 3) * Math.sin(rotation * deg2rad),
5323 y: y
5324 };
5325 },
5326
5327 /**
5328 * Add a label, a text item that can hold a colored or gradient background
5329 * as well as a border and shadow. Supported custom attributes include
5330 * `padding`.
5331 *
5332 * @param {string} str
5333 * @param {number} x
5334 * @param {number} y
5335 * @param {String} shape
5336 * @param {number} anchorX In case the shape has a pointer, like a flag, this is the
5337 * coordinates it should be pinned to
5338 * @param {number} anchorY
5339 * @param {Boolean} baseline Whether to position the label relative to the text baseline,
5340 * like renderer.text, or to the upper border of the rectangle.
5341 * @param {String} className Class name for the group
5342 */
5343 label: function(str, x, y, shape, anchorX, anchorY, useHTML, baseline, className) {
5344
5345 var renderer = this,
5346 wrapper = renderer.g(className !== 'button' && 'label'),
5347 text = wrapper.text = renderer.text('', 0, 0, useHTML)
5348 .attr({
5349 zIndex: 1
5350 }),
5351 box,
5352 bBox,
5353 alignFactor = 0,
5354 padding = 3,
5355 paddingLeft = 0,
5356 width,
5357 height,
5358 wrapperX,
5359 wrapperY,
5360 textAlign,
5361 deferredAttr = {},
5362 strokeWidth,
5363 baselineOffset,
5364 hasBGImage = /^url\((.*?)\)$/.test(shape),
5365 needsBox = hasBGImage,
5366 getCrispAdjust,
5367 updateBoxSize,
5368 updateTextPadding,
5369 boxAttr;
5370
5371 if (className) {
5372 wrapper.addClass('highcharts-' + className);
5373 }
5374
5375
5376 needsBox = hasBGImage;
5377 getCrispAdjust = function() {
5378 return (strokeWidth || 0) % 2 / 2;
5379 };
5380
5381
5382
5383 /**
5384 * This function runs after the label is added to the DOM (when the bounding box is
5385 * available), and after the text of the label is updated to detect the new bounding
5386 * box and reflect it in the border box.
5387 */
5388 updateBoxSize = function() {
5389 var style = text.element.style,
5390 crispAdjust,
5391 attribs = {};
5392
5393 bBox = (width === undefined || height === undefined || textAlign) && defined(text.textStr) &&
5394 text.getBBox(); //#3295 && 3514 box failure when string equals 0
5395 wrapper.width = (width || bBox.width || 0) + 2 * padding + paddingLeft;
5396 wrapper.height = (height || bBox.height || 0) + 2 * padding;
5397
5398 // Update the label-scoped y offset
5399 baselineOffset = padding + renderer.fontMetrics(style && style.fontSize, text).b;
5400
5401
5402 if (needsBox) {
5403
5404 // Create the border box if it is not already present
5405 if (!box) {
5406 wrapper.box = box = renderer.symbols[shape] || hasBGImage ? // Symbol definition exists (#5324)
5407 renderer.symbol(shape) :
5408 renderer.rect();
5409
5410 box.addClass(
5411 (className === 'button' ? '' : 'highcharts-label-box') + // Don't use label className for buttons
5412 (className ? ' highcharts-' + className + '-box' : '')
5413 );
5414
5415 box.add(wrapper);
5416
5417 crispAdjust = getCrispAdjust();
5418 attribs.x = crispAdjust;
5419 attribs.y = (baseline ? -baselineOffset : 0) + crispAdjust;
5420 }
5421
5422 // Apply the box attributes
5423 attribs.width = Math.round(wrapper.width);
5424 attribs.height = Math.round(wrapper.height);
5425
5426 box.attr(extend(attribs, deferredAttr));
5427 deferredAttr = {};
5428 }
5429 };
5430
5431 /**
5432 * This function runs after setting text or padding, but only if padding is changed
5433 */
5434 updateTextPadding = function() {
5435 var textX = paddingLeft + padding,
5436 textY;
5437
5438 // determin y based on the baseline
5439 textY = baseline ? 0 : baselineOffset;
5440
5441 // compensate for alignment
5442 if (defined(width) && bBox && (textAlign === 'center' || textAlign === 'right')) {
5443 textX += {
5444 center: 0.5,
5445 right: 1
5446 }[textAlign] * (width - bBox.width);
5447 }
5448
5449 // update if anything changed
5450 if (textX !== text.x || textY !== text.y) {
5451 text.attr('x', textX);
5452 if (textY !== undefined) {
5453 text.attr('y', textY);
5454 }
5455 }
5456
5457 // record current values
5458 text.x = textX;
5459 text.y = textY;
5460 };
5461
5462 /**
5463 * Set a box attribute, or defer it if the box is not yet created
5464 * @param {Object} key
5465 * @param {Object} value
5466 */
5467 boxAttr = function(key, value) {
5468 if (box) {
5469 box.attr(key, value);
5470 } else {
5471 deferredAttr[key] = value;
5472 }
5473 };
5474
5475 /**
5476 * After the text element is added, get the desired size of the border box
5477 * and add it before the text in the DOM.
5478 */
5479 wrapper.onAdd = function() {
5480 text.add(wrapper);
5481 wrapper.attr({
5482 text: (str || str === 0) ? str : '', // alignment is available now // #3295: 0 not rendered if given as a value
5483 x: x,
5484 y: y
5485 });
5486
5487 if (box && defined(anchorX)) {
5488 wrapper.attr({
5489 anchorX: anchorX,
5490 anchorY: anchorY
5491 });
5492 }
5493 };
5494
5495 /*
5496 * Add specific attribute setters.
5497 */
5498
5499 // only change local variables
5500 wrapper.widthSetter = function(value) {
5501 width = value;
5502 };
5503 wrapper.heightSetter = function(value) {
5504 height = value;
5505 };
5506 wrapper['text-alignSetter'] = function(value) {
5507 textAlign = value;
5508 };
5509 wrapper.paddingSetter = function(value) {
5510 if (defined(value) && value !== padding) {
5511 padding = wrapper.padding = value;
5512 updateTextPadding();
5513 }
5514 };
5515 wrapper.paddingLeftSetter = function(value) {
5516 if (defined(value) && value !== paddingLeft) {
5517 paddingLeft = value;
5518 updateTextPadding();
5519 }
5520 };
5521
5522
5523 // change local variable and prevent setting attribute on the group
5524 wrapper.alignSetter = function(value) {
5525 value = {
5526 left: 0,
5527 center: 0.5,
5528 right: 1
5529 }[value];
5530 if (value !== alignFactor) {
5531 alignFactor = value;
5532 if (bBox) { // Bounding box exists, means we're dynamically changing
5533 wrapper.attr({
5534 x: wrapperX
5535 }); // #5134
5536 }
5537 }
5538 };
5539
5540 // apply these to the box and the text alike
5541 wrapper.textSetter = function(value) {
5542 if (value !== undefined) {
5543 text.textSetter(value);
5544 }
5545 updateBoxSize();
5546 updateTextPadding();
5547 };
5548
5549 // apply these to the box but not to the text
5550 wrapper['stroke-widthSetter'] = function(value, key) {
5551 if (value) {
5552 needsBox = true;
5553 }
5554 strokeWidth = this['stroke-width'] = value;
5555 boxAttr(key, value);
5556 };
5557
5558 wrapper.strokeSetter = wrapper.fillSetter = wrapper.rSetter = function(value, key) {
5559 if (key === 'fill' && value) {
5560 needsBox = true;
5561 }
5562 boxAttr(key, value);
5563 };
5564
5565 wrapper.anchorXSetter = function(value, key) {
5566 anchorX = value;
5567 boxAttr(key, Math.round(value) - getCrispAdjust() - wrapperX);
5568 };
5569 wrapper.anchorYSetter = function(value, key) {
5570 anchorY = value;
5571 boxAttr(key, value - wrapperY);
5572 };
5573
5574 // rename attributes
5575 wrapper.xSetter = function(value) {
5576 wrapper.x = value; // for animation getter
5577 if (alignFactor) {
5578 value -= alignFactor * ((width || bBox.width) + 2 * padding);
5579 }
5580 wrapperX = Math.round(value);
5581 wrapper.attr('translateX', wrapperX);
5582 };
5583 wrapper.ySetter = function(value) {
5584 wrapperY = wrapper.y = Math.round(value);
5585 wrapper.attr('translateY', wrapperY);
5586 };
5587
5588 // Redirect certain methods to either the box or the text
5589 var baseCss = wrapper.css;
5590 return extend(wrapper, {
5591 /**
5592 * Pick up some properties and apply them to the text instead of the
5593 * wrapper.
5594 * @ignore
5595 */
5596 css: function(styles) {
5597 if (styles) {
5598 var textStyles = {};
5599 styles = merge(styles); // create a copy to avoid altering the original object (#537)
5600 each(wrapper.textProps, function(prop) {
5601 if (styles[prop] !== undefined) {
5602 textStyles[prop] = styles[prop];
5603 delete styles[prop];
5604 }
5605 });
5606 text.css(textStyles);
5607 }
5608 return baseCss.call(wrapper, styles);
5609 },
5610 /**
5611 * Return the bounding box of the box, not the group.
5612 * @ignore
5613 */
5614 getBBox: function() {
5615 return {
5616 width: bBox.width + 2 * padding,
5617 height: bBox.height + 2 * padding,
5618 x: bBox.x - padding,
5619 y: bBox.y - padding
5620 };
5621 },
5622
5623 /**
5624 * Apply the shadow to the box.
5625 * @ignore
5626 */
5627 shadow: function(b) {
5628 if (b) {
5629 updateBoxSize();
5630 if (box) {
5631 box.shadow(b);
5632 }
5633 }
5634 return wrapper;
5635 },
5636
5637 /**
5638 * Destroy and release memory.
5639 * @ignore
5640 */
5641 destroy: function() {
5642
5643 // Added by button implementation
5644 removeEvent(wrapper.element, 'mouseenter');
5645 removeEvent(wrapper.element, 'mouseleave');
5646
5647 if (text) {
5648 text = text.destroy();
5649 }
5650 if (box) {
5651 box = box.destroy();
5652 }
5653 // Call base implementation to destroy the rest
5654 SVGElement.prototype.destroy.call(wrapper);
5655
5656 // Release local pointers (#1298)
5657 wrapper = renderer = updateBoxSize = updateTextPadding = boxAttr = null;
5658 }
5659 });
5660 }
5661 }; // end SVGRenderer
5662
5663
5664 // general renderer
5665 H.Renderer = SVGRenderer;
5666
5667 }(Highcharts));
5668 (function(H) {
5669 /**
5670 * (c) 2010-2016 Torstein Honsi
5671 *
5672 * License: www.highcharts.com/license
5673 */
5674 'use strict';
5675 var attr = H.attr,
5676 createElement = H.createElement,
5677 css = H.css,
5678 defined = H.defined,
5679 each = H.each,
5680 extend = H.extend,
5681 isFirefox = H.isFirefox,
5682 isMS = H.isMS,
5683 isWebKit = H.isWebKit,
5684 pInt = H.pInt,
5685 SVGElement = H.SVGElement,
5686 SVGRenderer = H.SVGRenderer,
5687 win = H.win,
5688 wrap = H.wrap;
5689
5690 // Extend SvgElement for useHTML option
5691 extend(SVGElement.prototype, /** @lends SVGElement.prototype */ {
5692 /**
5693 * Apply CSS to HTML elements. This is used in text within SVG rendering and
5694 * by the VML renderer
5695 */
5696 htmlCss: function(styles) {
5697 var wrapper = this,
5698 element = wrapper.element,
5699 textWidth = styles && element.tagName === 'SPAN' && styles.width;
5700
5701 if (textWidth) {
5702 delete styles.width;
5703 wrapper.textWidth = textWidth;
5704 wrapper.updateTransform();
5705 }
5706 if (styles && styles.textOverflow === 'ellipsis') {
5707 styles.whiteSpace = 'nowrap';
5708 styles.overflow = 'hidden';
5709 }
5710 wrapper.styles = extend(wrapper.styles, styles);
5711 css(wrapper.element, styles);
5712
5713 return wrapper;
5714 },
5715
5716 /**
5717 * VML and useHTML method for calculating the bounding box based on offsets
5718 * @param {Boolean} refresh Whether to force a fresh value from the DOM or to
5719 * use the cached value
5720 *
5721 * @return {Object} A hash containing values for x, y, width and height
5722 */
5723
5724 htmlGetBBox: function() {
5725 var wrapper = this,
5726 element = wrapper.element;
5727
5728 // faking getBBox in exported SVG in legacy IE
5729 // faking getBBox in exported SVG in legacy IE (is this a duplicate of the fix for #1079?)
5730 if (element.nodeName === 'text') {
5731 element.style.position = 'absolute';
5732 }
5733
5734 return {
5735 x: element.offsetLeft,
5736 y: element.offsetTop,
5737 width: element.offsetWidth,
5738 height: element.offsetHeight
5739 };
5740 },
5741
5742 /**
5743 * VML override private method to update elements based on internal
5744 * properties based on SVG transform
5745 */
5746 htmlUpdateTransform: function() {
5747 // aligning non added elements is expensive
5748 if (!this.added) {
5749 this.alignOnAdd = true;
5750 return;
5751 }
5752
5753 var wrapper = this,
5754 renderer = wrapper.renderer,
5755 elem = wrapper.element,
5756 translateX = wrapper.translateX || 0,
5757 translateY = wrapper.translateY || 0,
5758 x = wrapper.x || 0,
5759 y = wrapper.y || 0,
5760 align = wrapper.textAlign || 'left',
5761 alignCorrection = {
5762 left: 0,
5763 center: 0.5,
5764 right: 1
5765 }[align],
5766 styles = wrapper.styles;
5767
5768 // apply translate
5769 css(elem, {
5770 marginLeft: translateX,
5771 marginTop: translateY
5772 });
5773
5774
5775 if (wrapper.shadows) { // used in labels/tooltip
5776 each(wrapper.shadows, function(shadow) {
5777 css(shadow, {
5778 marginLeft: translateX + 1,
5779 marginTop: translateY + 1
5780 });
5781 });
5782 }
5783
5784
5785 // apply inversion
5786 if (wrapper.inverted) { // wrapper is a group
5787 each(elem.childNodes, function(child) {
5788 renderer.invertChild(child, elem);
5789 });
5790 }
5791
5792 if (elem.tagName === 'SPAN') {
5793
5794 var rotation = wrapper.rotation,
5795 baseline,
5796 textWidth = pInt(wrapper.textWidth),
5797 whiteSpace = styles && styles.whiteSpace,
5798 currentTextTransform = [rotation, align, elem.innerHTML, wrapper.textWidth, wrapper.textAlign].join(',');
5799
5800 if (currentTextTransform !== wrapper.cTT) { // do the calculations and DOM access only if properties changed
5801
5802
5803 baseline = renderer.fontMetrics(elem.style.fontSize).b;
5804
5805 // Renderer specific handling of span rotation
5806 if (defined(rotation)) {
5807 wrapper.setSpanRotation(rotation, alignCorrection, baseline);
5808 }
5809
5810 // Reset multiline/ellipsis in order to read width (#4928, #5417)
5811 css(elem, {
5812 width: '',
5813 whiteSpace: whiteSpace || 'nowrap'
5814 });
5815
5816 // Update textWidth
5817 if (elem.offsetWidth > textWidth && /[ \-]/.test(elem.textContent || elem.innerText)) { // #983, #1254
5818 css(elem, {
5819 width: textWidth + 'px',
5820 display: 'block',
5821 whiteSpace: whiteSpace || 'normal' // #3331
5822 });
5823 }
5824
5825
5826 wrapper.getSpanCorrection(elem.offsetWidth, baseline, alignCorrection, rotation, align);
5827 }
5828
5829 // apply position with correction
5830 css(elem, {
5831 left: (x + (wrapper.xCorr || 0)) + 'px',
5832 top: (y + (wrapper.yCorr || 0)) + 'px'
5833 });
5834
5835 // force reflow in webkit to apply the left and top on useHTML element (#1249)
5836 if (isWebKit) {
5837 baseline = elem.offsetHeight; // assigned to baseline for lint purpose
5838 }
5839
5840 // record current text transform
5841 wrapper.cTT = currentTextTransform;
5842 }
5843 },
5844
5845 /**
5846 * Set the rotation of an individual HTML span
5847 */
5848 setSpanRotation: function(rotation, alignCorrection, baseline) {
5849 var rotationStyle = {},
5850 cssTransformKey = isMS ? '-ms-transform' : isWebKit ? '-webkit-transform' : isFirefox ? 'MozTransform' : win.opera ? '-o-transform' : '';
5851
5852 rotationStyle[cssTransformKey] = rotationStyle.transform = 'rotate(' + rotation + 'deg)';
5853 rotationStyle[cssTransformKey + (isFirefox ? 'Origin' : '-origin')] = rotationStyle.transformOrigin = (alignCorrection * 100) + '% ' + baseline + 'px';
5854 css(this.element, rotationStyle);
5855 },
5856
5857 /**
5858 * Get the correction in X and Y positioning as the element is rotated.
5859 */
5860 getSpanCorrection: function(width, baseline, alignCorrection) {
5861 this.xCorr = -width * alignCorrection;
5862 this.yCorr = -baseline;
5863 }
5864 });
5865
5866 // Extend SvgRenderer for useHTML option.
5867 extend(SVGRenderer.prototype, /** @lends SVGRenderer.prototype */ {
5868 /**
5869 * Create HTML text node. This is used by the VML renderer as well as the SVG
5870 * renderer through the useHTML option.
5871 *
5872 * @param {String} str
5873 * @param {Number} x
5874 * @param {Number} y
5875 */
5876 html: function(str, x, y) {
5877 var wrapper = this.createElement('span'),
5878 element = wrapper.element,
5879 renderer = wrapper.renderer,
5880 isSVG = renderer.isSVG,
5881 addSetters = function(element, style) {
5882 // These properties are set as attributes on the SVG group, and as
5883 // identical CSS properties on the div. (#3542)
5884 each(['opacity', 'visibility'], function(prop) {
5885 wrap(element, prop + 'Setter', function(proceed, value, key, elem) {
5886 proceed.call(this, value, key, elem);
5887 style[key] = value;
5888 });
5889 });
5890 };
5891
5892 // Text setter
5893 wrapper.textSetter = function(value) {
5894 if (value !== element.innerHTML) {
5895 delete this.bBox;
5896 }
5897 element.innerHTML = this.textStr = value;
5898 wrapper.htmlUpdateTransform();
5899 };
5900
5901 // Add setters for the element itself (#4938)
5902 if (isSVG) { // #4938, only for HTML within SVG
5903 addSetters(wrapper, wrapper.element.style);
5904 }
5905
5906 // Various setters which rely on update transform
5907 wrapper.xSetter = wrapper.ySetter = wrapper.alignSetter = wrapper.rotationSetter = function(value, key) {
5908 if (key === 'align') {
5909 key = 'textAlign'; // Do not overwrite the SVGElement.align method. Same as VML.
5910 }
5911 wrapper[key] = value;
5912 wrapper.htmlUpdateTransform();
5913 };
5914
5915 // Set the default attributes
5916 wrapper
5917 .attr({
5918 text: str,
5919 x: Math.round(x),
5920 y: Math.round(y)
5921 })
5922 .css({
5923
5924 fontFamily: this.style.fontFamily,
5925 fontSize: this.style.fontSize,
5926
5927 position: 'absolute'
5928 });
5929
5930 // Keep the whiteSpace style outside the wrapper.styles collection
5931 element.style.whiteSpace = 'nowrap';
5932
5933 // Use the HTML specific .css method
5934 wrapper.css = wrapper.htmlCss;
5935
5936 // This is specific for HTML within SVG
5937 if (isSVG) {
5938 wrapper.add = function(svgGroupWrapper) {
5939
5940 var htmlGroup,
5941 container = renderer.box.parentNode,
5942 parentGroup,
5943 parents = [];
5944
5945 this.parentGroup = svgGroupWrapper;
5946
5947 // Create a mock group to hold the HTML elements
5948 if (svgGroupWrapper) {
5949 htmlGroup = svgGroupWrapper.div;
5950 if (!htmlGroup) {
5951
5952 // Read the parent chain into an array and read from top down
5953 parentGroup = svgGroupWrapper;
5954 while (parentGroup) {
5955
5956 parents.push(parentGroup);
5957
5958 // Move up to the next parent group
5959 parentGroup = parentGroup.parentGroup;
5960 }
5961
5962 // Ensure dynamically updating position when any parent is translated
5963 each(parents.reverse(), function(parentGroup) {
5964 var htmlGroupStyle,
5965 cls = attr(parentGroup.element, 'class');
5966
5967 if (cls) {
5968 cls = {
5969 className: cls
5970 };
5971 } // else null
5972
5973 // Create a HTML div and append it to the parent div to emulate
5974 // the SVG group structure
5975 htmlGroup = parentGroup.div = parentGroup.div || createElement('div', cls, {
5976 position: 'absolute',
5977 left: (parentGroup.translateX || 0) + 'px',
5978 top: (parentGroup.translateY || 0) + 'px',
5979 display: parentGroup.display,
5980 opacity: parentGroup.opacity, // #5075
5981 pointerEvents: parentGroup.styles && parentGroup.styles.pointerEvents // #5595
5982 }, htmlGroup || container); // the top group is appended to container
5983
5984 // Shortcut
5985 htmlGroupStyle = htmlGroup.style;
5986
5987 // Set listeners to update the HTML div's position whenever the SVG group
5988 // position is changed
5989 extend(parentGroup, {
5990 on: function() {
5991 wrapper.on.apply({
5992 element: parents[0].div
5993 }, arguments);
5994 return parentGroup;
5995 },
5996 translateXSetter: function(value, key) {
5997 htmlGroupStyle.left = value + 'px';
5998 parentGroup[key] = value;
5999 parentGroup.doTransform = true;
6000 },
6001 translateYSetter: function(value, key) {
6002 htmlGroupStyle.top = value + 'px';
6003 parentGroup[key] = value;
6004 parentGroup.doTransform = true;
6005 }
6006 });
6007 addSetters(parentGroup, htmlGroupStyle);
6008 });
6009
6010 }
6011 } else {
6012 htmlGroup = container;
6013 }
6014
6015 htmlGroup.appendChild(element);
6016
6017 // Shared with VML:
6018 wrapper.added = true;
6019 if (wrapper.alignOnAdd) {
6020 wrapper.htmlUpdateTransform();
6021 }
6022
6023 return wrapper;
6024 };
6025 }
6026 return wrapper;
6027 }
6028 });
6029
6030 }(Highcharts));
6031 (function(H) {
6032 /**
6033 * (c) 2010-2016 Torstein Honsi
6034 *
6035 * License: www.highcharts.com/license
6036 */
6037 'use strict';
6038
6039 var VMLRenderer,
6040 VMLRendererExtension,
6041 VMLElement,
6042
6043 createElement = H.createElement,
6044 css = H.css,
6045 defined = H.defined,
6046 deg2rad = H.deg2rad,
6047 discardElement = H.discardElement,
6048 doc = H.doc,
6049 each = H.each,
6050 erase = H.erase,
6051 extend = H.extend,
6052 extendClass = H.extendClass,
6053 isArray = H.isArray,
6054 isNumber = H.isNumber,
6055 isObject = H.isObject,
6056 merge = H.merge,
6057 noop = H.noop,
6058 pick = H.pick,
6059 pInt = H.pInt,
6060 svg = H.svg,
6061 SVGElement = H.SVGElement,
6062 SVGRenderer = H.SVGRenderer,
6063 win = H.win;
6064
6065 /* ****************************************************************************
6066 * *
6067 * START OF INTERNET EXPLORER <= 8 SPECIFIC CODE *
6068 * *
6069 * For applications and websites that don't need IE support, like platform *
6070 * targeted mobile apps and web apps, this code can be removed. *
6071 * *
6072 *****************************************************************************/
6073
6074 /**
6075 * @constructor
6076 */
6077 if (!svg) {
6078
6079 /**
6080 * The VML element wrapper.
6081 */
6082 VMLElement = {
6083
6084 docMode8: doc && doc.documentMode === 8,
6085
6086 /**
6087 * Initialize a new VML element wrapper. It builds the markup as a string
6088 * to minimize DOM traffic.
6089 * @param {Object} renderer
6090 * @param {Object} nodeName
6091 */
6092 init: function(renderer, nodeName) {
6093 var wrapper = this,
6094 markup = ['<', nodeName, ' filled="f" stroked="f"'],
6095 style = ['position: ', 'absolute', ';'],
6096 isDiv = nodeName === 'div';
6097
6098 // divs and shapes need size
6099 if (nodeName === 'shape' || isDiv) {
6100 style.push('left:0;top:0;width:1px;height:1px;');
6101 }
6102 style.push('visibility: ', isDiv ? 'hidden' : 'visible');
6103
6104 markup.push(' style="', style.join(''), '"/>');
6105
6106 // create element with default attributes and style
6107 if (nodeName) {
6108 markup = isDiv || nodeName === 'span' || nodeName === 'img' ?
6109 markup.join('') :
6110 renderer.prepVML(markup);
6111 wrapper.element = createElement(markup);
6112 }
6113
6114 wrapper.renderer = renderer;
6115 },
6116
6117 /**
6118 * Add the node to the given parent
6119 * @param {Object} parent
6120 */
6121 add: function(parent) {
6122 var wrapper = this,
6123 renderer = wrapper.renderer,
6124 element = wrapper.element,
6125 box = renderer.box,
6126 inverted = parent && parent.inverted,
6127
6128 // get the parent node
6129 parentNode = parent ?
6130 parent.element || parent :
6131 box;
6132
6133 if (parent) {
6134 this.parentGroup = parent;
6135 }
6136
6137 // if the parent group is inverted, apply inversion on all children
6138 if (inverted) { // only on groups
6139 renderer.invertChild(element, parentNode);
6140 }
6141
6142 // append it
6143 parentNode.appendChild(element);
6144
6145 // align text after adding to be able to read offset
6146 wrapper.added = true;
6147 if (wrapper.alignOnAdd && !wrapper.deferUpdateTransform) {
6148 wrapper.updateTransform();
6149 }
6150
6151 // fire an event for internal hooks
6152 if (wrapper.onAdd) {
6153 wrapper.onAdd();
6154 }
6155
6156 // IE8 Standards can't set the class name before the element is appended
6157 if (this.className) {
6158 this.attr('class', this.className);
6159 }
6160
6161 return wrapper;
6162 },
6163
6164 /**
6165 * VML always uses htmlUpdateTransform
6166 */
6167 updateTransform: SVGElement.prototype.htmlUpdateTransform,
6168
6169 /**
6170 * Set the rotation of a span with oldIE's filter
6171 */
6172 setSpanRotation: function() {
6173 // Adjust for alignment and rotation. Rotation of useHTML content is not yet implemented
6174 // but it can probably be implemented for Firefox 3.5+ on user request. FF3.5+
6175 // has support for CSS3 transform. The getBBox method also needs to be updated
6176 // to compensate for the rotation, like it currently does for SVG.
6177 // Test case: http://jsfiddle.net/highcharts/Ybt44/
6178
6179 var rotation = this.rotation,
6180 costheta = Math.cos(rotation * deg2rad),
6181 sintheta = Math.sin(rotation * deg2rad);
6182
6183 css(this.element, {
6184 filter: rotation ? ['progid:DXImageTransform.Microsoft.Matrix(M11=', costheta,
6185 ', M12=', -sintheta, ', M21=', sintheta, ', M22=', costheta,
6186 ', sizingMethod=\'auto expand\')'
6187 ].join('') : 'none'
6188 });
6189 },
6190
6191 /**
6192 * Get the positioning correction for the span after rotating.
6193 */
6194 getSpanCorrection: function(width, baseline, alignCorrection, rotation, align) {
6195
6196 var costheta = rotation ? Math.cos(rotation * deg2rad) : 1,
6197 sintheta = rotation ? Math.sin(rotation * deg2rad) : 0,
6198 height = pick(this.elemHeight, this.element.offsetHeight),
6199 quad,
6200 nonLeft = align && align !== 'left';
6201
6202 // correct x and y
6203 this.xCorr = costheta < 0 && -width;
6204 this.yCorr = sintheta < 0 && -height;
6205
6206 // correct for baseline and corners spilling out after rotation
6207 quad = costheta * sintheta < 0;
6208 this.xCorr += sintheta * baseline * (quad ? 1 - alignCorrection : alignCorrection);
6209 this.yCorr -= costheta * baseline * (rotation ? (quad ? alignCorrection : 1 - alignCorrection) : 1);
6210 // correct for the length/height of the text
6211 if (nonLeft) {
6212 this.xCorr -= width * alignCorrection * (costheta < 0 ? -1 : 1);
6213 if (rotation) {
6214 this.yCorr -= height * alignCorrection * (sintheta < 0 ? -1 : 1);
6215 }
6216 css(this.element, {
6217 textAlign: align
6218 });
6219 }
6220 },
6221
6222 /**
6223 * Converts a subset of an SVG path definition to its VML counterpart. Takes an array
6224 * as the parameter and returns a string.
6225 */
6226 pathToVML: function(value) {
6227 // convert paths
6228 var i = value.length,
6229 path = [];
6230
6231 while (i--) {
6232
6233 // Multiply by 10 to allow subpixel precision.
6234 // Substracting half a pixel seems to make the coordinates
6235 // align with SVG, but this hasn't been tested thoroughly
6236 if (isNumber(value[i])) {
6237 path[i] = Math.round(value[i] * 10) - 5;
6238 } else if (value[i] === 'Z') { // close the path
6239 path[i] = 'x';
6240 } else {
6241 path[i] = value[i];
6242
6243 // When the start X and end X coordinates of an arc are too close,
6244 // they are rounded to the same value above. In this case, substract or
6245 // add 1 from the end X and Y positions. #186, #760, #1371, #1410.
6246 if (value.isArc && (value[i] === 'wa' || value[i] === 'at')) {
6247 // Start and end X
6248 if (path[i + 5] === path[i + 7]) {
6249 path[i + 7] += value[i + 7] > value[i + 5] ? 1 : -1;
6250 }
6251 // Start and end Y
6252 if (path[i + 6] === path[i + 8]) {
6253 path[i + 8] += value[i + 8] > value[i + 6] ? 1 : -1;
6254 }
6255 }
6256 }
6257 }
6258
6259
6260 // Loop up again to handle path shortcuts (#2132)
6261 /*while (i++ < path.length) {
6262 if (path[i] === 'H') { // horizontal line to
6263 path[i] = 'L';
6264 path.splice(i + 2, 0, path[i - 1]);
6265 } else if (path[i] === 'V') { // vertical line to
6266 path[i] = 'L';
6267 path.splice(i + 1, 0, path[i - 2]);
6268 }
6269 }*/
6270 return path.join(' ') || 'x';
6271 },
6272
6273 /**
6274 * Set the element's clipping to a predefined rectangle
6275 *
6276 * @param {String} id The id of the clip rectangle
6277 */
6278 clip: function(clipRect) {
6279 var wrapper = this,
6280 clipMembers,
6281 cssRet;
6282
6283 if (clipRect) {
6284 clipMembers = clipRect.members;
6285 erase(clipMembers, wrapper); // Ensure unique list of elements (#1258)
6286 clipMembers.push(wrapper);
6287 wrapper.destroyClip = function() {
6288 erase(clipMembers, wrapper);
6289 };
6290 cssRet = clipRect.getCSS(wrapper);
6291
6292 } else {
6293 if (wrapper.destroyClip) {
6294 wrapper.destroyClip();
6295 }
6296 cssRet = {
6297 clip: wrapper.docMode8 ? 'inherit' : 'rect(auto)'
6298 }; // #1214
6299 }
6300
6301 return wrapper.css(cssRet);
6302
6303 },
6304
6305 /**
6306 * Set styles for the element
6307 * @param {Object} styles
6308 */
6309 css: SVGElement.prototype.htmlCss,
6310
6311 /**
6312 * Removes a child either by removeChild or move to garbageBin.
6313 * Issue 490; in VML removeChild results in Orphaned nodes according to sIEve, discardElement does not.
6314 */
6315 safeRemoveChild: function(element) {
6316 // discardElement will detach the node from its parent before attaching it
6317 // to the garbage bin. Therefore it is important that the node is attached and have parent.
6318 if (element.parentNode) {
6319 discardElement(element);
6320 }
6321 },
6322
6323 /**
6324 * Extend element.destroy by removing it from the clip members array
6325 */
6326 destroy: function() {
6327 if (this.destroyClip) {
6328 this.destroyClip();
6329 }
6330
6331 return SVGElement.prototype.destroy.apply(this);
6332 },
6333
6334 /**
6335 * Add an event listener. VML override for normalizing event parameters.
6336 * @param {String} eventType
6337 * @param {Function} handler
6338 */
6339 on: function(eventType, handler) {
6340 // simplest possible event model for internal use
6341 this.element['on' + eventType] = function() {
6342 var evt = win.event;
6343 evt.target = evt.srcElement;
6344 handler(evt);
6345 };
6346 return this;
6347 },
6348
6349 /**
6350 * In stacked columns, cut off the shadows so that they don't overlap
6351 */
6352 cutOffPath: function(path, length) {
6353
6354 var len;
6355
6356 path = path.split(/[ ,]/); // The extra comma tricks the trailing comma remover in "gulp scripts" task
6357 len = path.length;
6358
6359 if (len === 9 || len === 11) {
6360 path[len - 4] = path[len - 2] = pInt(path[len - 2]) - 10 * length;
6361 }
6362 return path.join(' ');
6363 },
6364
6365 /**
6366 * Apply a drop shadow by copying elements and giving them different strokes
6367 * @param {Boolean|Object} shadowOptions
6368 */
6369 shadow: function(shadowOptions, group, cutOff) {
6370 var shadows = [],
6371 i,
6372 element = this.element,
6373 renderer = this.renderer,
6374 shadow,
6375 elemStyle = element.style,
6376 markup,
6377 path = element.path,
6378 strokeWidth,
6379 modifiedPath,
6380 shadowWidth,
6381 shadowElementOpacity;
6382
6383 // some times empty paths are not strings
6384 if (path && typeof path.value !== 'string') {
6385 path = 'x';
6386 }
6387 modifiedPath = path;
6388
6389 if (shadowOptions) {
6390 shadowWidth = pick(shadowOptions.width, 3);
6391 shadowElementOpacity = (shadowOptions.opacity || 0.15) / shadowWidth;
6392 for (i = 1; i <= 3; i++) {
6393
6394 strokeWidth = (shadowWidth * 2) + 1 - (2 * i);
6395
6396 // Cut off shadows for stacked column items
6397 if (cutOff) {
6398 modifiedPath = this.cutOffPath(path.value, strokeWidth + 0.5);
6399 }
6400
6401 markup = ['<shape isShadow="true" strokeweight="', strokeWidth,
6402 '" filled="false" path="', modifiedPath,
6403 '" coordsize="10 10" style="', element.style.cssText, '" />'
6404 ];
6405
6406 shadow = createElement(renderer.prepVML(markup),
6407 null, {
6408 left: pInt(elemStyle.left) + pick(shadowOptions.offsetX, 1),
6409 top: pInt(elemStyle.top) + pick(shadowOptions.offsetY, 1)
6410 }
6411 );
6412 if (cutOff) {
6413 shadow.cutOff = strokeWidth + 1;
6414 }
6415
6416 // apply the opacity
6417 markup = [
6418 '<stroke color="',
6419 shadowOptions.color || '#000000',
6420 '" opacity="', shadowElementOpacity * i, '"/>'
6421 ];
6422 createElement(renderer.prepVML(markup), null, null, shadow);
6423
6424
6425 // insert it
6426 if (group) {
6427 group.element.appendChild(shadow);
6428 } else {
6429 element.parentNode.insertBefore(shadow, element);
6430 }
6431
6432 // record it
6433 shadows.push(shadow);
6434
6435 }
6436
6437 this.shadows = shadows;
6438 }
6439 return this;
6440 },
6441 updateShadows: noop, // Used in SVG only
6442
6443 setAttr: function(key, value) {
6444 if (this.docMode8) { // IE8 setAttribute bug
6445 this.element[key] = value;
6446 } else {
6447 this.element.setAttribute(key, value);
6448 }
6449 },
6450 classSetter: function(value) {
6451 // IE8 Standards mode has problems retrieving the className unless set like this.
6452 // IE8 Standards can't set the class name before the element is appended.
6453 (this.added ? this.element : this).className = value;
6454 },
6455 dashstyleSetter: function(value, key, element) {
6456 var strokeElem = element.getElementsByTagName('stroke')[0] ||
6457 createElement(this.renderer.prepVML(['<stroke/>']), null, null, element);
6458 strokeElem[key] = value || 'solid';
6459 this[key] = value;
6460 /* because changing stroke-width will change the dash length
6461 and cause an epileptic effect */
6462 },
6463 dSetter: function(value, key, element) {
6464 var i,
6465 shadows = this.shadows;
6466 value = value || [];
6467 this.d = value.join && value.join(' '); // used in getter for animation
6468
6469 element.path = value = this.pathToVML(value);
6470
6471 // update shadows
6472 if (shadows) {
6473 i = shadows.length;
6474 while (i--) {
6475 shadows[i].path = shadows[i].cutOff ? this.cutOffPath(value, shadows[i].cutOff) : value;
6476 }
6477 }
6478 this.setAttr(key, value);
6479 },
6480 fillSetter: function(value, key, element) {
6481 var nodeName = element.nodeName;
6482 if (nodeName === 'SPAN') { // text color
6483 element.style.color = value;
6484 } else if (nodeName !== 'IMG') { // #1336
6485 element.filled = value !== 'none';
6486 this.setAttr('fillcolor', this.renderer.color(value, element, key, this));
6487 }
6488 },
6489 'fill-opacitySetter': function(value, key, element) {
6490 createElement(
6491 this.renderer.prepVML(['<', key.split('-')[0], ' opacity="', value, '"/>']),
6492 null,
6493 null,
6494 element
6495 );
6496 },
6497 opacitySetter: noop, // Don't bother - animation is too slow and filters introduce artifacts
6498 rotationSetter: function(value, key, element) {
6499 var style = element.style;
6500 this[key] = style[key] = value; // style is for #1873
6501
6502 // Correction for the 1x1 size of the shape container. Used in gauge needles.
6503 style.left = -Math.round(Math.sin(value * deg2rad) + 1) + 'px';
6504 style.top = Math.round(Math.cos(value * deg2rad)) + 'px';
6505 },
6506 strokeSetter: function(value, key, element) {
6507 this.setAttr('strokecolor', this.renderer.color(value, element, key, this));
6508 },
6509 'stroke-widthSetter': function(value, key, element) {
6510 element.stroked = !!value; // VML "stroked" attribute
6511 this[key] = value; // used in getter, issue #113
6512 if (isNumber(value)) {
6513 value += 'px';
6514 }
6515 this.setAttr('strokeweight', value);
6516 },
6517 titleSetter: function(value, key) {
6518 this.setAttr(key, value);
6519 },
6520 visibilitySetter: function(value, key, element) {
6521
6522 // Handle inherited visibility
6523 if (value === 'inherit') {
6524 value = 'visible';
6525 }
6526
6527 // Let the shadow follow the main element
6528 if (this.shadows) {
6529 each(this.shadows, function(shadow) {
6530 shadow.style[key] = value;
6531 });
6532 }
6533
6534 // Instead of toggling the visibility CSS property, move the div out of the viewport.
6535 // This works around #61 and #586
6536 if (element.nodeName === 'DIV') {
6537 value = value === 'hidden' ? '-999em' : 0;
6538
6539 // In order to redraw, IE7 needs the div to be visible when tucked away
6540 // outside the viewport. So the visibility is actually opposite of
6541 // the expected value. This applies to the tooltip only.
6542 if (!this.docMode8) {
6543 element.style[key] = value ? 'visible' : 'hidden';
6544 }
6545 key = 'top';
6546 }
6547 element.style[key] = value;
6548 },
6549 xSetter: function(value, key, element) {
6550 this[key] = value; // used in getter
6551
6552 if (key === 'x') {
6553 key = 'left';
6554 } else if (key === 'y') {
6555 key = 'top';
6556 }
6557 /* else {
6558 value = Math.max(0, value); // don't set width or height below zero (#311)
6559 }*/
6560
6561 // clipping rectangle special
6562 if (this.updateClipping) {
6563 this[key] = value; // the key is now 'left' or 'top' for 'x' and 'y'
6564 this.updateClipping();
6565 } else {
6566 // normal
6567 element.style[key] = value;
6568 }
6569 },
6570 zIndexSetter: function(value, key, element) {
6571 element.style[key] = value;
6572 }
6573 };
6574 VMLElement['stroke-opacitySetter'] = VMLElement['fill-opacitySetter'];
6575 H.VMLElement = VMLElement = extendClass(SVGElement, VMLElement);
6576
6577 // Some shared setters
6578 VMLElement.prototype.ySetter =
6579 VMLElement.prototype.widthSetter =
6580 VMLElement.prototype.heightSetter =
6581 VMLElement.prototype.xSetter;
6582
6583
6584 /**
6585 * The VML renderer
6586 */
6587 VMLRendererExtension = { // inherit SVGRenderer
6588
6589 Element: VMLElement,
6590 isIE8: win.navigator.userAgent.indexOf('MSIE 8.0') > -1,
6591
6592
6593 /**
6594 * Initialize the VMLRenderer
6595 * @param {Object} container
6596 * @param {Number} width
6597 * @param {Number} height
6598 */
6599 init: function(container, width, height) {
6600 var renderer = this,
6601 boxWrapper,
6602 box,
6603 css;
6604
6605 renderer.alignedObjects = [];
6606
6607 boxWrapper = renderer.createElement('div')
6608 .css({
6609 position: 'relative'
6610 });
6611 box = boxWrapper.element;
6612 container.appendChild(boxWrapper.element);
6613
6614
6615 // generate the containing box
6616 renderer.isVML = true;
6617 renderer.box = box;
6618 renderer.boxWrapper = boxWrapper;
6619 renderer.gradients = {};
6620 renderer.cache = {}; // Cache for numerical bounding boxes
6621 renderer.cacheKeys = [];
6622 renderer.imgCount = 0;
6623
6624
6625 renderer.setSize(width, height, false);
6626
6627 // The only way to make IE6 and IE7 print is to use a global namespace. However,
6628 // with IE8 the only way to make the dynamic shapes visible in screen and print mode
6629 // seems to be to add the xmlns attribute and the behaviour style inline.
6630 if (!doc.namespaces.hcv) {
6631
6632 doc.namespaces.add('hcv', 'urn:schemas-microsoft-com:vml');
6633
6634 // Setup default CSS (#2153, #2368, #2384)
6635 css = 'hcv\\:fill, hcv\\:path, hcv\\:shape, hcv\\:stroke' +
6636 '{ behavior:url(#default#VML); display: inline-block; } ';
6637 try {
6638 doc.createStyleSheet().cssText = css;
6639 } catch (e) {
6640 doc.styleSheets[0].cssText += css;
6641 }
6642
6643 }
6644 },
6645
6646
6647 /**
6648 * Detect whether the renderer is hidden. This happens when one of the parent elements
6649 * has display: none
6650 */
6651 isHidden: function() {
6652 return !this.box.offsetWidth;
6653 },
6654
6655 /**
6656 * Define a clipping rectangle. In VML it is accomplished by storing the values
6657 * for setting the CSS style to all associated members.
6658 *
6659 * @param {Number} x
6660 * @param {Number} y
6661 * @param {Number} width
6662 * @param {Number} height
6663 */
6664 clipRect: function(x, y, width, height) {
6665
6666 // create a dummy element
6667 var clipRect = this.createElement(),
6668 isObj = isObject(x);
6669
6670 // mimic a rectangle with its style object for automatic updating in attr
6671 return extend(clipRect, {
6672 members: [],
6673 count: 0,
6674 left: (isObj ? x.x : x) + 1,
6675 top: (isObj ? x.y : y) + 1,
6676 width: (isObj ? x.width : width) - 1,
6677 height: (isObj ? x.height : height) - 1,
6678 getCSS: function(wrapper) {
6679 var element = wrapper.element,
6680 nodeName = element.nodeName,
6681 isShape = nodeName === 'shape',
6682 inverted = wrapper.inverted,
6683 rect = this,
6684 top = rect.top - (isShape ? element.offsetTop : 0),
6685 left = rect.left,
6686 right = left + rect.width,
6687 bottom = top + rect.height,
6688 ret = {
6689 clip: 'rect(' +
6690 Math.round(inverted ? left : top) + 'px,' +
6691 Math.round(inverted ? bottom : right) + 'px,' +
6692 Math.round(inverted ? right : bottom) + 'px,' +
6693 Math.round(inverted ? top : left) + 'px)'
6694 };
6695
6696 // issue 74 workaround
6697 if (!inverted && wrapper.docMode8 && nodeName === 'DIV') {
6698 extend(ret, {
6699 width: right + 'px',
6700 height: bottom + 'px'
6701 });
6702 }
6703 return ret;
6704 },
6705
6706 // used in attr and animation to update the clipping of all members
6707 updateClipping: function() {
6708 each(clipRect.members, function(member) {
6709 // Member.element is falsy on deleted series, like in
6710 // stock/members/series-remove demo. Should be removed
6711 // from members, but this will do.
6712 if (member.element) {
6713 member.css(clipRect.getCSS(member));
6714 }
6715 });
6716 }
6717 });
6718
6719 },
6720
6721
6722 /**
6723 * Take a color and return it if it's a string, make it a gradient if it's a
6724 * gradient configuration object, and apply opacity.
6725 *
6726 * @param {Object} color The color or config object
6727 */
6728 color: function(color, elem, prop, wrapper) {
6729 var renderer = this,
6730 colorObject,
6731 regexRgba = /^rgba/,
6732 markup,
6733 fillType,
6734 ret = 'none';
6735
6736 // Check for linear or radial gradient
6737 if (color && color.linearGradient) {
6738 fillType = 'gradient';
6739 } else if (color && color.radialGradient) {
6740 fillType = 'pattern';
6741 }
6742
6743
6744 if (fillType) {
6745
6746 var stopColor,
6747 stopOpacity,
6748 gradient = color.linearGradient || color.radialGradient,
6749 x1,
6750 y1,
6751 x2,
6752 y2,
6753 opacity1,
6754 opacity2,
6755 color1,
6756 color2,
6757 fillAttr = '',
6758 stops = color.stops,
6759 firstStop,
6760 lastStop,
6761 colors = [],
6762 addFillNode = function() {
6763 // Add the fill subnode. When colors attribute is used, the meanings of opacity and o:opacity2
6764 // are reversed.
6765 markup = ['<fill colors="' + colors.join(',') +
6766 '" opacity="', opacity2, '" o:opacity2="',
6767 opacity1, '" type="', fillType, '" ', fillAttr,
6768 'focus="100%" method="any" />'
6769 ];
6770 createElement(renderer.prepVML(markup), null, null, elem);
6771 };
6772
6773 // Extend from 0 to 1
6774 firstStop = stops[0];
6775 lastStop = stops[stops.length - 1];
6776 if (firstStop[0] > 0) {
6777 stops.unshift([
6778 0,
6779 firstStop[1]
6780 ]);
6781 }
6782 if (lastStop[0] < 1) {
6783 stops.push([
6784 1,
6785 lastStop[1]
6786 ]);
6787 }
6788
6789 // Compute the stops
6790 each(stops, function(stop, i) {
6791 if (regexRgba.test(stop[1])) {
6792 colorObject = H.color(stop[1]);
6793 stopColor = colorObject.get('rgb');
6794 stopOpacity = colorObject.get('a');
6795 } else {
6796 stopColor = stop[1];
6797 stopOpacity = 1;
6798 }
6799
6800 // Build the color attribute
6801 colors.push((stop[0] * 100) + '% ' + stopColor);
6802
6803 // Only start and end opacities are allowed, so we use the first and the last
6804 if (!i) {
6805 opacity1 = stopOpacity;
6806 color2 = stopColor;
6807 } else {
6808 opacity2 = stopOpacity;
6809 color1 = stopColor;
6810 }
6811 });
6812
6813 // Apply the gradient to fills only.
6814 if (prop === 'fill') {
6815
6816 // Handle linear gradient angle
6817 if (fillType === 'gradient') {
6818 x1 = gradient.x1 || gradient[0] || 0;
6819 y1 = gradient.y1 || gradient[1] || 0;
6820 x2 = gradient.x2 || gradient[2] || 0;
6821 y2 = gradient.y2 || gradient[3] || 0;
6822 fillAttr = 'angle="' + (90 - Math.atan(
6823 (y2 - y1) / // y vector
6824 (x2 - x1) // x vector
6825 ) * 180 / Math.PI) + '"';
6826
6827 addFillNode();
6828
6829 // Radial (circular) gradient
6830 } else {
6831
6832 var r = gradient.r,
6833 sizex = r * 2,
6834 sizey = r * 2,
6835 cx = gradient.cx,
6836 cy = gradient.cy,
6837 radialReference = elem.radialReference,
6838 bBox,
6839 applyRadialGradient = function() {
6840 if (radialReference) {
6841 bBox = wrapper.getBBox();
6842 cx += (radialReference[0] - bBox.x) / bBox.width - 0.5;
6843 cy += (radialReference[1] - bBox.y) / bBox.height - 0.5;
6844 sizex *= radialReference[2] / bBox.width;
6845 sizey *= radialReference[2] / bBox.height;
6846 }
6847 fillAttr = 'src="' + H.getOptions().global.VMLRadialGradientURL + '" ' +
6848 'size="' + sizex + ',' + sizey + '" ' +
6849 'origin="0.5,0.5" ' +
6850 'position="' + cx + ',' + cy + '" ' +
6851 'color2="' + color2 + '" ';
6852
6853 addFillNode();
6854 };
6855
6856 // Apply radial gradient
6857 if (wrapper.added) {
6858 applyRadialGradient();
6859 } else {
6860 // We need to know the bounding box to get the size and position right
6861 wrapper.onAdd = applyRadialGradient;
6862 }
6863
6864 // The fill element's color attribute is broken in IE8 standards mode, so we
6865 // need to set the parent shape's fillcolor attribute instead.
6866 ret = color1;
6867 }
6868
6869 // Gradients are not supported for VML stroke, return the first color. #722.
6870 } else {
6871 ret = stopColor;
6872 }
6873
6874 // If the color is an rgba color, split it and add a fill node
6875 // to hold the opacity component
6876 } else if (regexRgba.test(color) && elem.tagName !== 'IMG') {
6877
6878 colorObject = H.color(color);
6879
6880 wrapper[prop + '-opacitySetter'](colorObject.get('a'), prop, elem);
6881
6882 ret = colorObject.get('rgb');
6883
6884
6885 } else {
6886 var propNodes = elem.getElementsByTagName(prop); // 'stroke' or 'fill' node
6887 if (propNodes.length) {
6888 propNodes[0].opacity = 1;
6889 propNodes[0].type = 'solid';
6890 }
6891 ret = color;
6892 }
6893
6894 return ret;
6895 },
6896
6897 /**
6898 * Take a VML string and prepare it for either IE8 or IE6/IE7.
6899 * @param {Array} markup A string array of the VML markup to prepare
6900 */
6901 prepVML: function(markup) {
6902 var vmlStyle = 'display:inline-block;behavior:url(#default#VML);',
6903 isIE8 = this.isIE8;
6904
6905 markup = markup.join('');
6906
6907 if (isIE8) { // add xmlns and style inline
6908 markup = markup.replace('/>', ' xmlns="urn:schemas-microsoft-com:vml" />');
6909 if (markup.indexOf('style="') === -1) {
6910 markup = markup.replace('/>', ' style="' + vmlStyle + '" />');
6911 } else {
6912 markup = markup.replace('style="', 'style="' + vmlStyle);
6913 }
6914
6915 } else { // add namespace
6916 markup = markup.replace('<', '<hcv:');
6917 }
6918
6919 return markup;
6920 },
6921
6922 /**
6923 * Create rotated and aligned text
6924 * @param {String} str
6925 * @param {Number} x
6926 * @param {Number} y
6927 */
6928 text: SVGRenderer.prototype.html,
6929
6930 /**
6931 * Create and return a path element
6932 * @param {Array} path
6933 */
6934 path: function(path) {
6935 var attr = {
6936 // subpixel precision down to 0.1 (width and height = 1px)
6937 coordsize: '10 10'
6938 };
6939 if (isArray(path)) {
6940 attr.d = path;
6941 } else if (isObject(path)) { // attributes
6942 extend(attr, path);
6943 }
6944 // create the shape
6945 return this.createElement('shape').attr(attr);
6946 },
6947
6948 /**
6949 * Create and return a circle element. In VML circles are implemented as
6950 * shapes, which is faster than v:oval
6951 * @param {Number} x
6952 * @param {Number} y
6953 * @param {Number} r
6954 */
6955 circle: function(x, y, r) {
6956 var circle = this.symbol('circle');
6957 if (isObject(x)) {
6958 r = x.r;
6959 y = x.y;
6960 x = x.x;
6961 }
6962 circle.isCircle = true; // Causes x and y to mean center (#1682)
6963 circle.r = r;
6964 return circle.attr({
6965 x: x,
6966 y: y
6967 });
6968 },
6969
6970 /**
6971 * Create a group using an outer div and an inner v:group to allow rotating
6972 * and flipping. A simple v:group would have problems with positioning
6973 * child HTML elements and CSS clip.
6974 *
6975 * @param {String} name The name of the group
6976 */
6977 g: function(name) {
6978 var wrapper,
6979 attribs;
6980
6981 // set the class name
6982 if (name) {
6983 attribs = {
6984 'className': 'highcharts-' + name,
6985 'class': 'highcharts-' + name
6986 };
6987 }
6988
6989 // the div to hold HTML and clipping
6990 wrapper = this.createElement('div').attr(attribs);
6991
6992 return wrapper;
6993 },
6994
6995 /**
6996 * VML override to create a regular HTML image
6997 * @param {String} src
6998 * @param {Number} x
6999 * @param {Number} y
7000 * @param {Number} width
7001 * @param {Number} height
7002 */
7003 image: function(src, x, y, width, height) {
7004 var obj = this.createElement('img')
7005 .attr({
7006 src: src
7007 });
7008
7009 if (arguments.length > 1) {
7010 obj.attr({
7011 x: x,
7012 y: y,
7013 width: width,
7014 height: height
7015 });
7016 }
7017 return obj;
7018 },
7019
7020 /**
7021 * For rectangles, VML uses a shape for rect to overcome bugs and rotation problems
7022 */
7023 createElement: function(nodeName) {
7024 return nodeName === 'rect' ?
7025 this.symbol(nodeName) :
7026 SVGRenderer.prototype.createElement.call(this, nodeName);
7027 },
7028
7029 /**
7030 * In the VML renderer, each child of an inverted div (group) is inverted
7031 * @param {Object} element
7032 * @param {Object} parentNode
7033 */
7034 invertChild: function(element, parentNode) {
7035 var ren = this,
7036 parentStyle = parentNode.style,
7037 imgStyle = element.tagName === 'IMG' && element.style; // #1111
7038
7039 css(element, {
7040 flip: 'x',
7041 left: pInt(parentStyle.width) - (imgStyle ? pInt(imgStyle.top) : 1),
7042 top: pInt(parentStyle.height) - (imgStyle ? pInt(imgStyle.left) : 1),
7043 rotation: -90
7044 });
7045
7046 // Recursively invert child elements, needed for nested composite
7047 // shapes like box plots and error bars. #1680, #1806.
7048 each(element.childNodes, function(child) {
7049 ren.invertChild(child, element);
7050 });
7051 },
7052
7053 /**
7054 * Symbol definitions that override the parent SVG renderer's symbols
7055 *
7056 */
7057 symbols: {
7058 // VML specific arc function
7059 arc: function(x, y, w, h, options) {
7060 var start = options.start,
7061 end = options.end,
7062 radius = options.r || w || h,
7063 innerRadius = options.innerR,
7064 cosStart = Math.cos(start),
7065 sinStart = Math.sin(start),
7066 cosEnd = Math.cos(end),
7067 sinEnd = Math.sin(end),
7068 ret;
7069
7070 if (end - start === 0) { // no angle, don't show it.
7071 return ['x'];
7072 }
7073
7074 ret = [
7075 'wa', // clockwise arc to
7076 x - radius, // left
7077 y - radius, // top
7078 x + radius, // right
7079 y + radius, // bottom
7080 x + radius * cosStart, // start x
7081 y + radius * sinStart, // start y
7082 x + radius * cosEnd, // end x
7083 y + radius * sinEnd // end y
7084 ];
7085
7086 if (options.open && !innerRadius) {
7087 ret.push(
7088 'e',
7089 'M',
7090 x, // - innerRadius,
7091 y // - innerRadius
7092 );
7093 }
7094
7095 ret.push(
7096 'at', // anti clockwise arc to
7097 x - innerRadius, // left
7098 y - innerRadius, // top
7099 x + innerRadius, // right
7100 y + innerRadius, // bottom
7101 x + innerRadius * cosEnd, // start x
7102 y + innerRadius * sinEnd, // start y
7103 x + innerRadius * cosStart, // end x
7104 y + innerRadius * sinStart, // end y
7105 'x', // finish path
7106 'e' // close
7107 );
7108
7109 ret.isArc = true;
7110 return ret;
7111
7112 },
7113 // Add circle symbol path. This performs significantly faster than v:oval.
7114 circle: function(x, y, w, h, wrapper) {
7115
7116 if (wrapper && defined(wrapper.r)) {
7117 w = h = 2 * wrapper.r;
7118 }
7119
7120 // Center correction, #1682
7121 if (wrapper && wrapper.isCircle) {
7122 x -= w / 2;
7123 y -= h / 2;
7124 }
7125
7126 // Return the path
7127 return [
7128 'wa', // clockwisearcto
7129 x, // left
7130 y, // top
7131 x + w, // right
7132 y + h, // bottom
7133 x + w, // start x
7134 y + h / 2, // start y
7135 x + w, // end x
7136 y + h / 2, // end y
7137 //'x', // finish path
7138 'e' // close
7139 ];
7140 },
7141 /**
7142 * Add rectangle symbol path which eases rotation and omits arcsize problems
7143 * compared to the built-in VML roundrect shape. When borders are not rounded,
7144 * use the simpler square path, else use the callout path without the arrow.
7145 */
7146 rect: function(x, y, w, h, options) {
7147 return SVGRenderer.prototype.symbols[!defined(options) || !options.r ? 'square' : 'callout'].call(0, x, y, w, h, options);
7148 }
7149 }
7150 };
7151 H.VMLRenderer = VMLRenderer = function() {
7152 this.init.apply(this, arguments);
7153 };
7154 VMLRenderer.prototype = merge(SVGRenderer.prototype, VMLRendererExtension);
7155
7156 // general renderer
7157 H.Renderer = VMLRenderer;
7158 }
7159
7160 // This method is used with exporting in old IE, when emulating SVG (see #2314)
7161 SVGRenderer.prototype.measureSpanWidth = function(text, styles) {
7162 var measuringSpan = doc.createElement('span'),
7163 offsetWidth,
7164 textNode = doc.createTextNode(text);
7165
7166 measuringSpan.appendChild(textNode);
7167 css(measuringSpan, styles);
7168 this.box.appendChild(measuringSpan);
7169 offsetWidth = measuringSpan.offsetWidth;
7170 discardElement(measuringSpan); // #2463
7171 return offsetWidth;
7172 };
7173
7174
7175 /* ****************************************************************************
7176 * *
7177 * END OF INTERNET EXPLORER <= 8 SPECIFIC CODE *
7178 * *
7179 *****************************************************************************/
7180
7181
7182 }(Highcharts));
7183 (function(H) {
7184 /**
7185 * (c) 2010-2016 Torstein Honsi
7186 *
7187 * License: www.highcharts.com/license
7188 */
7189 'use strict';
7190 var color = H.color,
7191 each = H.each,
7192 getTZOffset = H.getTZOffset,
7193 isTouchDevice = H.isTouchDevice,
7194 merge = H.merge,
7195 pick = H.pick,
7196 svg = H.svg,
7197 win = H.win;
7198
7199 /* ****************************************************************************
7200 * Handle the options *
7201 *****************************************************************************/
7202 H.defaultOptions = {
7203
7204 colors: '#7cb5ec #434348 #90ed7d #f7a35c #8085e9 #f15c80 #e4d354 #2b908f #f45b5b #91e8e1'.split(' '),
7205
7206 symbols: ['circle', 'diamond', 'square', 'triangle', 'triangle-down'],
7207 lang: {
7208 loading: 'Loading...',
7209 months: ['January', 'February', 'March', 'April', 'May', 'June', 'July',
7210 'August', 'September', 'October', 'November', 'December'
7211 ],
7212 shortMonths: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
7213 weekdays: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
7214 // invalidDate: '',
7215 decimalPoint: '.',
7216 numericSymbols: ['k', 'M', 'G', 'T', 'P', 'E'], // SI prefixes used in axis labels
7217 resetZoom: 'Reset zoom',
7218 resetZoomTitle: 'Reset zoom level 1:1',
7219 thousandsSep: ' '
7220 },
7221 global: {
7222 useUTC: true,
7223 //timezoneOffset: 0,
7224
7225 VMLRadialGradientURL: 'http://code.highcharts.com/5.0.5/gfx/vml-radial-gradient.png'
7226
7227 },
7228 chart: {
7229 //animation: true,
7230 //alignTicks: false,
7231 //reflow: true,
7232 //className: null,
7233 //events: { load, selection },
7234 //margin: [null],
7235 //marginTop: null,
7236 //marginRight: null,
7237 //marginBottom: null,
7238 //marginLeft: null,
7239 borderRadius: 0,
7240
7241 defaultSeriesType: 'line',
7242 ignoreHiddenSeries: true,
7243 //inverted: false,
7244 spacing: [10, 10, 15, 10],
7245 //spacingTop: 10,
7246 //spacingRight: 10,
7247 //spacingBottom: 15,
7248 //spacingLeft: 10,
7249 //zoomType: ''
7250 resetZoomButton: {
7251 theme: {
7252 zIndex: 20
7253 },
7254 position: {
7255 align: 'right',
7256 x: -10,
7257 //verticalAlign: 'top',
7258 y: 10
7259 }
7260 // relativeTo: 'plot'
7261 },
7262 width: null,
7263 height: null,
7264
7265
7266 borderColor: '#335cad',
7267 //borderWidth: 0,
7268 //style: {
7269 // fontFamily: '"Lucida Grande", "Lucida Sans Unicode", Verdana, Arial, Helvetica, sans-serif', // default font
7270 // fontSize: '12px'
7271 //},
7272 backgroundColor: '#ffffff',
7273 //plotBackgroundColor: null,
7274 plotBorderColor: '#cccccc'
7275 //plotBorderWidth: 0,
7276 //plotShadow: false
7277
7278 },
7279 title: {
7280 text: 'Chart title',
7281 align: 'center',
7282 // floating: false,
7283 margin: 15,
7284 // x: 0,
7285 // verticalAlign: 'top',
7286 // y: null,
7287 // style: {}, // defined inline
7288 widthAdjust: -44
7289
7290 },
7291 subtitle: {
7292 text: '',
7293 align: 'center',
7294 // floating: false
7295 // x: 0,
7296 // verticalAlign: 'top',
7297 // y: null,
7298 // style: {}, // defined inline
7299 widthAdjust: -44
7300 },
7301
7302 plotOptions: {},
7303 labels: {
7304 //items: [],
7305 style: {
7306 //font: defaultFont,
7307 position: 'absolute',
7308 color: '#333333'
7309 }
7310 },
7311 legend: {
7312 enabled: true,
7313 align: 'center',
7314 //floating: false,
7315 layout: 'horizontal',
7316 labelFormatter: function() {
7317 return this.name;
7318 },
7319 //borderWidth: 0,
7320 borderColor: '#999999',
7321 borderRadius: 0,
7322 navigation: {
7323
7324 activeColor: '#003399',
7325 inactiveColor: '#cccccc'
7326
7327 // animation: true,
7328 // arrowSize: 12
7329 // style: {} // text styles
7330 },
7331 // margin: 20,
7332 // reversed: false,
7333 // backgroundColor: null,
7334 /*style: {
7335 padding: '5px'
7336 },*/
7337
7338 itemStyle: {
7339 color: '#333333',
7340 fontSize: '12px',
7341 fontWeight: 'bold'
7342 },
7343 itemHoverStyle: {
7344 //cursor: 'pointer', removed as of #601
7345 color: '#000000'
7346 },
7347 itemHiddenStyle: {
7348 color: '#cccccc'
7349 },
7350 shadow: false,
7351
7352 itemCheckboxStyle: {
7353 position: 'absolute',
7354 width: '13px', // for IE precision
7355 height: '13px'
7356 },
7357 // itemWidth: undefined,
7358 squareSymbol: true,
7359 // symbolRadius: 0,
7360 // symbolWidth: 16,
7361 symbolPadding: 5,
7362 verticalAlign: 'bottom',
7363 // width: undefined,
7364 x: 0,
7365 y: 0,
7366 title: {
7367 //text: null,
7368
7369 style: {
7370 fontWeight: 'bold'
7371 }
7372
7373 }
7374 },
7375
7376 loading: {
7377 // hideDuration: 100,
7378 // showDuration: 0,
7379
7380 labelStyle: {
7381 fontWeight: 'bold',
7382 position: 'relative',
7383 top: '45%'
7384 },
7385 style: {
7386 position: 'absolute',
7387 backgroundColor: '#ffffff',
7388 opacity: 0.5,
7389 textAlign: 'center'
7390 }
7391
7392 },
7393
7394 tooltip: {
7395 enabled: true,
7396 animation: svg,
7397 //crosshairs: null,
7398 borderRadius: 3,
7399 dateTimeLabelFormats: {
7400 millisecond: '%A, %b %e, %H:%M:%S.%L',
7401 second: '%A, %b %e, %H:%M:%S',
7402 minute: '%A, %b %e, %H:%M',
7403 hour: '%A, %b %e, %H:%M',
7404 day: '%A, %b %e, %Y',
7405 week: 'Week from %A, %b %e, %Y',
7406 month: '%B %Y',
7407 year: '%Y'
7408 },
7409 footerFormat: '',
7410 //formatter: defaultFormatter,
7411 /* todo: em font-size when finished comparing against HC4
7412 headerFormat: '<span style="font-size: 0.85em">{point.key}</span><br/>',
7413 */
7414 padding: 8,
7415
7416 //shape: 'callout',
7417 //shared: false,
7418 snap: isTouchDevice ? 25 : 10,
7419
7420 backgroundColor: color('#f7f7f7').setOpacity(0.85).get(),
7421 borderWidth: 1,
7422 headerFormat: '<span style="font-size: 10px">{point.key}</span><br/>',
7423 pointFormat: '<span style="color:{point.color}">\u25CF</span> {series.name}: <b>{point.y}</b><br/>',
7424 shadow: true,
7425 style: {
7426 color: '#333333',
7427 cursor: 'default',
7428 fontSize: '12px',
7429 pointerEvents: 'none', // #1686 http://caniuse.com/#feat=pointer-events
7430 whiteSpace: 'nowrap'
7431 }
7432
7433 //xDateFormat: '%A, %b %e, %Y',
7434 //valueDecimals: null,
7435 //valuePrefix: '',
7436 //valueSuffix: ''
7437 },
7438
7439 credits: {
7440 enabled: true,
7441 href: 'http://www.highcharts.com',
7442 position: {
7443 align: 'right',
7444 x: -10,
7445 verticalAlign: 'bottom',
7446 y: -5
7447 },
7448
7449 style: {
7450 cursor: 'pointer',
7451 color: '#999999',
7452 fontSize: '9px'
7453 },
7454
7455 text: 'Highcharts.com'
7456 }
7457 };
7458
7459
7460
7461 /**
7462 * Set the time methods globally based on the useUTC option. Time method can be
7463 * either local time or UTC (default). It is called internally on initiating
7464 * Highcharts and after running `Highcharts.setOptions`.
7465 *
7466 * @private
7467 */
7468 function setTimeMethods() {
7469 var globalOptions = H.defaultOptions.global,
7470 Date,
7471 useUTC = globalOptions.useUTC,
7472 GET = useUTC ? 'getUTC' : 'get',
7473 SET = useUTC ? 'setUTC' : 'set';
7474
7475 H.Date = Date = globalOptions.Date || win.Date; // Allow using a different Date class
7476 Date.hcTimezoneOffset = useUTC && globalOptions.timezoneOffset;
7477 Date.hcGetTimezoneOffset = useUTC && globalOptions.getTimezoneOffset;
7478 Date.hcMakeTime = function(year, month, date, hours, minutes, seconds) {
7479 var d;
7480 if (useUTC) {
7481 d = Date.UTC.apply(0, arguments);
7482 d += getTZOffset(d);
7483 } else {
7484 d = new Date(
7485 year,
7486 month,
7487 pick(date, 1),
7488 pick(hours, 0),
7489 pick(minutes, 0),
7490 pick(seconds, 0)
7491 ).getTime();
7492 }
7493 return d;
7494 };
7495 each(['Minutes', 'Hours', 'Day', 'Date', 'Month', 'FullYear'], function(s) {
7496 Date['hcGet' + s] = GET + s;
7497 });
7498 each(['Milliseconds', 'Seconds', 'Minutes', 'Hours', 'Date', 'Month', 'FullYear'], function(s) {
7499 Date['hcSet' + s] = SET + s;
7500 });
7501 }
7502
7503 /**
7504 * Merge the default options with custom options and return the new options structure
7505 * @param {Object} options The new custom options
7506 */
7507 H.setOptions = function(options) {
7508
7509 // Copy in the default options
7510 H.defaultOptions = merge(true, H.defaultOptions, options);
7511
7512 // Apply UTC
7513 setTimeMethods();
7514
7515 return H.defaultOptions;
7516 };
7517
7518 /**
7519 * Get the updated default options. Until 3.0.7, merely exposing defaultOptions for outside modules
7520 * wasn't enough because the setOptions method created a new object.
7521 */
7522 H.getOptions = function() {
7523 return H.defaultOptions;
7524 };
7525
7526
7527 // Series defaults
7528 H.defaultPlotOptions = H.defaultOptions.plotOptions;
7529
7530 // set the default time methods
7531 setTimeMethods();
7532
7533 }(Highcharts));
7534 (function(H) {
7535 /**
7536 * (c) 2010-2016 Torstein Honsi
7537 *
7538 * License: www.highcharts.com/license
7539 */
7540 'use strict';
7541 var arrayMax = H.arrayMax,
7542 arrayMin = H.arrayMin,
7543 defined = H.defined,
7544 destroyObjectProperties = H.destroyObjectProperties,
7545 each = H.each,
7546 erase = H.erase,
7547 merge = H.merge,
7548 pick = H.pick;
7549 /*
7550 * The object wrapper for plot lines and plot bands
7551 * @param {Object} options
7552 */
7553 H.PlotLineOrBand = function(axis, options) {
7554 this.axis = axis;
7555
7556 if (options) {
7557 this.options = options;
7558 this.id = options.id;
7559 }
7560 };
7561
7562 H.PlotLineOrBand.prototype = {
7563
7564 /**
7565 * Render the plot line or plot band. If it is already existing,
7566 * move it.
7567 */
7568 render: function() {
7569 var plotLine = this,
7570 axis = plotLine.axis,
7571 horiz = axis.horiz,
7572 options = plotLine.options,
7573 optionsLabel = options.label,
7574 label = plotLine.label,
7575 to = options.to,
7576 from = options.from,
7577 value = options.value,
7578 isBand = defined(from) && defined(to),
7579 isLine = defined(value),
7580 svgElem = plotLine.svgElem,
7581 isNew = !svgElem,
7582 path = [],
7583 addEvent,
7584 eventType,
7585 color = options.color,
7586 zIndex = pick(options.zIndex, 0),
7587 events = options.events,
7588 attribs = {
7589 'class': 'highcharts-plot-' + (isBand ? 'band ' : 'line ') + (options.className || '')
7590 },
7591 groupAttribs = {},
7592 renderer = axis.chart.renderer,
7593 groupName = isBand ? 'bands' : 'lines',
7594 group,
7595 log2lin = axis.log2lin;
7596
7597 // logarithmic conversion
7598 if (axis.isLog) {
7599 from = log2lin(from);
7600 to = log2lin(to);
7601 value = log2lin(value);
7602 }
7603
7604
7605 // Set the presentational attributes
7606 if (isLine) {
7607 attribs = {
7608 stroke: color,
7609 'stroke-width': options.width
7610 };
7611 if (options.dashStyle) {
7612 attribs.dashstyle = options.dashStyle;
7613 }
7614
7615 } else if (isBand) { // plot band
7616 if (color) {
7617 attribs.fill = color;
7618 }
7619 if (options.borderWidth) {
7620 attribs.stroke = options.borderColor;
7621 attribs['stroke-width'] = options.borderWidth;
7622 }
7623 }
7624
7625
7626 // Grouping and zIndex
7627 groupAttribs.zIndex = zIndex;
7628 groupName += '-' + zIndex;
7629
7630 group = axis[groupName];
7631 if (!group) {
7632 axis[groupName] = group = renderer.g('plot-' + groupName)
7633 .attr(groupAttribs).add();
7634 }
7635
7636 // Create the path
7637 if (isNew) {
7638 plotLine.svgElem = svgElem =
7639 renderer
7640 .path()
7641 .attr(attribs).add(group);
7642 }
7643
7644
7645 // Set the path or return
7646 if (isLine) {
7647 path = axis.getPlotLinePath(value, svgElem.strokeWidth());
7648 } else if (isBand) { // plot band
7649 path = axis.getPlotBandPath(from, to, options);
7650 } else {
7651 return;
7652 }
7653
7654 // common for lines and bands
7655 if (isNew && path && path.length) {
7656 svgElem.attr({
7657 d: path
7658 });
7659
7660 // events
7661 if (events) {
7662 addEvent = function(eventType) {
7663 svgElem.on(eventType, function(e) {
7664 events[eventType].apply(plotLine, [e]);
7665 });
7666 };
7667 for (eventType in events) {
7668 addEvent(eventType);
7669 }
7670 }
7671 } else if (svgElem) {
7672 if (path) {
7673 svgElem.show();
7674 svgElem.animate({
7675 d: path
7676 });
7677 } else {
7678 svgElem.hide();
7679 if (label) {
7680 plotLine.label = label = label.destroy();
7681 }
7682 }
7683 }
7684
7685 // the plot band/line label
7686 if (optionsLabel && defined(optionsLabel.text) && path && path.length &&
7687 axis.width > 0 && axis.height > 0 && !path.flat) {
7688 // apply defaults
7689 optionsLabel = merge({
7690 align: horiz && isBand && 'center',
7691 x: horiz ? !isBand && 4 : 10,
7692 verticalAlign: !horiz && isBand && 'middle',
7693 y: horiz ? isBand ? 16 : 10 : isBand ? 6 : -4,
7694 rotation: horiz && !isBand && 90
7695 }, optionsLabel);
7696
7697 this.renderLabel(optionsLabel, path, isBand, zIndex);
7698
7699 } else if (label) { // move out of sight
7700 label.hide();
7701 }
7702
7703 // chainable
7704 return plotLine;
7705 },
7706
7707 /**
7708 * Render and align label for plot line or band.
7709 */
7710 renderLabel: function(optionsLabel, path, isBand, zIndex) {
7711 var plotLine = this,
7712 label = plotLine.label,
7713 renderer = plotLine.axis.chart.renderer,
7714 attribs,
7715 xs,
7716 ys,
7717 x,
7718 y;
7719
7720 // add the SVG element
7721 if (!label) {
7722 attribs = {
7723 align: optionsLabel.textAlign || optionsLabel.align,
7724 rotation: optionsLabel.rotation,
7725 'class': 'highcharts-plot-' + (isBand ? 'band' : 'line') + '-label ' + (optionsLabel.className || '')
7726 };
7727
7728 attribs.zIndex = zIndex;
7729
7730 plotLine.label = label = renderer.text(
7731 optionsLabel.text,
7732 0,
7733 0,
7734 optionsLabel.useHTML
7735 )
7736 .attr(attribs)
7737 .add();
7738
7739
7740 label.css(optionsLabel.style);
7741
7742 }
7743
7744 // get the bounding box and align the label
7745 // #3000 changed to better handle choice between plotband or plotline
7746 xs = [path[1], path[4], (isBand ? path[6] : path[1])];
7747 ys = [path[2], path[5], (isBand ? path[7] : path[2])];
7748 x = arrayMin(xs);
7749 y = arrayMin(ys);
7750
7751 label.align(optionsLabel, false, {
7752 x: x,
7753 y: y,
7754 width: arrayMax(xs) - x,
7755 height: arrayMax(ys) - y
7756 });
7757 label.show();
7758 },
7759
7760 /**
7761 * Remove the plot line or band
7762 */
7763 destroy: function() {
7764 // remove it from the lookup
7765 erase(this.axis.plotLinesAndBands, this);
7766
7767 delete this.axis;
7768 destroyObjectProperties(this);
7769 }
7770 };
7771
7772 /**
7773 * Object with members for extending the Axis prototype
7774 * @todo Extend directly instead of adding object to Highcharts first
7775 */
7776
7777 H.AxisPlotLineOrBandExtension = {
7778
7779 /**
7780 * Create the path for a plot band
7781 */
7782 getPlotBandPath: function(from, to) {
7783 var toPath = this.getPlotLinePath(to, null, null, true),
7784 path = this.getPlotLinePath(from, null, null, true);
7785
7786 if (path && toPath) {
7787
7788 // Flat paths don't need labels (#3836)
7789 path.flat = path.toString() === toPath.toString();
7790
7791 path.push(
7792 toPath[4],
7793 toPath[5],
7794 toPath[1],
7795 toPath[2],
7796 'z' // #5909
7797 );
7798 } else { // outside the axis area
7799 path = null;
7800 }
7801
7802 return path;
7803 },
7804
7805 addPlotBand: function(options) {
7806 return this.addPlotBandOrLine(options, 'plotBands');
7807 },
7808
7809 addPlotLine: function(options) {
7810 return this.addPlotBandOrLine(options, 'plotLines');
7811 },
7812
7813 /**
7814 * Add a plot band or plot line after render time
7815 *
7816 * @param options {Object} The plotBand or plotLine configuration object
7817 */
7818 addPlotBandOrLine: function(options, coll) {
7819 var obj = new H.PlotLineOrBand(this, options).render(),
7820 userOptions = this.userOptions;
7821
7822 if (obj) { // #2189
7823 // Add it to the user options for exporting and Axis.update
7824 if (coll) {
7825 userOptions[coll] = userOptions[coll] || [];
7826 userOptions[coll].push(options);
7827 }
7828 this.plotLinesAndBands.push(obj);
7829 }
7830
7831 return obj;
7832 },
7833
7834 /**
7835 * Remove a plot band or plot line from the chart by id
7836 * @param {Object} id
7837 */
7838 removePlotBandOrLine: function(id) {
7839 var plotLinesAndBands = this.plotLinesAndBands,
7840 options = this.options,
7841 userOptions = this.userOptions,
7842 i = plotLinesAndBands.length;
7843 while (i--) {
7844 if (plotLinesAndBands[i].id === id) {
7845 plotLinesAndBands[i].destroy();
7846 }
7847 }
7848 each([options.plotLines || [], userOptions.plotLines || [], options.plotBands || [], userOptions.plotBands || []], function(arr) {
7849 i = arr.length;
7850 while (i--) {
7851 if (arr[i].id === id) {
7852 erase(arr, arr[i]);
7853 }
7854 }
7855 });
7856 }
7857 };
7858
7859 }(Highcharts));
7860 (function(H) {
7861 /**
7862 * (c) 2010-2016 Torstein Honsi
7863 *
7864 * License: www.highcharts.com/license
7865 */
7866 'use strict';
7867 var correctFloat = H.correctFloat,
7868 defined = H.defined,
7869 destroyObjectProperties = H.destroyObjectProperties,
7870 isNumber = H.isNumber,
7871 merge = H.merge,
7872 pick = H.pick,
7873 deg2rad = H.deg2rad;
7874
7875 /**
7876 * The Tick class
7877 */
7878 H.Tick = function(axis, pos, type, noLabel) {
7879 this.axis = axis;
7880 this.pos = pos;
7881 this.type = type || '';
7882 this.isNew = true;
7883
7884 if (!type && !noLabel) {
7885 this.addLabel();
7886 }
7887 };
7888
7889 H.Tick.prototype = {
7890 /**
7891 * Write the tick label
7892 */
7893 addLabel: function() {
7894 var tick = this,
7895 axis = tick.axis,
7896 options = axis.options,
7897 chart = axis.chart,
7898 categories = axis.categories,
7899 names = axis.names,
7900 pos = tick.pos,
7901 labelOptions = options.labels,
7902 str,
7903 tickPositions = axis.tickPositions,
7904 isFirst = pos === tickPositions[0],
7905 isLast = pos === tickPositions[tickPositions.length - 1],
7906 value = categories ?
7907 pick(categories[pos], names[pos], pos) :
7908 pos,
7909 label = tick.label,
7910 tickPositionInfo = tickPositions.info,
7911 dateTimeLabelFormat;
7912
7913 // Set the datetime label format. If a higher rank is set for this position, use that. If not,
7914 // use the general format.
7915 if (axis.isDatetimeAxis && tickPositionInfo) {
7916 dateTimeLabelFormat =
7917 options.dateTimeLabelFormats[
7918 tickPositionInfo.higherRanks[pos] || tickPositionInfo.unitName
7919 ];
7920 }
7921 // set properties for access in render method
7922 tick.isFirst = isFirst;
7923 tick.isLast = isLast;
7924
7925 // get the string
7926 str = axis.labelFormatter.call({
7927 axis: axis,
7928 chart: chart,
7929 isFirst: isFirst,
7930 isLast: isLast,
7931 dateTimeLabelFormat: dateTimeLabelFormat,
7932 value: axis.isLog ? correctFloat(axis.lin2log(value)) : value
7933 });
7934
7935 // prepare CSS
7936 //css = width && { width: Math.max(1, Math.round(width - 2 * (labelOptions.padding || 10))) + 'px' };
7937
7938 // first call
7939 if (!defined(label)) {
7940
7941 tick.label = label =
7942 defined(str) && labelOptions.enabled ?
7943 chart.renderer.text(
7944 str,
7945 0,
7946 0,
7947 labelOptions.useHTML
7948 )
7949
7950 // without position absolute, IE export sometimes is wrong
7951 .css(merge(labelOptions.style))
7952
7953 .add(axis.labelGroup):
7954 null;
7955 tick.labelLength = label && label.getBBox().width; // Un-rotated length
7956 tick.rotation = 0; // Base value to detect change for new calls to getBBox
7957
7958 // update
7959 } else if (label) {
7960 label.attr({
7961 text: str
7962 });
7963 }
7964 },
7965
7966 /**
7967 * Get the offset height or width of the label
7968 */
7969 getLabelSize: function() {
7970 return this.label ?
7971 this.label.getBBox()[this.axis.horiz ? 'height' : 'width'] :
7972 0;
7973 },
7974
7975 /**
7976 * Handle the label overflow by adjusting the labels to the left and right edge, or
7977 * hide them if they collide into the neighbour label.
7978 */
7979 handleOverflow: function(xy) {
7980 var axis = this.axis,
7981 pxPos = xy.x,
7982 chartWidth = axis.chart.chartWidth,
7983 spacing = axis.chart.spacing,
7984 leftBound = pick(axis.labelLeft, Math.min(axis.pos, spacing[3])),
7985 rightBound = pick(axis.labelRight, Math.max(axis.pos + axis.len, chartWidth - spacing[1])),
7986 label = this.label,
7987 rotation = this.rotation,
7988 factor = {
7989 left: 0,
7990 center: 0.5,
7991 right: 1
7992 }[axis.labelAlign],
7993 labelWidth = label.getBBox().width,
7994 slotWidth = axis.getSlotWidth(),
7995 modifiedSlotWidth = slotWidth,
7996 xCorrection = factor,
7997 goRight = 1,
7998 leftPos,
7999 rightPos,
8000 textWidth,
8001 css = {};
8002
8003 // Check if the label overshoots the chart spacing box. If it does, move it.
8004 // If it now overshoots the slotWidth, add ellipsis.
8005 if (!rotation) {
8006 leftPos = pxPos - factor * labelWidth;
8007 rightPos = pxPos + (1 - factor) * labelWidth;
8008
8009 if (leftPos < leftBound) {
8010 modifiedSlotWidth = xy.x + modifiedSlotWidth * (1 - factor) - leftBound;
8011 } else if (rightPos > rightBound) {
8012 modifiedSlotWidth = rightBound - xy.x + modifiedSlotWidth * factor;
8013 goRight = -1;
8014 }
8015
8016 modifiedSlotWidth = Math.min(slotWidth, modifiedSlotWidth); // #4177
8017 if (modifiedSlotWidth < slotWidth && axis.labelAlign === 'center') {
8018 xy.x += goRight * (slotWidth - modifiedSlotWidth - xCorrection *
8019 (slotWidth - Math.min(labelWidth, modifiedSlotWidth)));
8020 }
8021 // If the label width exceeds the available space, set a text width to be
8022 // picked up below. Also, if a width has been set before, we need to set a new
8023 // one because the reported labelWidth will be limited by the box (#3938).
8024 if (labelWidth > modifiedSlotWidth || (axis.autoRotation && (label.styles || {}).width)) {
8025 textWidth = modifiedSlotWidth;
8026 }
8027
8028 // Add ellipsis to prevent rotated labels to be clipped against the edge of the chart
8029 } else if (rotation < 0 && pxPos - factor * labelWidth < leftBound) {
8030 textWidth = Math.round(pxPos / Math.cos(rotation * deg2rad) - leftBound);
8031 } else if (rotation > 0 && pxPos + factor * labelWidth > rightBound) {
8032 textWidth = Math.round((chartWidth - pxPos) / Math.cos(rotation * deg2rad));
8033 }
8034
8035 if (textWidth) {
8036 css.width = textWidth;
8037 if (!(axis.options.labels.style || {}).textOverflow) {
8038 css.textOverflow = 'ellipsis';
8039 }
8040 label.css(css);
8041 }
8042 },
8043
8044 /**
8045 * Get the x and y position for ticks and labels
8046 */
8047 getPosition: function(horiz, pos, tickmarkOffset, old) {
8048 var axis = this.axis,
8049 chart = axis.chart,
8050 cHeight = (old && chart.oldChartHeight) || chart.chartHeight;
8051
8052 return {
8053 x: horiz ?
8054 axis.translate(pos + tickmarkOffset, null, null, old) + axis.transB : axis.left + axis.offset +
8055 (axis.opposite ?
8056 ((old && chart.oldChartWidth) || chart.chartWidth) - axis.right - axis.left :
8057 0
8058 ),
8059
8060 y: horiz ?
8061 cHeight - axis.bottom + axis.offset - (axis.opposite ? axis.height : 0) : cHeight - axis.translate(pos + tickmarkOffset, null, null, old) - axis.transB
8062 };
8063
8064 },
8065
8066 /**
8067 * Get the x, y position of the tick label
8068 */
8069 getLabelPosition: function(x, y, label, horiz, labelOptions, tickmarkOffset, index, step) {
8070 var axis = this.axis,
8071 transA = axis.transA,
8072 reversed = axis.reversed,
8073 staggerLines = axis.staggerLines,
8074 rotCorr = axis.tickRotCorr || {
8075 x: 0,
8076 y: 0
8077 },
8078 yOffset = labelOptions.y,
8079 line;
8080
8081 if (!defined(yOffset)) {
8082 if (axis.side === 0) {
8083 yOffset = label.rotation ? -8 : -label.getBBox().height;
8084 } else if (axis.side === 2) {
8085 yOffset = rotCorr.y + 8;
8086 } else {
8087 // #3140, #3140
8088 yOffset = Math.cos(label.rotation * deg2rad) * (rotCorr.y - label.getBBox(false, 0).height / 2);
8089 }
8090 }
8091
8092 x = x + labelOptions.x + rotCorr.x - (tickmarkOffset && horiz ?
8093 tickmarkOffset * transA * (reversed ? -1 : 1) : 0);
8094 y = y + yOffset - (tickmarkOffset && !horiz ?
8095 tickmarkOffset * transA * (reversed ? 1 : -1) : 0);
8096
8097 // Correct for staggered labels
8098 if (staggerLines) {
8099 line = (index / (step || 1) % staggerLines);
8100 if (axis.opposite) {
8101 line = staggerLines - line - 1;
8102 }
8103 y += line * (axis.labelOffset / staggerLines);
8104 }
8105
8106 return {
8107 x: x,
8108 y: Math.round(y)
8109 };
8110 },
8111
8112 /**
8113 * Extendible method to return the path of the marker
8114 */
8115 getMarkPath: function(x, y, tickLength, tickWidth, horiz, renderer) {
8116 return renderer.crispLine([
8117 'M',
8118 x,
8119 y,
8120 'L',
8121 x + (horiz ? 0 : -tickLength),
8122 y + (horiz ? tickLength : 0)
8123 ], tickWidth);
8124 },
8125
8126 /**
8127 * Put everything in place
8128 *
8129 * @param index {Number}
8130 * @param old {Boolean} Use old coordinates to prepare an animation into new position
8131 */
8132 render: function(index, old, opacity) {
8133 var tick = this,
8134 axis = tick.axis,
8135 options = axis.options,
8136 chart = axis.chart,
8137 renderer = chart.renderer,
8138 horiz = axis.horiz,
8139 type = tick.type,
8140 label = tick.label,
8141 pos = tick.pos,
8142 labelOptions = options.labels,
8143 gridLine = tick.gridLine,
8144 tickPrefix = type ? type + 'Tick' : 'tick',
8145 tickSize = axis.tickSize(tickPrefix),
8146 gridLinePath,
8147 mark = tick.mark,
8148 isNewMark = !mark,
8149 step = labelOptions.step,
8150 attribs = {},
8151 show = true,
8152 tickmarkOffset = axis.tickmarkOffset,
8153 xy = tick.getPosition(horiz, pos, tickmarkOffset, old),
8154 x = xy.x,
8155 y = xy.y,
8156 reverseCrisp = ((horiz && x === axis.pos + axis.len) ||
8157 (!horiz && y === axis.pos)) ? -1 : 1; // #1480, #1687
8158
8159
8160 var gridPrefix = type ? type + 'Grid' : 'grid',
8161 gridLineWidth = options[gridPrefix + 'LineWidth'],
8162 gridLineColor = options[gridPrefix + 'LineColor'],
8163 dashStyle = options[gridPrefix + 'LineDashStyle'],
8164 tickWidth = pick(options[tickPrefix + 'Width'], !type && axis.isXAxis ? 1 : 0), // X axis defaults to 1
8165 tickColor = options[tickPrefix + 'Color'];
8166
8167
8168 opacity = pick(opacity, 1);
8169 this.isActive = true;
8170
8171 // Create the grid line
8172 if (!gridLine) {
8173
8174 attribs.stroke = gridLineColor;
8175 attribs['stroke-width'] = gridLineWidth;
8176 if (dashStyle) {
8177 attribs.dashstyle = dashStyle;
8178 }
8179
8180 if (!type) {
8181 attribs.zIndex = 1;
8182 }
8183 if (old) {
8184 attribs.opacity = 0;
8185 }
8186 tick.gridLine = gridLine = renderer.path()
8187 .attr(attribs)
8188 .addClass('highcharts-' + (type ? type + '-' : '') + 'grid-line')
8189 .add(axis.gridGroup);
8190 }
8191
8192 // If the parameter 'old' is set, the current call will be followed
8193 // by another call, therefore do not do any animations this time
8194 if (!old && gridLine) {
8195 gridLinePath = axis.getPlotLinePath(pos + tickmarkOffset, gridLine.strokeWidth() * reverseCrisp, old, true);
8196 if (gridLinePath) {
8197 gridLine[tick.isNew ? 'attr' : 'animate']({
8198 d: gridLinePath,
8199 opacity: opacity
8200 });
8201 }
8202 }
8203
8204 // create the tick mark
8205 if (tickSize) {
8206
8207 // negate the length
8208 if (axis.opposite) {
8209 tickSize[0] = -tickSize[0];
8210 }
8211
8212 // First time, create it
8213 if (isNewMark) {
8214 tick.mark = mark = renderer.path()
8215 .addClass('highcharts-' + (type ? type + '-' : '') + 'tick')
8216 .add(axis.axisGroup);
8217
8218
8219 mark.attr({
8220 stroke: tickColor,
8221 'stroke-width': tickWidth
8222 });
8223
8224 }
8225 mark[isNewMark ? 'attr' : 'animate']({
8226 d: tick.getMarkPath(x, y, tickSize[0], mark.strokeWidth() * reverseCrisp, horiz, renderer),
8227 opacity: opacity
8228 });
8229
8230 }
8231
8232 // the label is created on init - now move it into place
8233 if (label && isNumber(x)) {
8234 label.xy = xy = tick.getLabelPosition(x, y, label, horiz, labelOptions, tickmarkOffset, index, step);
8235
8236 // Apply show first and show last. If the tick is both first and last, it is
8237 // a single centered tick, in which case we show the label anyway (#2100).
8238 if ((tick.isFirst && !tick.isLast && !pick(options.showFirstLabel, 1)) ||
8239 (tick.isLast && !tick.isFirst && !pick(options.showLastLabel, 1))) {
8240 show = false;
8241
8242 // Handle label overflow and show or hide accordingly
8243 } else if (horiz && !axis.isRadial && !labelOptions.step &&
8244 !labelOptions.rotation && !old && opacity !== 0) {
8245 tick.handleOverflow(xy);
8246 }
8247
8248 // apply step
8249 if (step && index % step) {
8250 // show those indices dividable by step
8251 show = false;
8252 }
8253
8254 // Set the new position, and show or hide
8255 if (show && isNumber(xy.y)) {
8256 xy.opacity = opacity;
8257 label[tick.isNew ? 'attr' : 'animate'](xy);
8258 } else {
8259 label.attr('y', -9999); // #1338
8260 }
8261 tick.isNew = false;
8262 }
8263 },
8264
8265 /**
8266 * Destructor for the tick prototype
8267 */
8268 destroy: function() {
8269 destroyObjectProperties(this, this.axis);
8270 }
8271 };
8272
8273 }(Highcharts));
8274 (function(H) {
8275 /**
8276 * (c) 2010-2016 Torstein Honsi
8277 *
8278 * License: www.highcharts.com/license
8279 */
8280 'use strict';
8281
8282 var addEvent = H.addEvent,
8283 animObject = H.animObject,
8284 arrayMax = H.arrayMax,
8285 arrayMin = H.arrayMin,
8286 AxisPlotLineOrBandExtension = H.AxisPlotLineOrBandExtension,
8287 color = H.color,
8288 correctFloat = H.correctFloat,
8289 defaultOptions = H.defaultOptions,
8290 defined = H.defined,
8291 deg2rad = H.deg2rad,
8292 destroyObjectProperties = H.destroyObjectProperties,
8293 each = H.each,
8294 error = H.error,
8295 extend = H.extend,
8296 fireEvent = H.fireEvent,
8297 format = H.format,
8298 getMagnitude = H.getMagnitude,
8299 grep = H.grep,
8300 inArray = H.inArray,
8301 isArray = H.isArray,
8302 isNumber = H.isNumber,
8303 isString = H.isString,
8304 merge = H.merge,
8305 normalizeTickInterval = H.normalizeTickInterval,
8306 pick = H.pick,
8307 PlotLineOrBand = H.PlotLineOrBand,
8308 removeEvent = H.removeEvent,
8309 splat = H.splat,
8310 syncTimeout = H.syncTimeout,
8311 Tick = H.Tick;
8312
8313 /**
8314 * Create a new axis object.
8315 * @constructor Axis
8316 * @param {Object} chart
8317 * @param {Object} options
8318 */
8319 H.Axis = function() {
8320 this.init.apply(this, arguments);
8321 };
8322
8323 H.Axis.prototype = {
8324
8325 /**
8326 * Default options for the X axis - the Y axis has extended defaults
8327 */
8328 defaultOptions: {
8329 // allowDecimals: null,
8330 // alternateGridColor: null,
8331 // categories: [],
8332 dateTimeLabelFormats: {
8333 millisecond: '%H:%M:%S.%L',
8334 second: '%H:%M:%S',
8335 minute: '%H:%M',
8336 hour: '%H:%M',
8337 day: '%e. %b',
8338 week: '%e. %b',
8339 month: '%b \'%y',
8340 year: '%Y'
8341 },
8342 endOnTick: false,
8343 // reversed: false,
8344
8345 labels: {
8346 enabled: true,
8347 // rotation: 0,
8348 // align: 'center',
8349 // step: null,
8350
8351 style: {
8352 color: '#666666',
8353 cursor: 'default',
8354 fontSize: '11px'
8355 },
8356
8357 x: 0
8358 //y: undefined
8359 /*formatter: function () {
8360 return this.value;
8361 },*/
8362 },
8363 //linkedTo: null,
8364 //max: undefined,
8365 //min: undefined,
8366 minPadding: 0.01,
8367 maxPadding: 0.01,
8368 //minRange: null,
8369 //minorTickInterval: null,
8370 minorTickLength: 2,
8371 minorTickPosition: 'outside', // inside or outside
8372 //opposite: false,
8373 //offset: 0,
8374 //plotBands: [{
8375 // events: {},
8376 // zIndex: 1,
8377 // labels: { align, x, verticalAlign, y, style, rotation, textAlign }
8378 //}],
8379 //plotLines: [{
8380 // events: {}
8381 // dashStyle: {}
8382 // zIndex:
8383 // labels: { align, x, verticalAlign, y, style, rotation, textAlign }
8384 //}],
8385 //reversed: false,
8386 // showFirstLabel: true,
8387 // showLastLabel: true,
8388 startOfWeek: 1,
8389 startOnTick: false,
8390 //tickInterval: null,
8391 tickLength: 10,
8392 tickmarkPlacement: 'between', // on or between
8393 tickPixelInterval: 100,
8394 tickPosition: 'outside',
8395 title: {
8396 //text: null,
8397 align: 'middle', // low, middle or high
8398 //margin: 0 for horizontal, 10 for vertical axes,
8399 //rotation: 0,
8400 //side: 'outside',
8401
8402 style: {
8403 color: '#666666'
8404 }
8405
8406 //x: 0,
8407 //y: 0
8408 },
8409 type: 'linear', // linear, logarithmic or datetime
8410 //visible: true
8411
8412 minorGridLineColor: '#f2f2f2',
8413 // minorGridLineDashStyle: null,
8414 minorGridLineWidth: 1,
8415 minorTickColor: '#999999',
8416 //minorTickWidth: 0,
8417 lineColor: '#ccd6eb',
8418 lineWidth: 1,
8419 gridLineColor: '#e6e6e6',
8420 // gridLineDashStyle: 'solid',
8421 // gridLineWidth: 0,
8422 tickColor: '#ccd6eb'
8423 // tickWidth: 1
8424
8425 },
8426
8427 /**
8428 * This options set extends the defaultOptions for Y axes
8429 */
8430 defaultYAxisOptions: {
8431 endOnTick: true,
8432 tickPixelInterval: 72,
8433 showLastLabel: true,
8434 labels: {
8435 x: -8
8436 },
8437 maxPadding: 0.05,
8438 minPadding: 0.05,
8439 startOnTick: true,
8440 title: {
8441 rotation: 270,
8442 text: 'Values'
8443 },
8444 stackLabels: {
8445 enabled: false,
8446 //align: dynamic,
8447 //y: dynamic,
8448 //x: dynamic,
8449 //verticalAlign: dynamic,
8450 //textAlign: dynamic,
8451 //rotation: 0,
8452 formatter: function() {
8453 return H.numberFormat(this.total, -1);
8454 },
8455
8456 style: {
8457 fontSize: '11px',
8458 fontWeight: 'bold',
8459 color: '#000000',
8460 textOutline: '1px contrast'
8461 }
8462
8463 },
8464
8465 gridLineWidth: 1,
8466 lineWidth: 0
8467 // tickWidth: 0
8468
8469 },
8470
8471 /**
8472 * These options extend the defaultOptions for left axes
8473 */
8474 defaultLeftAxisOptions: {
8475 labels: {
8476 x: -15
8477 },
8478 title: {
8479 rotation: 270
8480 }
8481 },
8482
8483 /**
8484 * These options extend the defaultOptions for right axes
8485 */
8486 defaultRightAxisOptions: {
8487 labels: {
8488 x: 15
8489 },
8490 title: {
8491 rotation: 90
8492 }
8493 },
8494
8495 /**
8496 * These options extend the defaultOptions for bottom axes
8497 */
8498 defaultBottomAxisOptions: {
8499 labels: {
8500 autoRotation: [-45],
8501 x: 0
8502 // overflow: undefined,
8503 // staggerLines: null
8504 },
8505 title: {
8506 rotation: 0
8507 }
8508 },
8509 /**
8510 * These options extend the defaultOptions for top axes
8511 */
8512 defaultTopAxisOptions: {
8513 labels: {
8514 autoRotation: [-45],
8515 x: 0
8516 // overflow: undefined
8517 // staggerLines: null
8518 },
8519 title: {
8520 rotation: 0
8521 }
8522 },
8523
8524 /**
8525 * Initialize the axis
8526 */
8527 init: function(chart, userOptions) {
8528
8529
8530 var isXAxis = userOptions.isX,
8531 axis = this;
8532
8533 axis.chart = chart;
8534
8535 // Flag, is the axis horizontal
8536 axis.horiz = chart.inverted ? !isXAxis : isXAxis;
8537
8538 // Flag, isXAxis
8539 axis.isXAxis = isXAxis;
8540 axis.coll = axis.coll || (isXAxis ? 'xAxis' : 'yAxis');
8541
8542 axis.opposite = userOptions.opposite; // needed in setOptions
8543 axis.side = userOptions.side || (axis.horiz ?
8544 (axis.opposite ? 0 : 2) : // top : bottom
8545 (axis.opposite ? 1 : 3)); // right : left
8546
8547 axis.setOptions(userOptions);
8548
8549
8550 var options = this.options,
8551 type = options.type,
8552 isDatetimeAxis = type === 'datetime';
8553
8554 axis.labelFormatter = options.labels.formatter || axis.defaultLabelFormatter; // can be overwritten by dynamic format
8555
8556
8557 // Flag, stagger lines or not
8558 axis.userOptions = userOptions;
8559
8560 //axis.axisTitleMargin = undefined,// = options.title.margin,
8561 axis.minPixelPadding = 0;
8562
8563 axis.reversed = options.reversed;
8564 axis.visible = options.visible !== false;
8565 axis.zoomEnabled = options.zoomEnabled !== false;
8566
8567 // Initial categories
8568 axis.hasNames = type === 'category' || options.categories === true;
8569 axis.categories = options.categories || axis.hasNames;
8570 axis.names = axis.names || []; // Preserve on update (#3830)
8571
8572 // Elements
8573 //axis.axisGroup = undefined;
8574 //axis.gridGroup = undefined;
8575 //axis.axisTitle = undefined;
8576 //axis.axisLine = undefined;
8577
8578 // Shorthand types
8579 axis.isLog = type === 'logarithmic';
8580 axis.isDatetimeAxis = isDatetimeAxis;
8581
8582 // Flag, if axis is linked to another axis
8583 axis.isLinked = defined(options.linkedTo);
8584 // Linked axis.
8585 //axis.linkedParent = undefined;
8586
8587 // Tick positions
8588 //axis.tickPositions = undefined; // array containing predefined positions
8589 // Tick intervals
8590 //axis.tickInterval = undefined;
8591 //axis.minorTickInterval = undefined;
8592
8593
8594 // Major ticks
8595 axis.ticks = {};
8596 axis.labelEdge = [];
8597 // Minor ticks
8598 axis.minorTicks = {};
8599
8600 // List of plotLines/Bands
8601 axis.plotLinesAndBands = [];
8602
8603 // Alternate bands
8604 axis.alternateBands = {};
8605
8606 // Axis metrics
8607 //axis.left = undefined;
8608 //axis.top = undefined;
8609 //axis.width = undefined;
8610 //axis.height = undefined;
8611 //axis.bottom = undefined;
8612 //axis.right = undefined;
8613 //axis.transA = undefined;
8614 //axis.transB = undefined;
8615 //axis.oldTransA = undefined;
8616 axis.len = 0;
8617 //axis.oldMin = undefined;
8618 //axis.oldMax = undefined;
8619 //axis.oldUserMin = undefined;
8620 //axis.oldUserMax = undefined;
8621 //axis.oldAxisLength = undefined;
8622 axis.minRange = axis.userMinRange = options.minRange || options.maxZoom;
8623 axis.range = options.range;
8624 axis.offset = options.offset || 0;
8625
8626
8627 // Dictionary for stacks
8628 axis.stacks = {};
8629 axis.oldStacks = {};
8630 axis.stacksTouched = 0;
8631
8632 // Min and max in the data
8633 //axis.dataMin = undefined,
8634 //axis.dataMax = undefined,
8635
8636 // The axis range
8637 axis.max = null;
8638 axis.min = null;
8639
8640 // User set min and max
8641 //axis.userMin = undefined,
8642 //axis.userMax = undefined,
8643
8644 // Crosshair options
8645 axis.crosshair = pick(options.crosshair, splat(chart.options.tooltip.crosshairs)[isXAxis ? 0 : 1], false);
8646 // Run Axis
8647
8648 var eventType,
8649 events = axis.options.events;
8650
8651 // Register
8652 if (inArray(axis, chart.axes) === -1) { // don't add it again on Axis.update()
8653 if (isXAxis) { // #2713
8654 chart.axes.splice(chart.xAxis.length, 0, axis);
8655 } else {
8656 chart.axes.push(axis);
8657 }
8658
8659 chart[axis.coll].push(axis);
8660 }
8661
8662 axis.series = axis.series || []; // populated by Series
8663
8664 // inverted charts have reversed xAxes as default
8665 if (chart.inverted && isXAxis && axis.reversed === undefined) {
8666 axis.reversed = true;
8667 }
8668
8669 axis.removePlotBand = axis.removePlotBandOrLine;
8670 axis.removePlotLine = axis.removePlotBandOrLine;
8671
8672
8673 // register event listeners
8674 for (eventType in events) {
8675 addEvent(axis, eventType, events[eventType]);
8676 }
8677
8678 // extend logarithmic axis
8679 if (axis.isLog) {
8680 axis.val2lin = axis.log2lin;
8681 axis.lin2val = axis.lin2log;
8682 }
8683 },
8684
8685 /**
8686 * Merge and set options
8687 */
8688 setOptions: function(userOptions) {
8689 this.options = merge(
8690 this.defaultOptions,
8691 this.coll === 'yAxis' && this.defaultYAxisOptions, [this.defaultTopAxisOptions, this.defaultRightAxisOptions,
8692 this.defaultBottomAxisOptions, this.defaultLeftAxisOptions
8693 ][this.side],
8694 merge(
8695 defaultOptions[this.coll], // if set in setOptions (#1053)
8696 userOptions
8697 )
8698 );
8699 },
8700
8701 /**
8702 * The default label formatter. The context is a special config object for the label.
8703 */
8704 defaultLabelFormatter: function() {
8705 var axis = this.axis,
8706 value = this.value,
8707 categories = axis.categories,
8708 dateTimeLabelFormat = this.dateTimeLabelFormat,
8709 lang = defaultOptions.lang,
8710 numericSymbols = lang.numericSymbols,
8711 numSymMagnitude = lang.numericSymbolMagnitude || 1000,
8712 i = numericSymbols && numericSymbols.length,
8713 multi,
8714 ret,
8715 formatOption = axis.options.labels.format,
8716
8717 // make sure the same symbol is added for all labels on a linear axis
8718 numericSymbolDetector = axis.isLog ? value : axis.tickInterval;
8719
8720 if (formatOption) {
8721 ret = format(formatOption, this);
8722
8723 } else if (categories) {
8724 ret = value;
8725
8726 } else if (dateTimeLabelFormat) { // datetime axis
8727 ret = H.dateFormat(dateTimeLabelFormat, value);
8728
8729 } else if (i && numericSymbolDetector >= 1000) {
8730 // Decide whether we should add a numeric symbol like k (thousands) or M (millions).
8731 // If we are to enable this in tooltip or other places as well, we can move this
8732 // logic to the numberFormatter and enable it by a parameter.
8733 while (i-- && ret === undefined) {
8734 multi = Math.pow(numSymMagnitude, i + 1);
8735 if (numericSymbolDetector >= multi && (value * 10) % multi === 0 && numericSymbols[i] !== null && value !== 0) { // #5480
8736 ret = H.numberFormat(value / multi, -1) + numericSymbols[i];
8737 }
8738 }
8739 }
8740
8741 if (ret === undefined) {
8742 if (Math.abs(value) >= 10000) { // add thousands separators
8743 ret = H.numberFormat(value, -1);
8744 } else { // small numbers
8745 ret = H.numberFormat(value, -1, undefined, ''); // #2466
8746 }
8747 }
8748
8749 return ret;
8750 },
8751
8752 /**
8753 * Get the minimum and maximum for the series of each axis
8754 */
8755 getSeriesExtremes: function() {
8756 var axis = this,
8757 chart = axis.chart;
8758 axis.hasVisibleSeries = false;
8759
8760 // Reset properties in case we're redrawing (#3353)
8761 axis.dataMin = axis.dataMax = axis.threshold = null;
8762 axis.softThreshold = !axis.isXAxis;
8763
8764 if (axis.buildStacks) {
8765 axis.buildStacks();
8766 }
8767
8768 // loop through this axis' series
8769 each(axis.series, function(series) {
8770
8771 if (series.visible || !chart.options.chart.ignoreHiddenSeries) {
8772
8773 var seriesOptions = series.options,
8774 xData,
8775 threshold = seriesOptions.threshold,
8776 seriesDataMin,
8777 seriesDataMax;
8778
8779 axis.hasVisibleSeries = true;
8780
8781 // Validate threshold in logarithmic axes
8782 if (axis.isLog && threshold <= 0) {
8783 threshold = null;
8784 }
8785
8786 // Get dataMin and dataMax for X axes
8787 if (axis.isXAxis) {
8788 xData = series.xData;
8789 if (xData.length) {
8790 // If xData contains values which is not numbers, then filter them out.
8791 // To prevent performance hit, we only do this after we have already
8792 // found seriesDataMin because in most cases all data is valid. #5234.
8793 seriesDataMin = arrayMin(xData);
8794 if (!isNumber(seriesDataMin) && !(seriesDataMin instanceof Date)) { // Date for #5010
8795 xData = grep(xData, function(x) {
8796 return isNumber(x);
8797 });
8798 seriesDataMin = arrayMin(xData); // Do it again with valid data
8799 }
8800
8801 axis.dataMin = Math.min(pick(axis.dataMin, xData[0]), seriesDataMin);
8802 axis.dataMax = Math.max(pick(axis.dataMax, xData[0]), arrayMax(xData));
8803
8804 }
8805
8806 // Get dataMin and dataMax for Y axes, as well as handle stacking and processed data
8807 } else {
8808
8809 // Get this particular series extremes
8810 series.getExtremes();
8811 seriesDataMax = series.dataMax;
8812 seriesDataMin = series.dataMin;
8813
8814 // Get the dataMin and dataMax so far. If percentage is used, the min and max are
8815 // always 0 and 100. If seriesDataMin and seriesDataMax is null, then series
8816 // doesn't have active y data, we continue with nulls
8817 if (defined(seriesDataMin) && defined(seriesDataMax)) {
8818 axis.dataMin = Math.min(pick(axis.dataMin, seriesDataMin), seriesDataMin);
8819 axis.dataMax = Math.max(pick(axis.dataMax, seriesDataMax), seriesDataMax);
8820 }
8821
8822 // Adjust to threshold
8823 if (defined(threshold)) {
8824 axis.threshold = threshold;
8825 }
8826 // If any series has a hard threshold, it takes precedence
8827 if (!seriesOptions.softThreshold || axis.isLog) {
8828 axis.softThreshold = false;
8829 }
8830 }
8831 }
8832 });
8833 },
8834
8835 /**
8836 * Translate from axis value to pixel position on the chart, or back
8837 *
8838 */
8839 translate: function(val, backwards, cvsCoord, old, handleLog, pointPlacement) {
8840 var axis = this.linkedParent || this, // #1417
8841 sign = 1,
8842 cvsOffset = 0,
8843 localA = old ? axis.oldTransA : axis.transA,
8844 localMin = old ? axis.oldMin : axis.min,
8845 returnValue,
8846 minPixelPadding = axis.minPixelPadding,
8847 doPostTranslate = (axis.isOrdinal || axis.isBroken || (axis.isLog && handleLog)) && axis.lin2val;
8848
8849 if (!localA) {
8850 localA = axis.transA;
8851 }
8852
8853 // In vertical axes, the canvas coordinates start from 0 at the top like in
8854 // SVG.
8855 if (cvsCoord) {
8856 sign *= -1; // canvas coordinates inverts the value
8857 cvsOffset = axis.len;
8858 }
8859
8860 // Handle reversed axis
8861 if (axis.reversed) {
8862 sign *= -1;
8863 cvsOffset -= sign * (axis.sector || axis.len);
8864 }
8865
8866 // From pixels to value
8867 if (backwards) { // reverse translation
8868
8869 val = val * sign + cvsOffset;
8870 val -= minPixelPadding;
8871 returnValue = val / localA + localMin; // from chart pixel to value
8872 if (doPostTranslate) { // log and ordinal axes
8873 returnValue = axis.lin2val(returnValue);
8874 }
8875
8876 // From value to pixels
8877 } else {
8878 if (doPostTranslate) { // log and ordinal axes
8879 val = axis.val2lin(val);
8880 }
8881 returnValue = sign * (val - localMin) * localA + cvsOffset +
8882 (sign * minPixelPadding) +
8883 (isNumber(pointPlacement) ? localA * pointPlacement : 0);
8884 }
8885
8886 return returnValue;
8887 },
8888
8889 /**
8890 * Utility method to translate an axis value to pixel position.
8891 * @param {Number} value A value in terms of axis units
8892 * @param {Boolean} paneCoordinates Whether to return the pixel coordinate relative to the chart
8893 * or just the axis/pane itself.
8894 */
8895 toPixels: function(value, paneCoordinates) {
8896 return this.translate(value, false, !this.horiz, null, true) + (paneCoordinates ? 0 : this.pos);
8897 },
8898
8899 /*
8900 * Utility method to translate a pixel position in to an axis value
8901 * @param {Number} pixel The pixel value coordinate
8902 * @param {Boolean} paneCoordiantes Whether the input pixel is relative to the chart or just the
8903 * axis/pane itself.
8904 */
8905 toValue: function(pixel, paneCoordinates) {
8906 return this.translate(pixel - (paneCoordinates ? 0 : this.pos), true, !this.horiz, null, true);
8907 },
8908
8909 /**
8910 * Create the path for a plot line that goes from the given value on
8911 * this axis, across the plot to the opposite side
8912 * @param {Number} value
8913 * @param {Number} lineWidth Used for calculation crisp line
8914 * @param {Number] old Use old coordinates (for resizing and rescaling)
8915 */
8916 getPlotLinePath: function(value, lineWidth, old, force, translatedValue) {
8917 var axis = this,
8918 chart = axis.chart,
8919 axisLeft = axis.left,
8920 axisTop = axis.top,
8921 x1,
8922 y1,
8923 x2,
8924 y2,
8925 cHeight = (old && chart.oldChartHeight) || chart.chartHeight,
8926 cWidth = (old && chart.oldChartWidth) || chart.chartWidth,
8927 skip,
8928 transB = axis.transB,
8929 /**
8930 * Check if x is between a and b. If not, either move to a/b or skip,
8931 * depending on the force parameter.
8932 */
8933 between = function(x, a, b) {
8934 if (x < a || x > b) {
8935 if (force) {
8936 x = Math.min(Math.max(a, x), b);
8937 } else {
8938 skip = true;
8939 }
8940 }
8941 return x;
8942 };
8943
8944 translatedValue = pick(translatedValue, axis.translate(value, null, null, old));
8945 x1 = x2 = Math.round(translatedValue + transB);
8946 y1 = y2 = Math.round(cHeight - translatedValue - transB);
8947 if (!isNumber(translatedValue)) { // no min or max
8948 skip = true;
8949
8950 } else if (axis.horiz) {
8951 y1 = axisTop;
8952 y2 = cHeight - axis.bottom;
8953 x1 = x2 = between(x1, axisLeft, axisLeft + axis.width);
8954 } else {
8955 x1 = axisLeft;
8956 x2 = cWidth - axis.right;
8957 y1 = y2 = between(y1, axisTop, axisTop + axis.height);
8958 }
8959 return skip && !force ?
8960 null :
8961 chart.renderer.crispLine(['M', x1, y1, 'L', x2, y2], lineWidth || 1);
8962 },
8963
8964 /**
8965 * Set the tick positions of a linear axis to round values like whole tens or every five.
8966 */
8967 getLinearTickPositions: function(tickInterval, min, max) {
8968 var pos,
8969 lastPos,
8970 roundedMin = correctFloat(Math.floor(min / tickInterval) * tickInterval),
8971 roundedMax = correctFloat(Math.ceil(max / tickInterval) * tickInterval),
8972 tickPositions = [];
8973
8974 // For single points, add a tick regardless of the relative position (#2662)
8975 if (min === max && isNumber(min)) {
8976 return [min];
8977 }
8978
8979 // Populate the intermediate values
8980 pos = roundedMin;
8981 while (pos <= roundedMax) {
8982
8983 // Place the tick on the rounded value
8984 tickPositions.push(pos);
8985
8986 // Always add the raw tickInterval, not the corrected one.
8987 pos = correctFloat(pos + tickInterval);
8988
8989 // If the interval is not big enough in the current min - max range to actually increase
8990 // the loop variable, we need to break out to prevent endless loop. Issue #619
8991 if (pos === lastPos) {
8992 break;
8993 }
8994
8995 // Record the last value
8996 lastPos = pos;
8997 }
8998 return tickPositions;
8999 },
9000
9001 /**
9002 * Return the minor tick positions. For logarithmic axes, reuse the same logic
9003 * as for major ticks.
9004 */
9005 getMinorTickPositions: function() {
9006 var axis = this,
9007 options = axis.options,
9008 tickPositions = axis.tickPositions,
9009 minorTickInterval = axis.minorTickInterval,
9010 minorTickPositions = [],
9011 pos,
9012 i,
9013 pointRangePadding = axis.pointRangePadding || 0,
9014 min = axis.min - pointRangePadding, // #1498
9015 max = axis.max + pointRangePadding, // #1498
9016 range = max - min,
9017 len;
9018
9019 // If minor ticks get too dense, they are hard to read, and may cause long running script. So we don't draw them.
9020 if (range && range / minorTickInterval < axis.len / 3) { // #3875
9021
9022 if (axis.isLog) {
9023 len = tickPositions.length;
9024 for (i = 1; i < len; i++) {
9025 minorTickPositions = minorTickPositions.concat(
9026 axis.getLogTickPositions(minorTickInterval, tickPositions[i - 1], tickPositions[i], true)
9027 );
9028 }
9029 } else if (axis.isDatetimeAxis && options.minorTickInterval === 'auto') { // #1314
9030 minorTickPositions = minorTickPositions.concat(
9031 axis.getTimeTicks(
9032 axis.normalizeTimeTickInterval(minorTickInterval),
9033 min,
9034 max,
9035 options.startOfWeek
9036 )
9037 );
9038 } else {
9039 for (
9040 pos = min + (tickPositions[0] - min) % minorTickInterval; pos <= max; pos += minorTickInterval
9041 ) {
9042 // Very, very, tight grid lines (#5771)
9043 if (pos === minorTickPositions[0]) {
9044 break;
9045 }
9046 minorTickPositions.push(pos);
9047 }
9048 }
9049 }
9050
9051 if (minorTickPositions.length !== 0) { // don't change the extremes, when there is no minor ticks
9052 axis.trimTicks(minorTickPositions, options.startOnTick, options.endOnTick); // #3652 #3743 #1498
9053 }
9054 return minorTickPositions;
9055 },
9056
9057 /**
9058 * Adjust the min and max for the minimum range. Keep in mind that the series data is
9059 * not yet processed, so we don't have information on data cropping and grouping, or
9060 * updated axis.pointRange or series.pointRange. The data can't be processed until
9061 * we have finally established min and max.
9062 */
9063 adjustForMinRange: function() {
9064 var axis = this,
9065 options = axis.options,
9066 min = axis.min,
9067 max = axis.max,
9068 zoomOffset,
9069 spaceAvailable = axis.dataMax - axis.dataMin >= axis.minRange,
9070 closestDataRange,
9071 i,
9072 distance,
9073 xData,
9074 loopLength,
9075 minArgs,
9076 maxArgs,
9077 minRange;
9078
9079 // Set the automatic minimum range based on the closest point distance
9080 if (axis.isXAxis && axis.minRange === undefined && !axis.isLog) {
9081
9082 if (defined(options.min) || defined(options.max)) {
9083 axis.minRange = null; // don't do this again
9084
9085 } else {
9086
9087 // Find the closest distance between raw data points, as opposed to
9088 // closestPointRange that applies to processed points (cropped and grouped)
9089 each(axis.series, function(series) {
9090 xData = series.xData;
9091 loopLength = series.xIncrement ? 1 : xData.length - 1;
9092 for (i = loopLength; i > 0; i--) {
9093 distance = xData[i] - xData[i - 1];
9094 if (closestDataRange === undefined || distance < closestDataRange) {
9095 closestDataRange = distance;
9096 }
9097 }
9098 });
9099 axis.minRange = Math.min(closestDataRange * 5, axis.dataMax - axis.dataMin);
9100 }
9101 }
9102
9103 // if minRange is exceeded, adjust
9104 if (max - min < axis.minRange) {
9105 minRange = axis.minRange;
9106 zoomOffset = (minRange - max + min) / 2;
9107
9108 // if min and max options have been set, don't go beyond it
9109 minArgs = [min - zoomOffset, pick(options.min, min - zoomOffset)];
9110 if (spaceAvailable) { // if space is available, stay within the data range
9111 minArgs[2] = axis.isLog ? axis.log2lin(axis.dataMin) : axis.dataMin;
9112 }
9113 min = arrayMax(minArgs);
9114
9115 maxArgs = [min + minRange, pick(options.max, min + minRange)];
9116 if (spaceAvailable) { // if space is availabe, stay within the data range
9117 maxArgs[2] = axis.isLog ? axis.log2lin(axis.dataMax) : axis.dataMax;
9118 }
9119
9120 max = arrayMin(maxArgs);
9121
9122 // now if the max is adjusted, adjust the min back
9123 if (max - min < minRange) {
9124 minArgs[0] = max - minRange;
9125 minArgs[1] = pick(options.min, max - minRange);
9126 min = arrayMax(minArgs);
9127 }
9128 }
9129
9130 // Record modified extremes
9131 axis.min = min;
9132 axis.max = max;
9133 },
9134
9135 /**
9136 * Find the closestPointRange across all series
9137 */
9138 getClosest: function() {
9139 var ret;
9140
9141 if (this.categories) {
9142 ret = 1;
9143 } else {
9144 each(this.series, function(series) {
9145 var seriesClosest = series.closestPointRange,
9146 visible = series.visible ||
9147 !series.chart.options.chart.ignoreHiddenSeries;
9148
9149 if (!series.noSharedTooltip &&
9150 defined(seriesClosest) &&
9151 visible
9152 ) {
9153 ret = defined(ret) ?
9154 Math.min(ret, seriesClosest) :
9155 seriesClosest;
9156 }
9157 });
9158 }
9159 return ret;
9160 },
9161
9162 /**
9163 * When a point name is given and no x, search for the name in the existing categories,
9164 * or if categories aren't provided, search names or create a new category (#2522).
9165 */
9166 nameToX: function(point) {
9167 var explicitCategories = isArray(this.categories),
9168 names = explicitCategories ? this.categories : this.names,
9169 nameX = point.options.x,
9170 x;
9171
9172 point.series.requireSorting = false;
9173
9174 if (!defined(nameX)) {
9175 nameX = this.options.uniqueNames === false ?
9176 point.series.autoIncrement() :
9177 inArray(point.name, names);
9178 }
9179 if (nameX === -1) { // The name is not found in currenct categories
9180 if (!explicitCategories) {
9181 x = names.length;
9182 }
9183 } else {
9184 x = nameX;
9185 }
9186
9187 // Write the last point's name to the names array
9188 this.names[x] = point.name;
9189
9190 return x;
9191 },
9192
9193 /**
9194 * When changes have been done to series data, update the axis.names.
9195 */
9196 updateNames: function() {
9197 var axis = this;
9198
9199 if (this.names.length > 0) {
9200 this.names.length = 0;
9201 this.minRange = undefined;
9202 each(this.series || [], function(series) {
9203
9204 // Reset incrementer (#5928)
9205 series.xIncrement = null;
9206
9207 // When adding a series, points are not yet generated
9208 if (!series.points || series.isDirtyData) {
9209 series.processData();
9210 series.generatePoints();
9211 }
9212
9213 each(series.points, function(point, i) {
9214 var x;
9215 if (point.options && point.options.x === undefined) {
9216 x = axis.nameToX(point);
9217 if (x !== point.x) {
9218 point.x = x;
9219 series.xData[i] = x;
9220 }
9221 }
9222 });
9223 });
9224 }
9225 },
9226
9227 /**
9228 * Update translation information
9229 */
9230 setAxisTranslation: function(saveOld) {
9231 var axis = this,
9232 range = axis.max - axis.min,
9233 pointRange = axis.axisPointRange || 0,
9234 closestPointRange,
9235 minPointOffset = 0,
9236 pointRangePadding = 0,
9237 linkedParent = axis.linkedParent,
9238 ordinalCorrection,
9239 hasCategories = !!axis.categories,
9240 transA = axis.transA,
9241 isXAxis = axis.isXAxis;
9242
9243 // Adjust translation for padding. Y axis with categories need to go through the same (#1784).
9244 if (isXAxis || hasCategories || pointRange) {
9245
9246 // Get the closest points
9247 closestPointRange = axis.getClosest();
9248
9249 if (linkedParent) {
9250 minPointOffset = linkedParent.minPointOffset;
9251 pointRangePadding = linkedParent.pointRangePadding;
9252 } else {
9253 each(axis.series, function(series) {
9254 var seriesPointRange = hasCategories ?
9255 1 :
9256 (isXAxis ?
9257 pick(series.options.pointRange, closestPointRange, 0) :
9258 (axis.axisPointRange || 0)), // #2806
9259 pointPlacement = series.options.pointPlacement;
9260
9261 pointRange = Math.max(pointRange, seriesPointRange);
9262
9263 if (!axis.single) {
9264 // minPointOffset is the value padding to the left of the axis in order to make
9265 // room for points with a pointRange, typically columns. When the pointPlacement option
9266 // is 'between' or 'on', this padding does not apply.
9267 minPointOffset = Math.max(
9268 minPointOffset,
9269 isString(pointPlacement) ? 0 : seriesPointRange / 2
9270 );
9271
9272 // Determine the total padding needed to the length of the axis to make room for the
9273 // pointRange. If the series' pointPlacement is 'on', no padding is added.
9274 pointRangePadding = Math.max(
9275 pointRangePadding,
9276 pointPlacement === 'on' ? 0 : seriesPointRange
9277 );
9278 }
9279 });
9280 }
9281
9282 // Record minPointOffset and pointRangePadding
9283 ordinalCorrection = axis.ordinalSlope && closestPointRange ? axis.ordinalSlope / closestPointRange : 1; // #988, #1853
9284 axis.minPointOffset = minPointOffset = minPointOffset * ordinalCorrection;
9285 axis.pointRangePadding = pointRangePadding = pointRangePadding * ordinalCorrection;
9286
9287 // pointRange means the width reserved for each point, like in a column chart
9288 axis.pointRange = Math.min(pointRange, range);
9289
9290 // closestPointRange means the closest distance between points. In columns
9291 // it is mostly equal to pointRange, but in lines pointRange is 0 while closestPointRange
9292 // is some other value
9293 if (isXAxis) {
9294 axis.closestPointRange = closestPointRange;
9295 }
9296 }
9297
9298 // Secondary values
9299 if (saveOld) {
9300 axis.oldTransA = transA;
9301 }
9302 axis.translationSlope = axis.transA = transA = axis.len / ((range + pointRangePadding) || 1);
9303 axis.transB = axis.horiz ? axis.left : axis.bottom; // translation addend
9304 axis.minPixelPadding = transA * minPointOffset;
9305 },
9306
9307 minFromRange: function() {
9308 return this.max - this.range;
9309 },
9310
9311 /**
9312 * Set the tick positions to round values and optionally extend the extremes
9313 * to the nearest tick
9314 */
9315 setTickInterval: function(secondPass) {
9316 var axis = this,
9317 chart = axis.chart,
9318 options = axis.options,
9319 isLog = axis.isLog,
9320 log2lin = axis.log2lin,
9321 isDatetimeAxis = axis.isDatetimeAxis,
9322 isXAxis = axis.isXAxis,
9323 isLinked = axis.isLinked,
9324 maxPadding = options.maxPadding,
9325 minPadding = options.minPadding,
9326 length,
9327 linkedParentExtremes,
9328 tickIntervalOption = options.tickInterval,
9329 minTickInterval,
9330 tickPixelIntervalOption = options.tickPixelInterval,
9331 categories = axis.categories,
9332 threshold = axis.threshold,
9333 softThreshold = axis.softThreshold,
9334 thresholdMin,
9335 thresholdMax,
9336 hardMin,
9337 hardMax;
9338
9339 if (!isDatetimeAxis && !categories && !isLinked) {
9340 this.getTickAmount();
9341 }
9342
9343 // Min or max set either by zooming/setExtremes or initial options
9344 hardMin = pick(axis.userMin, options.min);
9345 hardMax = pick(axis.userMax, options.max);
9346
9347 // Linked axis gets the extremes from the parent axis
9348 if (isLinked) {
9349 axis.linkedParent = chart[axis.coll][options.linkedTo];
9350 linkedParentExtremes = axis.linkedParent.getExtremes();
9351 axis.min = pick(linkedParentExtremes.min, linkedParentExtremes.dataMin);
9352 axis.max = pick(linkedParentExtremes.max, linkedParentExtremes.dataMax);
9353 if (options.type !== axis.linkedParent.options.type) {
9354 error(11, 1); // Can't link axes of different type
9355 }
9356
9357 // Initial min and max from the extreme data values
9358 } else {
9359
9360 // Adjust to hard threshold
9361 if (!softThreshold && defined(threshold)) {
9362 if (axis.dataMin >= threshold) {
9363 thresholdMin = threshold;
9364 minPadding = 0;
9365 } else if (axis.dataMax <= threshold) {
9366 thresholdMax = threshold;
9367 maxPadding = 0;
9368 }
9369 }
9370
9371 axis.min = pick(hardMin, thresholdMin, axis.dataMin);
9372 axis.max = pick(hardMax, thresholdMax, axis.dataMax);
9373
9374 }
9375
9376 if (isLog) {
9377 if (!secondPass && Math.min(axis.min, pick(axis.dataMin, axis.min)) <= 0) { // #978
9378 error(10, 1); // Can't plot negative values on log axis
9379 }
9380 // The correctFloat cures #934, float errors on full tens. But it
9381 // was too aggressive for #4360 because of conversion back to lin,
9382 // therefore use precision 15.
9383 axis.min = correctFloat(log2lin(axis.min), 15);
9384 axis.max = correctFloat(log2lin(axis.max), 15);
9385 }
9386
9387 // handle zoomed range
9388 if (axis.range && defined(axis.max)) {
9389 axis.userMin = axis.min = hardMin = Math.max(axis.min, axis.minFromRange()); // #618
9390 axis.userMax = hardMax = axis.max;
9391
9392 axis.range = null; // don't use it when running setExtremes
9393 }
9394
9395 // Hook for Highstock Scroller. Consider combining with beforePadding.
9396 fireEvent(axis, 'foundExtremes');
9397
9398 // Hook for adjusting this.min and this.max. Used by bubble series.
9399 if (axis.beforePadding) {
9400 axis.beforePadding();
9401 }
9402
9403 // adjust min and max for the minimum range
9404 axis.adjustForMinRange();
9405
9406 // Pad the values to get clear of the chart's edges. To avoid tickInterval taking the padding
9407 // into account, we do this after computing tick interval (#1337).
9408 if (!categories && !axis.axisPointRange && !axis.usePercentage && !isLinked && defined(axis.min) && defined(axis.max)) {
9409 length = axis.max - axis.min;
9410 if (length) {
9411 if (!defined(hardMin) && minPadding) {
9412 axis.min -= length * minPadding;
9413 }
9414 if (!defined(hardMax) && maxPadding) {
9415 axis.max += length * maxPadding;
9416 }
9417 }
9418 }
9419
9420 // Handle options for floor, ceiling, softMin and softMax
9421 if (isNumber(options.floor)) {
9422 axis.min = Math.max(axis.min, options.floor);
9423 } else if (isNumber(options.softMin)) {
9424 axis.min = Math.min(axis.min, options.softMin);
9425 }
9426 if (isNumber(options.ceiling)) {
9427 axis.max = Math.min(axis.max, options.ceiling);
9428 } else if (isNumber(options.softMax)) {
9429 axis.max = Math.max(axis.max, options.softMax);
9430 }
9431
9432 // When the threshold is soft, adjust the extreme value only if
9433 // the data extreme and the padded extreme land on either side of the threshold. For example,
9434 // a series of [0, 1, 2, 3] would make the yAxis add a tick for -1 because of the
9435 // default minPadding and startOnTick options. This is prevented by the softThreshold
9436 // option.
9437 if (softThreshold && defined(axis.dataMin)) {
9438 threshold = threshold || 0;
9439 if (!defined(hardMin) && axis.min < threshold && axis.dataMin >= threshold) {
9440 axis.min = threshold;
9441 } else if (!defined(hardMax) && axis.max > threshold && axis.dataMax <= threshold) {
9442 axis.max = threshold;
9443 }
9444 }
9445
9446
9447 // get tickInterval
9448 if (axis.min === axis.max || axis.min === undefined || axis.max === undefined) {
9449 axis.tickInterval = 1;
9450 } else if (isLinked && !tickIntervalOption &&
9451 tickPixelIntervalOption === axis.linkedParent.options.tickPixelInterval) {
9452 axis.tickInterval = tickIntervalOption = axis.linkedParent.tickInterval;
9453 } else {
9454 axis.tickInterval = pick(
9455 tickIntervalOption,
9456 this.tickAmount ? ((axis.max - axis.min) / Math.max(this.tickAmount - 1, 1)) : undefined,
9457 categories ? // for categoried axis, 1 is default, for linear axis use tickPix
9458 1 :
9459 // don't let it be more than the data range
9460 (axis.max - axis.min) * tickPixelIntervalOption / Math.max(axis.len, tickPixelIntervalOption)
9461 );
9462 }
9463
9464 // Now we're finished detecting min and max, crop and group series data. This
9465 // is in turn needed in order to find tick positions in ordinal axes.
9466 if (isXAxis && !secondPass) {
9467 each(axis.series, function(series) {
9468 series.processData(axis.min !== axis.oldMin || axis.max !== axis.oldMax);
9469 });
9470 }
9471
9472 // set the translation factor used in translate function
9473 axis.setAxisTranslation(true);
9474
9475 // hook for ordinal axes and radial axes
9476 if (axis.beforeSetTickPositions) {
9477 axis.beforeSetTickPositions();
9478 }
9479
9480 // hook for extensions, used in Highstock ordinal axes
9481 if (axis.postProcessTickInterval) {
9482 axis.tickInterval = axis.postProcessTickInterval(axis.tickInterval);
9483 }
9484
9485 // In column-like charts, don't cramp in more ticks than there are points (#1943, #4184)
9486 if (axis.pointRange && !tickIntervalOption) {
9487 axis.tickInterval = Math.max(axis.pointRange, axis.tickInterval);
9488 }
9489
9490 // Before normalizing the tick interval, handle minimum tick interval. This applies only if tickInterval is not defined.
9491 minTickInterval = pick(options.minTickInterval, axis.isDatetimeAxis && axis.closestPointRange);
9492 if (!tickIntervalOption && axis.tickInterval < minTickInterval) {
9493 axis.tickInterval = minTickInterval;
9494 }
9495
9496 // for linear axes, get magnitude and normalize the interval
9497 if (!isDatetimeAxis && !isLog && !tickIntervalOption) {
9498 axis.tickInterval = normalizeTickInterval(
9499 axis.tickInterval,
9500 null,
9501 getMagnitude(axis.tickInterval),
9502 // If the tick interval is between 0.5 and 5 and the axis max is in the order of
9503 // thousands, chances are we are dealing with years. Don't allow decimals. #3363.
9504 pick(options.allowDecimals, !(axis.tickInterval > 0.5 && axis.tickInterval < 5 && axis.max > 1000 && axis.max < 9999)), !!this.tickAmount
9505 );
9506 }
9507
9508 // Prevent ticks from getting so close that we can't draw the labels
9509 if (!this.tickAmount) {
9510 axis.tickInterval = axis.unsquish();
9511 }
9512
9513 this.setTickPositions();
9514 },
9515
9516 /**
9517 * Now we have computed the normalized tickInterval, get the tick positions
9518 */
9519 setTickPositions: function() {
9520
9521 var options = this.options,
9522 tickPositions,
9523 tickPositionsOption = options.tickPositions,
9524 tickPositioner = options.tickPositioner,
9525 startOnTick = options.startOnTick,
9526 endOnTick = options.endOnTick,
9527 single;
9528
9529 // Set the tickmarkOffset
9530 this.tickmarkOffset = (this.categories && options.tickmarkPlacement === 'between' &&
9531 this.tickInterval === 1) ? 0.5 : 0; // #3202
9532
9533
9534 // get minorTickInterval
9535 this.minorTickInterval = options.minorTickInterval === 'auto' && this.tickInterval ?
9536 this.tickInterval / 5 : options.minorTickInterval;
9537
9538 // Find the tick positions
9539 this.tickPositions = tickPositions = tickPositionsOption && tickPositionsOption.slice(); // Work on a copy (#1565)
9540 if (!tickPositions) {
9541
9542 if (this.isDatetimeAxis) {
9543 tickPositions = this.getTimeTicks(
9544 this.normalizeTimeTickInterval(this.tickInterval, options.units),
9545 this.min,
9546 this.max,
9547 options.startOfWeek,
9548 this.ordinalPositions,
9549 this.closestPointRange,
9550 true
9551 );
9552 } else if (this.isLog) {
9553 tickPositions = this.getLogTickPositions(this.tickInterval, this.min, this.max);
9554 } else {
9555 tickPositions = this.getLinearTickPositions(this.tickInterval, this.min, this.max);
9556 }
9557
9558 // Too dense ticks, keep only the first and last (#4477)
9559 if (tickPositions.length > this.len) {
9560 tickPositions = [tickPositions[0], tickPositions.pop()];
9561 }
9562
9563 this.tickPositions = tickPositions;
9564
9565 // Run the tick positioner callback, that allows modifying auto tick positions.
9566 if (tickPositioner) {
9567 tickPositioner = tickPositioner.apply(this, [this.min, this.max]);
9568 if (tickPositioner) {
9569 this.tickPositions = tickPositions = tickPositioner;
9570 }
9571 }
9572
9573 }
9574
9575 if (!this.isLinked) {
9576
9577 // reset min/max or remove extremes based on start/end on tick
9578 this.trimTicks(tickPositions, startOnTick, endOnTick);
9579
9580 // When there is only one point, or all points have the same value on this axis, then min
9581 // and max are equal and tickPositions.length is 0 or 1. In this case, add some padding
9582 // in order to center the point, but leave it with one tick. #1337.
9583 if (this.min === this.max && defined(this.min) && !this.tickAmount) {
9584 // Substract half a unit (#2619, #2846, #2515, #3390)
9585 single = true;
9586 this.min -= 0.5;
9587 this.max += 0.5;
9588 }
9589 this.single = single;
9590
9591 if (!tickPositionsOption && !tickPositioner) {
9592 this.adjustTickAmount();
9593 }
9594 }
9595 },
9596
9597 /**
9598 * Handle startOnTick and endOnTick by either adapting to padding min/max or rounded min/max
9599 */
9600 trimTicks: function(tickPositions, startOnTick, endOnTick) {
9601 var roundedMin = tickPositions[0],
9602 roundedMax = tickPositions[tickPositions.length - 1],
9603 minPointOffset = this.minPointOffset || 0;
9604
9605 if (startOnTick) {
9606 this.min = roundedMin;
9607 } else {
9608 while (this.min - minPointOffset > tickPositions[0]) {
9609 tickPositions.shift();
9610 }
9611 }
9612
9613 if (endOnTick) {
9614 this.max = roundedMax;
9615 } else {
9616 while (this.max + minPointOffset < tickPositions[tickPositions.length - 1]) {
9617 tickPositions.pop();
9618 }
9619 }
9620
9621 // If no tick are left, set one tick in the middle (#3195)
9622 if (tickPositions.length === 0 && defined(roundedMin)) {
9623 tickPositions.push((roundedMax + roundedMin) / 2);
9624 }
9625 },
9626
9627 /**
9628 * Check if there are multiple axes in the same pane
9629 * @returns {Boolean} There are other axes
9630 */
9631 alignToOthers: function() {
9632 var others = {}, // Whether there is another axis to pair with this one
9633 hasOther,
9634 options = this.options;
9635
9636 if (this.chart.options.chart.alignTicks !== false && options.alignTicks !== false) {
9637 each(this.chart[this.coll], function(axis) {
9638 var otherOptions = axis.options,
9639 horiz = axis.horiz,
9640 key = [
9641 horiz ? otherOptions.left : otherOptions.top,
9642 otherOptions.width,
9643 otherOptions.height,
9644 otherOptions.pane
9645 ].join(',');
9646
9647
9648 if (axis.series.length) { // #4442
9649 if (others[key]) {
9650 hasOther = true; // #4201
9651 } else {
9652 others[key] = 1;
9653 }
9654 }
9655 });
9656 }
9657 return hasOther;
9658 },
9659
9660 /**
9661 * Set the max ticks of either the x and y axis collection
9662 */
9663 getTickAmount: function() {
9664 var options = this.options,
9665 tickAmount = options.tickAmount,
9666 tickPixelInterval = options.tickPixelInterval;
9667
9668 if (!defined(options.tickInterval) && this.len < tickPixelInterval && !this.isRadial &&
9669 !this.isLog && options.startOnTick && options.endOnTick) {
9670 tickAmount = 2;
9671 }
9672
9673 if (!tickAmount && this.alignToOthers()) {
9674 // Add 1 because 4 tick intervals require 5 ticks (including first and last)
9675 tickAmount = Math.ceil(this.len / tickPixelInterval) + 1;
9676 }
9677
9678 // For tick amounts of 2 and 3, compute five ticks and remove the intermediate ones. This
9679 // prevents the axis from adding ticks that are too far away from the data extremes.
9680 if (tickAmount < 4) {
9681 this.finalTickAmt = tickAmount;
9682 tickAmount = 5;
9683 }
9684
9685 this.tickAmount = tickAmount;
9686 },
9687
9688 /**
9689 * When using multiple axes, adjust the number of ticks to match the highest
9690 * number of ticks in that group
9691 */
9692 adjustTickAmount: function() {
9693 var tickInterval = this.tickInterval,
9694 tickPositions = this.tickPositions,
9695 tickAmount = this.tickAmount,
9696 finalTickAmt = this.finalTickAmt,
9697 currentTickAmount = tickPositions && tickPositions.length,
9698 i,
9699 len;
9700
9701 if (currentTickAmount < tickAmount) {
9702 while (tickPositions.length < tickAmount) {
9703 tickPositions.push(correctFloat(
9704 tickPositions[tickPositions.length - 1] + tickInterval
9705 ));
9706 }
9707 this.transA *= (currentTickAmount - 1) / (tickAmount - 1);
9708 this.max = tickPositions[tickPositions.length - 1];
9709
9710 // We have too many ticks, run second pass to try to reduce ticks
9711 } else if (currentTickAmount > tickAmount) {
9712 this.tickInterval *= 2;
9713 this.setTickPositions();
9714 }
9715
9716 // The finalTickAmt property is set in getTickAmount
9717 if (defined(finalTickAmt)) {
9718 i = len = tickPositions.length;
9719 while (i--) {
9720 if (
9721 (finalTickAmt === 3 && i % 2 === 1) || // Remove every other tick
9722 (finalTickAmt <= 2 && i > 0 && i < len - 1) // Remove all but first and last
9723 ) {
9724 tickPositions.splice(i, 1);
9725 }
9726 }
9727 this.finalTickAmt = undefined;
9728 }
9729 },
9730
9731 /**
9732 * Set the scale based on data min and max, user set min and max or options
9733 *
9734 */
9735 setScale: function() {
9736 var axis = this,
9737 isDirtyData,
9738 isDirtyAxisLength;
9739
9740 axis.oldMin = axis.min;
9741 axis.oldMax = axis.max;
9742 axis.oldAxisLength = axis.len;
9743
9744 // set the new axisLength
9745 axis.setAxisSize();
9746 //axisLength = horiz ? axisWidth : axisHeight;
9747 isDirtyAxisLength = axis.len !== axis.oldAxisLength;
9748
9749 // is there new data?
9750 each(axis.series, function(series) {
9751 if (series.isDirtyData || series.isDirty ||
9752 series.xAxis.isDirty) { // when x axis is dirty, we need new data extremes for y as well
9753 isDirtyData = true;
9754 }
9755 });
9756
9757 // do we really need to go through all this?
9758 if (isDirtyAxisLength || isDirtyData || axis.isLinked || axis.forceRedraw ||
9759 axis.userMin !== axis.oldUserMin || axis.userMax !== axis.oldUserMax || axis.alignToOthers()) {
9760
9761 if (axis.resetStacks) {
9762 axis.resetStacks();
9763 }
9764
9765 axis.forceRedraw = false;
9766
9767 // get data extremes if needed
9768 axis.getSeriesExtremes();
9769
9770 // get fixed positions based on tickInterval
9771 axis.setTickInterval();
9772
9773 // record old values to decide whether a rescale is necessary later on (#540)
9774 axis.oldUserMin = axis.userMin;
9775 axis.oldUserMax = axis.userMax;
9776
9777 // Mark as dirty if it is not already set to dirty and extremes have changed. #595.
9778 if (!axis.isDirty) {
9779 axis.isDirty = isDirtyAxisLength || axis.min !== axis.oldMin || axis.max !== axis.oldMax;
9780 }
9781 } else if (axis.cleanStacks) {
9782 axis.cleanStacks();
9783 }
9784 },
9785
9786 /**
9787 * Set the extremes and optionally redraw
9788 * @param {Number} newMin
9789 * @param {Number} newMax
9790 * @param {Boolean} redraw
9791 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
9792 * configuration
9793 * @param {Object} eventArguments
9794 *
9795 */
9796 setExtremes: function(newMin, newMax, redraw, animation, eventArguments) {
9797 var axis = this,
9798 chart = axis.chart;
9799
9800 redraw = pick(redraw, true); // defaults to true
9801
9802 each(axis.series, function(serie) {
9803 delete serie.kdTree;
9804 });
9805
9806 // Extend the arguments with min and max
9807 eventArguments = extend(eventArguments, {
9808 min: newMin,
9809 max: newMax
9810 });
9811
9812 // Fire the event
9813 fireEvent(axis, 'setExtremes', eventArguments, function() { // the default event handler
9814
9815 axis.userMin = newMin;
9816 axis.userMax = newMax;
9817 axis.eventArgs = eventArguments;
9818
9819 if (redraw) {
9820 chart.redraw(animation);
9821 }
9822 });
9823 },
9824
9825 /**
9826 * Overridable method for zooming chart. Pulled out in a separate method to allow overriding
9827 * in stock charts.
9828 */
9829 zoom: function(newMin, newMax) {
9830 var dataMin = this.dataMin,
9831 dataMax = this.dataMax,
9832 options = this.options,
9833 min = Math.min(dataMin, pick(options.min, dataMin)),
9834 max = Math.max(dataMax, pick(options.max, dataMax));
9835
9836 if (newMin !== this.min || newMax !== this.max) { // #5790
9837
9838 // Prevent pinch zooming out of range. Check for defined is for #1946. #1734.
9839 if (!this.allowZoomOutside) {
9840 // #6014, sometimes newMax will be smaller than min (or newMin will be larger than max).
9841 if (defined(dataMin)) {
9842 if (newMin < min) {
9843 newMin = min;
9844 }
9845 if (newMin > max) {
9846 newMin = max;
9847 }
9848 }
9849 if (defined(dataMax)) {
9850 if (newMax < min) {
9851 newMax = min;
9852 }
9853 if (newMax > max) {
9854 newMax = max;
9855 }
9856 }
9857 }
9858
9859 // In full view, displaying the reset zoom button is not required
9860 this.displayBtn = newMin !== undefined || newMax !== undefined;
9861
9862 // Do it
9863 this.setExtremes(
9864 newMin,
9865 newMax,
9866 false,
9867 undefined, {
9868 trigger: 'zoom'
9869 }
9870 );
9871 }
9872
9873 return true;
9874 },
9875
9876 /**
9877 * Update the axis metrics
9878 */
9879 setAxisSize: function() {
9880 var chart = this.chart,
9881 options = this.options,
9882 offsetLeft = options.offsetLeft || 0,
9883 offsetRight = options.offsetRight || 0,
9884 horiz = this.horiz,
9885 width = pick(options.width, chart.plotWidth - offsetLeft + offsetRight),
9886 height = pick(options.height, chart.plotHeight),
9887 top = pick(options.top, chart.plotTop),
9888 left = pick(options.left, chart.plotLeft + offsetLeft),
9889 percentRegex = /%$/;
9890
9891 // Check for percentage based input values. Rounding fixes problems with
9892 // column overflow and plot line filtering (#4898, #4899)
9893 if (percentRegex.test(height)) {
9894 height = Math.round(parseFloat(height) / 100 * chart.plotHeight);
9895 }
9896 if (percentRegex.test(top)) {
9897 top = Math.round(parseFloat(top) / 100 * chart.plotHeight + chart.plotTop);
9898 }
9899
9900 // Expose basic values to use in Series object and navigator
9901 this.left = left;
9902 this.top = top;
9903 this.width = width;
9904 this.height = height;
9905 this.bottom = chart.chartHeight - height - top;
9906 this.right = chart.chartWidth - width - left;
9907
9908 // Direction agnostic properties
9909 this.len = Math.max(horiz ? width : height, 0); // Math.max fixes #905
9910 this.pos = horiz ? left : top; // distance from SVG origin
9911 },
9912
9913 /**
9914 * Get the actual axis extremes
9915 */
9916 getExtremes: function() {
9917 var axis = this,
9918 isLog = axis.isLog,
9919 lin2log = axis.lin2log;
9920
9921 return {
9922 min: isLog ? correctFloat(lin2log(axis.min)) : axis.min,
9923 max: isLog ? correctFloat(lin2log(axis.max)) : axis.max,
9924 dataMin: axis.dataMin,
9925 dataMax: axis.dataMax,
9926 userMin: axis.userMin,
9927 userMax: axis.userMax
9928 };
9929 },
9930
9931 /**
9932 * Get the zero plane either based on zero or on the min or max value.
9933 * Used in bar and area plots
9934 */
9935 getThreshold: function(threshold) {
9936 var axis = this,
9937 isLog = axis.isLog,
9938 lin2log = axis.lin2log,
9939 realMin = isLog ? lin2log(axis.min) : axis.min,
9940 realMax = isLog ? lin2log(axis.max) : axis.max;
9941
9942 if (threshold === null) {
9943 threshold = realMin;
9944 } else if (realMin > threshold) {
9945 threshold = realMin;
9946 } else if (realMax < threshold) {
9947 threshold = realMax;
9948 }
9949
9950 return axis.translate(threshold, 0, 1, 0, 1);
9951 },
9952
9953 /**
9954 * Compute auto alignment for the axis label based on which side the axis is on
9955 * and the given rotation for the label
9956 */
9957 autoLabelAlign: function(rotation) {
9958 var ret,
9959 angle = (pick(rotation, 0) - (this.side * 90) + 720) % 360;
9960
9961 if (angle > 15 && angle < 165) {
9962 ret = 'right';
9963 } else if (angle > 195 && angle < 345) {
9964 ret = 'left';
9965 } else {
9966 ret = 'center';
9967 }
9968 return ret;
9969 },
9970
9971 /**
9972 * Get the tick length and width for the axis.
9973 * @param {String} prefix 'tick' or 'minorTick'
9974 * @returns {Array} An array of tickLength and tickWidth
9975 */
9976 tickSize: function(prefix) {
9977 var options = this.options,
9978 tickLength = options[prefix + 'Length'],
9979 tickWidth = pick(options[prefix + 'Width'], prefix === 'tick' && this.isXAxis ? 1 : 0); // X axis defaults to 1
9980
9981 if (tickWidth && tickLength) {
9982 // Negate the length
9983 if (options[prefix + 'Position'] === 'inside') {
9984 tickLength = -tickLength;
9985 }
9986 return [tickLength, tickWidth];
9987 }
9988
9989 },
9990
9991 /**
9992 * Return the size of the labels
9993 */
9994 labelMetrics: function() {
9995 return this.chart.renderer.fontMetrics(
9996 this.options.labels.style && this.options.labels.style.fontSize,
9997 this.ticks[0] && this.ticks[0].label
9998 );
9999 },
10000
10001 /**
10002 * Prevent the ticks from getting so close we can't draw the labels. On a horizontal
10003 * axis, this is handled by rotating the labels, removing ticks and adding ellipsis.
10004 * On a vertical axis remove ticks and add ellipsis.
10005 */
10006 unsquish: function() {
10007 var labelOptions = this.options.labels,
10008 horiz = this.horiz,
10009 tickInterval = this.tickInterval,
10010 newTickInterval = tickInterval,
10011 slotSize = this.len / (((this.categories ? 1 : 0) + this.max - this.min) / tickInterval),
10012 rotation,
10013 rotationOption = labelOptions.rotation,
10014 labelMetrics = this.labelMetrics(),
10015 step,
10016 bestScore = Number.MAX_VALUE,
10017 autoRotation,
10018 // Return the multiple of tickInterval that is needed to avoid collision
10019 getStep = function(spaceNeeded) {
10020 var step = spaceNeeded / (slotSize || 1);
10021 step = step > 1 ? Math.ceil(step) : 1;
10022 return step * tickInterval;
10023 };
10024
10025 if (horiz) {
10026 autoRotation = !labelOptions.staggerLines && !labelOptions.step && ( // #3971
10027 defined(rotationOption) ? [rotationOption] :
10028 slotSize < pick(labelOptions.autoRotationLimit, 80) && labelOptions.autoRotation
10029 );
10030
10031 if (autoRotation) {
10032
10033 // Loop over the given autoRotation options, and determine which gives the best score. The
10034 // best score is that with the lowest number of steps and a rotation closest to horizontal.
10035 each(autoRotation, function(rot) {
10036 var score;
10037
10038 if (rot === rotationOption || (rot && rot >= -90 && rot <= 90)) { // #3891
10039
10040 step = getStep(Math.abs(labelMetrics.h / Math.sin(deg2rad * rot)));
10041
10042 score = step + Math.abs(rot / 360);
10043
10044 if (score < bestScore) {
10045 bestScore = score;
10046 rotation = rot;
10047 newTickInterval = step;
10048 }
10049 }
10050 });
10051 }
10052
10053 } else if (!labelOptions.step) { // #4411
10054 newTickInterval = getStep(labelMetrics.h);
10055 }
10056
10057 this.autoRotation = autoRotation;
10058 this.labelRotation = pick(rotation, rotationOption);
10059
10060 return newTickInterval;
10061 },
10062
10063 /**
10064 * Get the general slot width for this axis. This may change between the pre-render (from Axis.getOffset)
10065 * and the final tick rendering and placement (#5086).
10066 */
10067 getSlotWidth: function() {
10068 var chart = this.chart,
10069 horiz = this.horiz,
10070 labelOptions = this.options.labels,
10071 slotCount = Math.max(this.tickPositions.length - (this.categories ? 0 : 1), 1),
10072 marginLeft = chart.margin[3];
10073
10074 return (horiz && (labelOptions.step || 0) < 2 && !labelOptions.rotation && // #4415
10075 ((this.staggerLines || 1) * chart.plotWidth) / slotCount) ||
10076 (!horiz && ((marginLeft && (marginLeft - chart.spacing[3])) || chart.chartWidth * 0.33)); // #1580, #1931
10077
10078 },
10079
10080 /**
10081 * Render the axis labels and determine whether ellipsis or rotation need to be applied
10082 */
10083 renderUnsquish: function() {
10084 var chart = this.chart,
10085 renderer = chart.renderer,
10086 tickPositions = this.tickPositions,
10087 ticks = this.ticks,
10088 labelOptions = this.options.labels,
10089 horiz = this.horiz,
10090 slotWidth = this.getSlotWidth(),
10091 innerWidth = Math.max(1, Math.round(slotWidth - 2 * (labelOptions.padding || 5))),
10092 attr = {},
10093 labelMetrics = this.labelMetrics(),
10094 textOverflowOption = labelOptions.style && labelOptions.style.textOverflow,
10095 css,
10096 maxLabelLength = 0,
10097 label,
10098 i,
10099 pos;
10100
10101 // Set rotation option unless it is "auto", like in gauges
10102 if (!isString(labelOptions.rotation)) {
10103 attr.rotation = labelOptions.rotation || 0; // #4443
10104 }
10105
10106 // Get the longest label length
10107 each(tickPositions, function(tick) {
10108 tick = ticks[tick];
10109 if (tick && tick.labelLength > maxLabelLength) {
10110 maxLabelLength = tick.labelLength;
10111 }
10112 });
10113 this.maxLabelLength = maxLabelLength;
10114
10115
10116 // Handle auto rotation on horizontal axis
10117 if (this.autoRotation) {
10118
10119 // Apply rotation only if the label is too wide for the slot, and
10120 // the label is wider than its height.
10121 if (maxLabelLength > innerWidth && maxLabelLength > labelMetrics.h) {
10122 attr.rotation = this.labelRotation;
10123 } else {
10124 this.labelRotation = 0;
10125 }
10126
10127 // Handle word-wrap or ellipsis on vertical axis
10128 } else if (slotWidth) {
10129 // For word-wrap or ellipsis
10130 css = {
10131 width: innerWidth + 'px'
10132 };
10133
10134 if (!textOverflowOption) {
10135 css.textOverflow = 'clip';
10136
10137 // On vertical axis, only allow word wrap if there is room for more lines.
10138 i = tickPositions.length;
10139 while (!horiz && i--) {
10140 pos = tickPositions[i];
10141 label = ticks[pos].label;
10142 if (label) {
10143 // Reset ellipsis in order to get the correct bounding box (#4070)
10144 if (label.styles && label.styles.textOverflow === 'ellipsis') {
10145 label.css({
10146 textOverflow: 'clip'
10147 });
10148
10149 // Set the correct width in order to read the bounding box height (#4678, #5034)
10150 } else if (ticks[pos].labelLength > slotWidth) {
10151 label.css({
10152 width: slotWidth + 'px'
10153 });
10154 }
10155
10156 if (label.getBBox().height > this.len / tickPositions.length - (labelMetrics.h - labelMetrics.f)) {
10157 label.specCss = {
10158 textOverflow: 'ellipsis'
10159 };
10160 }
10161 }
10162 }
10163 }
10164 }
10165
10166
10167 // Add ellipsis if the label length is significantly longer than ideal
10168 if (attr.rotation) {
10169 css = {
10170 width: (maxLabelLength > chart.chartHeight * 0.5 ? chart.chartHeight * 0.33 : chart.chartHeight) + 'px'
10171 };
10172 if (!textOverflowOption) {
10173 css.textOverflow = 'ellipsis';
10174 }
10175 }
10176
10177 // Set the explicit or automatic label alignment
10178 this.labelAlign = labelOptions.align || this.autoLabelAlign(this.labelRotation);
10179 if (this.labelAlign) {
10180 attr.align = this.labelAlign;
10181 }
10182
10183 // Apply general and specific CSS
10184 each(tickPositions, function(pos) {
10185 var tick = ticks[pos],
10186 label = tick && tick.label;
10187 if (label) {
10188 label.attr(attr); // This needs to go before the CSS in old IE (#4502)
10189 if (css) {
10190 label.css(merge(css, label.specCss));
10191 }
10192 delete label.specCss;
10193 tick.rotation = attr.rotation;
10194 }
10195 });
10196
10197 // Note: Why is this not part of getLabelPosition?
10198 this.tickRotCorr = renderer.rotCorr(labelMetrics.b, this.labelRotation || 0, this.side !== 0);
10199 },
10200
10201 /**
10202 * Return true if the axis has associated data
10203 */
10204 hasData: function() {
10205 return this.hasVisibleSeries || (defined(this.min) && defined(this.max) && !!this.tickPositions);
10206 },
10207
10208 /**
10209 * Render the tick labels to a preliminary position to get their sizes
10210 */
10211 getOffset: function() {
10212 var axis = this,
10213 chart = axis.chart,
10214 renderer = chart.renderer,
10215 options = axis.options,
10216 tickPositions = axis.tickPositions,
10217 ticks = axis.ticks,
10218 horiz = axis.horiz,
10219 side = axis.side,
10220 invertedSide = chart.inverted ? [1, 0, 3, 2][side] : side,
10221 hasData,
10222 showAxis,
10223 titleOffset = 0,
10224 titleOffsetOption,
10225 titleMargin = 0,
10226 axisTitleOptions = options.title,
10227 labelOptions = options.labels,
10228 labelOffset = 0, // reset
10229 labelOffsetPadded,
10230 opposite = axis.opposite,
10231 axisOffset = chart.axisOffset,
10232 clipOffset = chart.clipOffset,
10233 clip,
10234 directionFactor = [-1, 1, 1, -1][side],
10235 n,
10236 className = options.className,
10237 textAlign,
10238 axisParent = axis.axisParent, // Used in color axis
10239 lineHeightCorrection,
10240 tickSize = this.tickSize('tick');
10241
10242 // For reuse in Axis.render
10243 hasData = axis.hasData();
10244 axis.showAxis = showAxis = hasData || pick(options.showEmpty, true);
10245
10246 // Set/reset staggerLines
10247 axis.staggerLines = axis.horiz && labelOptions.staggerLines;
10248
10249 // Create the axisGroup and gridGroup elements on first iteration
10250 if (!axis.axisGroup) {
10251 axis.gridGroup = renderer.g('grid')
10252 .attr({
10253 zIndex: options.gridZIndex || 1
10254 })
10255 .addClass('highcharts-' + this.coll.toLowerCase() + '-grid ' + (className || ''))
10256 .add(axisParent);
10257 axis.axisGroup = renderer.g('axis')
10258 .attr({
10259 zIndex: options.zIndex || 2
10260 })
10261 .addClass('highcharts-' + this.coll.toLowerCase() + ' ' + (className || ''))
10262 .add(axisParent);
10263 axis.labelGroup = renderer.g('axis-labels')
10264 .attr({
10265 zIndex: labelOptions.zIndex || 7
10266 })
10267 .addClass('highcharts-' + axis.coll.toLowerCase() + '-labels ' + (className || ''))
10268 .add(axisParent);
10269 }
10270
10271 if (hasData || axis.isLinked) {
10272
10273 // Generate ticks
10274 each(tickPositions, function(pos) {
10275 if (!ticks[pos]) {
10276 ticks[pos] = new Tick(axis, pos);
10277 } else {
10278 ticks[pos].addLabel(); // update labels depending on tick interval
10279 }
10280 });
10281
10282 axis.renderUnsquish();
10283
10284
10285 // Left side must be align: right and right side must have align: left for labels
10286 if (labelOptions.reserveSpace !== false && (side === 0 || side === 2 || {
10287 1: 'left',
10288 3: 'right'
10289 }[side] === axis.labelAlign || axis.labelAlign === 'center')) {
10290 each(tickPositions, function(pos) {
10291
10292 // get the highest offset
10293 labelOffset = Math.max(
10294 ticks[pos].getLabelSize(),
10295 labelOffset
10296 );
10297 });
10298 }
10299
10300 if (axis.staggerLines) {
10301 labelOffset *= axis.staggerLines;
10302 axis.labelOffset = labelOffset * (axis.opposite ? -1 : 1);
10303 }
10304
10305
10306 } else { // doesn't have data
10307 for (n in ticks) {
10308 ticks[n].destroy();
10309 delete ticks[n];
10310 }
10311 }
10312
10313 if (axisTitleOptions && axisTitleOptions.text && axisTitleOptions.enabled !== false) {
10314 if (!axis.axisTitle) {
10315 textAlign = axisTitleOptions.textAlign;
10316 if (!textAlign) {
10317 textAlign = (horiz ? {
10318 low: 'left',
10319 middle: 'center',
10320 high: 'right'
10321 } : {
10322 low: opposite ? 'right' : 'left',
10323 middle: 'center',
10324 high: opposite ? 'left' : 'right'
10325 })[axisTitleOptions.align];
10326 }
10327 axis.axisTitle = renderer.text(
10328 axisTitleOptions.text,
10329 0,
10330 0,
10331 axisTitleOptions.useHTML
10332 )
10333 .attr({
10334 zIndex: 7,
10335 rotation: axisTitleOptions.rotation || 0,
10336 align: textAlign
10337 })
10338 .addClass('highcharts-axis-title')
10339
10340 .css(axisTitleOptions.style)
10341
10342 .add(axis.axisGroup);
10343 axis.axisTitle.isNew = true;
10344 }
10345
10346 if (showAxis) {
10347 titleOffset = axis.axisTitle.getBBox()[horiz ? 'height' : 'width'];
10348 titleOffsetOption = axisTitleOptions.offset;
10349 titleMargin = defined(titleOffsetOption) ? 0 : pick(axisTitleOptions.margin, horiz ? 5 : 10);
10350 }
10351
10352 // hide or show the title depending on whether showEmpty is set
10353 axis.axisTitle[showAxis ? 'show' : 'hide'](true);
10354 }
10355
10356 // Render the axis line
10357 axis.renderLine();
10358
10359 // handle automatic or user set offset
10360 axis.offset = directionFactor * pick(options.offset, axisOffset[side]);
10361
10362 axis.tickRotCorr = axis.tickRotCorr || {
10363 x: 0,
10364 y: 0
10365 }; // polar
10366 if (side === 0) {
10367 lineHeightCorrection = -axis.labelMetrics().h;
10368 } else if (side === 2) {
10369 lineHeightCorrection = axis.tickRotCorr.y;
10370 } else {
10371 lineHeightCorrection = 0;
10372 }
10373
10374 // Find the padded label offset
10375 labelOffsetPadded = Math.abs(labelOffset) + titleMargin;
10376 if (labelOffset) {
10377 labelOffsetPadded -= lineHeightCorrection;
10378 labelOffsetPadded += directionFactor * (horiz ? pick(labelOptions.y, axis.tickRotCorr.y + directionFactor * 8) : labelOptions.x);
10379 }
10380 axis.axisTitleMargin = pick(titleOffsetOption, labelOffsetPadded);
10381
10382 axisOffset[side] = Math.max(
10383 axisOffset[side],
10384 axis.axisTitleMargin + titleOffset + directionFactor * axis.offset,
10385 labelOffsetPadded, // #3027
10386 hasData && tickPositions.length && tickSize ? tickSize[0] : 0 // #4866
10387 );
10388
10389 // Decide the clipping needed to keep the graph inside the plot area and axis lines
10390 clip = options.offset ? 0 : Math.floor(axis.axisLine.strokeWidth() / 2) * 2; // #4308, #4371
10391 clipOffset[invertedSide] = Math.max(clipOffset[invertedSide], clip);
10392 },
10393
10394 /**
10395 * Get the path for the axis line
10396 */
10397 getLinePath: function(lineWidth) {
10398 var chart = this.chart,
10399 opposite = this.opposite,
10400 offset = this.offset,
10401 horiz = this.horiz,
10402 lineLeft = this.left + (opposite ? this.width : 0) + offset,
10403 lineTop = chart.chartHeight - this.bottom - (opposite ? this.height : 0) + offset;
10404
10405 if (opposite) {
10406 lineWidth *= -1; // crispify the other way - #1480, #1687
10407 }
10408
10409 return chart.renderer
10410 .crispLine([
10411 'M',
10412 horiz ?
10413 this.left :
10414 lineLeft,
10415 horiz ?
10416 lineTop :
10417 this.top,
10418 'L',
10419 horiz ?
10420 chart.chartWidth - this.right :
10421 lineLeft,
10422 horiz ?
10423 lineTop :
10424 chart.chartHeight - this.bottom
10425 ], lineWidth);
10426 },
10427
10428 /**
10429 * Render the axis line
10430 */
10431 renderLine: function() {
10432 if (!this.axisLine) {
10433 this.axisLine = this.chart.renderer.path()
10434 .addClass('highcharts-axis-line')
10435 .add(this.axisGroup);
10436
10437
10438 this.axisLine.attr({
10439 stroke: this.options.lineColor,
10440 'stroke-width': this.options.lineWidth,
10441 zIndex: 7
10442 });
10443
10444 }
10445 },
10446
10447 /**
10448 * Position the title
10449 */
10450 getTitlePosition: function() {
10451 // compute anchor points for each of the title align options
10452 var horiz = this.horiz,
10453 axisLeft = this.left,
10454 axisTop = this.top,
10455 axisLength = this.len,
10456 axisTitleOptions = this.options.title,
10457 margin = horiz ? axisLeft : axisTop,
10458 opposite = this.opposite,
10459 offset = this.offset,
10460 xOption = axisTitleOptions.x || 0,
10461 yOption = axisTitleOptions.y || 0,
10462 fontSize = this.chart.renderer.fontMetrics(axisTitleOptions.style && axisTitleOptions.style.fontSize, this.axisTitle).f,
10463
10464 // the position in the length direction of the axis
10465 alongAxis = {
10466 low: margin + (horiz ? 0 : axisLength),
10467 middle: margin + axisLength / 2,
10468 high: margin + (horiz ? axisLength : 0)
10469 }[axisTitleOptions.align],
10470
10471 // the position in the perpendicular direction of the axis
10472 offAxis = (horiz ? axisTop + this.height : axisLeft) +
10473 (horiz ? 1 : -1) * // horizontal axis reverses the margin
10474 (opposite ? -1 : 1) * // so does opposite axes
10475 this.axisTitleMargin +
10476 (this.side === 2 ? fontSize : 0);
10477
10478 return {
10479 x: horiz ?
10480 alongAxis + xOption : offAxis + (opposite ? this.width : 0) + offset + xOption,
10481 y: horiz ?
10482 offAxis + yOption - (opposite ? this.height : 0) + offset : alongAxis + yOption
10483 };
10484 },
10485
10486 /**
10487 * Render the axis
10488 */
10489 render: function() {
10490 var axis = this,
10491 chart = axis.chart,
10492 renderer = chart.renderer,
10493 options = axis.options,
10494 isLog = axis.isLog,
10495 lin2log = axis.lin2log,
10496 isLinked = axis.isLinked,
10497 tickPositions = axis.tickPositions,
10498 axisTitle = axis.axisTitle,
10499 ticks = axis.ticks,
10500 minorTicks = axis.minorTicks,
10501 alternateBands = axis.alternateBands,
10502 stackLabelOptions = options.stackLabels,
10503 alternateGridColor = options.alternateGridColor,
10504 tickmarkOffset = axis.tickmarkOffset,
10505 axisLine = axis.axisLine,
10506 hasRendered = chart.hasRendered,
10507 slideInTicks = hasRendered && isNumber(axis.oldMin),
10508 showAxis = axis.showAxis,
10509 animation = animObject(renderer.globalAnimation),
10510 from,
10511 to;
10512
10513 // Reset
10514 axis.labelEdge.length = 0;
10515 //axis.justifyToPlot = overflow === 'justify';
10516 axis.overlap = false;
10517
10518 // Mark all elements inActive before we go over and mark the active ones
10519 each([ticks, minorTicks, alternateBands], function(coll) {
10520 var pos;
10521 for (pos in coll) {
10522 coll[pos].isActive = false;
10523 }
10524 });
10525
10526 // If the series has data draw the ticks. Else only the line and title
10527 if (axis.hasData() || isLinked) {
10528
10529 // minor ticks
10530 if (axis.minorTickInterval && !axis.categories) {
10531 each(axis.getMinorTickPositions(), function(pos) {
10532 if (!minorTicks[pos]) {
10533 minorTicks[pos] = new Tick(axis, pos, 'minor');
10534 }
10535
10536 // render new ticks in old position
10537 if (slideInTicks && minorTicks[pos].isNew) {
10538 minorTicks[pos].render(null, true);
10539 }
10540
10541 minorTicks[pos].render(null, false, 1);
10542 });
10543 }
10544
10545 // Major ticks. Pull out the first item and render it last so that
10546 // we can get the position of the neighbour label. #808.
10547 if (tickPositions.length) { // #1300
10548 each(tickPositions, function(pos, i) {
10549
10550 // linked axes need an extra check to find out if
10551 if (!isLinked || (pos >= axis.min && pos <= axis.max)) {
10552
10553 if (!ticks[pos]) {
10554 ticks[pos] = new Tick(axis, pos);
10555 }
10556
10557 // render new ticks in old position
10558 if (slideInTicks && ticks[pos].isNew) {
10559 ticks[pos].render(i, true, 0.1);
10560 }
10561
10562 ticks[pos].render(i);
10563 }
10564
10565 });
10566 // In a categorized axis, the tick marks are displayed between labels. So
10567 // we need to add a tick mark and grid line at the left edge of the X axis.
10568 if (tickmarkOffset && (axis.min === 0 || axis.single)) {
10569 if (!ticks[-1]) {
10570 ticks[-1] = new Tick(axis, -1, null, true);
10571 }
10572 ticks[-1].render(-1);
10573 }
10574
10575 }
10576
10577 // alternate grid color
10578 if (alternateGridColor) {
10579 each(tickPositions, function(pos, i) {
10580 to = tickPositions[i + 1] !== undefined ? tickPositions[i + 1] + tickmarkOffset : axis.max - tickmarkOffset;
10581 if (i % 2 === 0 && pos < axis.max && to <= axis.max + (chart.polar ? -tickmarkOffset : tickmarkOffset)) { // #2248, #4660
10582 if (!alternateBands[pos]) {
10583 alternateBands[pos] = new PlotLineOrBand(axis);
10584 }
10585 from = pos + tickmarkOffset; // #949
10586 alternateBands[pos].options = {
10587 from: isLog ? lin2log(from) : from,
10588 to: isLog ? lin2log(to) : to,
10589 color: alternateGridColor
10590 };
10591 alternateBands[pos].render();
10592 alternateBands[pos].isActive = true;
10593 }
10594 });
10595 }
10596
10597 // custom plot lines and bands
10598 if (!axis._addedPlotLB) { // only first time
10599 each((options.plotLines || []).concat(options.plotBands || []), function(plotLineOptions) {
10600 axis.addPlotBandOrLine(plotLineOptions);
10601 });
10602 axis._addedPlotLB = true;
10603 }
10604
10605 } // end if hasData
10606
10607 // Remove inactive ticks
10608 each([ticks, minorTicks, alternateBands], function(coll) {
10609 var pos,
10610 i,
10611 forDestruction = [],
10612 delay = animation.duration,
10613 destroyInactiveItems = function() {
10614 i = forDestruction.length;
10615 while (i--) {
10616 // When resizing rapidly, the same items may be destroyed in different timeouts,
10617 // or the may be reactivated
10618 if (coll[forDestruction[i]] && !coll[forDestruction[i]].isActive) {
10619 coll[forDestruction[i]].destroy();
10620 delete coll[forDestruction[i]];
10621 }
10622 }
10623
10624 };
10625
10626 for (pos in coll) {
10627
10628 if (!coll[pos].isActive) {
10629 // Render to zero opacity
10630 coll[pos].render(pos, false, 0);
10631 coll[pos].isActive = false;
10632 forDestruction.push(pos);
10633 }
10634 }
10635
10636 // When the objects are finished fading out, destroy them
10637 syncTimeout(
10638 destroyInactiveItems,
10639 coll === alternateBands || !chart.hasRendered || !delay ? 0 : delay
10640 );
10641 });
10642
10643 // Set the axis line path
10644 if (axisLine) {
10645 axisLine[axisLine.isPlaced ? 'animate' : 'attr']({
10646 d: this.getLinePath(axisLine.strokeWidth())
10647 });
10648 axisLine.isPlaced = true;
10649
10650 // Show or hide the line depending on options.showEmpty
10651 axisLine[showAxis ? 'show' : 'hide'](true);
10652 }
10653
10654 if (axisTitle && showAxis) {
10655
10656 axisTitle[axisTitle.isNew ? 'attr' : 'animate'](
10657 axis.getTitlePosition()
10658 );
10659 axisTitle.isNew = false;
10660 }
10661
10662 // Stacked totals:
10663 if (stackLabelOptions && stackLabelOptions.enabled) {
10664 axis.renderStackTotals();
10665 }
10666 // End stacked totals
10667
10668 axis.isDirty = false;
10669 },
10670
10671 /**
10672 * Redraw the axis to reflect changes in the data or axis extremes
10673 */
10674 redraw: function() {
10675
10676 if (this.visible) {
10677 // render the axis
10678 this.render();
10679
10680 // move plot lines and bands
10681 each(this.plotLinesAndBands, function(plotLine) {
10682 plotLine.render();
10683 });
10684 }
10685
10686 // mark associated series as dirty and ready for redraw
10687 each(this.series, function(series) {
10688 series.isDirty = true;
10689 });
10690
10691 },
10692
10693 // Properties to survive after destroy, needed for Axis.update (#4317,
10694 // #5773, #5881).
10695 keepProps: ['extKey', 'hcEvents', 'names', 'series', 'userMax', 'userMin'],
10696
10697 /**
10698 * Destroys an Axis instance.
10699 */
10700 destroy: function(keepEvents) {
10701 var axis = this,
10702 stacks = axis.stacks,
10703 stackKey,
10704 plotLinesAndBands = axis.plotLinesAndBands,
10705 i,
10706 n;
10707
10708 // Remove the events
10709 if (!keepEvents) {
10710 removeEvent(axis);
10711 }
10712
10713 // Destroy each stack total
10714 for (stackKey in stacks) {
10715 destroyObjectProperties(stacks[stackKey]);
10716
10717 stacks[stackKey] = null;
10718 }
10719
10720 // Destroy collections
10721 each([axis.ticks, axis.minorTicks, axis.alternateBands], function(coll) {
10722 destroyObjectProperties(coll);
10723 });
10724 if (plotLinesAndBands) {
10725 i = plotLinesAndBands.length;
10726 while (i--) { // #1975
10727 plotLinesAndBands[i].destroy();
10728 }
10729 }
10730
10731 // Destroy local variables
10732 each(['stackTotalGroup', 'axisLine', 'axisTitle', 'axisGroup', 'gridGroup', 'labelGroup', 'cross'], function(prop) {
10733 if (axis[prop]) {
10734 axis[prop] = axis[prop].destroy();
10735 }
10736 });
10737
10738 // Delete all properties and fall back to the prototype.
10739 for (n in axis) {
10740 if (axis.hasOwnProperty(n) && inArray(n, axis.keepProps) === -1) {
10741 delete axis[n];
10742 }
10743 }
10744 },
10745
10746 /**
10747 * Draw the crosshair
10748 *
10749 * @param {Object} e The event arguments from the modified pointer event
10750 * @param {Object} point The Point object
10751 */
10752 drawCrosshair: function(e, point) {
10753
10754 var path,
10755 options = this.crosshair,
10756 snap = pick(options.snap, true),
10757 pos,
10758 categorized,
10759 graphic = this.cross;
10760
10761 // Use last available event when updating non-snapped crosshairs without
10762 // mouse interaction (#5287)
10763 if (!e) {
10764 e = this.cross && this.cross.e;
10765 }
10766
10767 if (
10768 // Disabled in options
10769 !this.crosshair ||
10770 // Snap
10771 ((defined(point) || !snap) === false)
10772 ) {
10773 this.hideCrosshair();
10774 } else {
10775
10776 // Get the path
10777 if (!snap) {
10778 pos = e && (this.horiz ? e.chartX - this.pos : this.len - e.chartY + this.pos);
10779 } else if (defined(point)) {
10780 pos = this.isXAxis ? point.plotX : this.len - point.plotY; // #3834
10781 }
10782
10783 if (defined(pos)) {
10784 path = this.getPlotLinePath(
10785 // First argument, value, only used on radial
10786 point && (this.isXAxis ? point.x : pick(point.stackY, point.y)),
10787 null,
10788 null,
10789 null,
10790 pos // Translated position
10791 ) || null; // #3189
10792 }
10793
10794 if (!defined(path)) {
10795 this.hideCrosshair();
10796 return;
10797 }
10798
10799 categorized = this.categories && !this.isRadial;
10800
10801 // Draw the cross
10802 if (!graphic) {
10803 this.cross = graphic = this.chart.renderer
10804 .path()
10805 .addClass('highcharts-crosshair highcharts-crosshair-' +
10806 (categorized ? 'category ' : 'thin ') + options.className)
10807 .attr({
10808 zIndex: pick(options.zIndex, 2)
10809 })
10810 .add();
10811
10812
10813 // Presentational attributes
10814 graphic.attr({
10815 'stroke': options.color || (categorized ? color('#ccd6eb').setOpacity(0.25).get() : '#cccccc'),
10816 'stroke-width': pick(options.width, 1)
10817 });
10818 if (options.dashStyle) {
10819 graphic.attr({
10820 dashstyle: options.dashStyle
10821 });
10822 }
10823
10824
10825 }
10826
10827 graphic.show().attr({
10828 d: path
10829 });
10830
10831 if (categorized && !options.width) {
10832 graphic.attr({
10833 'stroke-width': this.transA
10834 });
10835 }
10836 this.cross.e = e;
10837 }
10838 },
10839
10840 /**
10841 * Hide the crosshair.
10842 */
10843 hideCrosshair: function() {
10844 if (this.cross) {
10845 this.cross.hide();
10846 }
10847 }
10848 }; // end Axis
10849
10850 extend(H.Axis.prototype, AxisPlotLineOrBandExtension);
10851
10852 }(Highcharts));
10853 (function(H) {
10854 /**
10855 * (c) 2010-2016 Torstein Honsi
10856 *
10857 * License: www.highcharts.com/license
10858 */
10859 'use strict';
10860 var Axis = H.Axis,
10861 Date = H.Date,
10862 dateFormat = H.dateFormat,
10863 defaultOptions = H.defaultOptions,
10864 defined = H.defined,
10865 each = H.each,
10866 extend = H.extend,
10867 getMagnitude = H.getMagnitude,
10868 getTZOffset = H.getTZOffset,
10869 normalizeTickInterval = H.normalizeTickInterval,
10870 pick = H.pick,
10871 timeUnits = H.timeUnits;
10872 /**
10873 * Set the tick positions to a time unit that makes sense, for example
10874 * on the first of each month or on every Monday. Return an array
10875 * with the time positions. Used in datetime axes as well as for grouping
10876 * data on a datetime axis.
10877 *
10878 * @param {Object} normalizedInterval The interval in axis values (ms) and the count
10879 * @param {Number} min The minimum in axis values
10880 * @param {Number} max The maximum in axis values
10881 * @param {Number} startOfWeek
10882 */
10883 Axis.prototype.getTimeTicks = function(normalizedInterval, min, max, startOfWeek) {
10884 var tickPositions = [],
10885 i,
10886 higherRanks = {},
10887 useUTC = defaultOptions.global.useUTC,
10888 minYear, // used in months and years as a basis for Date.UTC()
10889 minDate = new Date(min - getTZOffset(min)),
10890 makeTime = Date.hcMakeTime,
10891 interval = normalizedInterval.unitRange,
10892 count = normalizedInterval.count,
10893 variableDayLength;
10894
10895 if (defined(min)) { // #1300
10896 minDate[Date.hcSetMilliseconds](interval >= timeUnits.second ? 0 : // #3935
10897 count * Math.floor(minDate.getMilliseconds() / count)); // #3652, #3654
10898
10899 if (interval >= timeUnits.second) { // second
10900 minDate[Date.hcSetSeconds](interval >= timeUnits.minute ? 0 : // #3935
10901 count * Math.floor(minDate.getSeconds() / count));
10902 }
10903
10904 if (interval >= timeUnits.minute) { // minute
10905 minDate[Date.hcSetMinutes](interval >= timeUnits.hour ? 0 :
10906 count * Math.floor(minDate[Date.hcGetMinutes]() / count));
10907 }
10908
10909 if (interval >= timeUnits.hour) { // hour
10910 minDate[Date.hcSetHours](interval >= timeUnits.day ? 0 :
10911 count * Math.floor(minDate[Date.hcGetHours]() / count));
10912 }
10913
10914 if (interval >= timeUnits.day) { // day
10915 minDate[Date.hcSetDate](interval >= timeUnits.month ? 1 :
10916 count * Math.floor(minDate[Date.hcGetDate]() / count));
10917 }
10918
10919 if (interval >= timeUnits.month) { // month
10920 minDate[Date.hcSetMonth](interval >= timeUnits.year ? 0 :
10921 count * Math.floor(minDate[Date.hcGetMonth]() / count));
10922 minYear = minDate[Date.hcGetFullYear]();
10923 }
10924
10925 if (interval >= timeUnits.year) { // year
10926 minYear -= minYear % count;
10927 minDate[Date.hcSetFullYear](minYear);
10928 }
10929
10930 // week is a special case that runs outside the hierarchy
10931 if (interval === timeUnits.week) {
10932 // get start of current week, independent of count
10933 minDate[Date.hcSetDate](minDate[Date.hcGetDate]() - minDate[Date.hcGetDay]() +
10934 pick(startOfWeek, 1));
10935 }
10936
10937
10938 // Get basics for variable time spans
10939 minYear = minDate[Date.hcGetFullYear]();
10940 var minMonth = minDate[Date.hcGetMonth](),
10941 minDateDate = minDate[Date.hcGetDate](),
10942 minHours = minDate[Date.hcGetHours]();
10943
10944
10945 // Handle local timezone offset
10946 if (Date.hcTimezoneOffset || Date.hcGetTimezoneOffset) {
10947
10948 // Detect whether we need to take the DST crossover into
10949 // consideration. If we're crossing over DST, the day length may be
10950 // 23h or 25h and we need to compute the exact clock time for each
10951 // tick instead of just adding hours. This comes at a cost, so first
10952 // we found out if it is needed. #4951.
10953 variableDayLength =
10954 (!useUTC || !!Date.hcGetTimezoneOffset) &&
10955 (
10956 // Long range, assume we're crossing over.
10957 max - min > 4 * timeUnits.month ||
10958 // Short range, check if min and max are in different time
10959 // zones.
10960 getTZOffset(min) !== getTZOffset(max)
10961 );
10962
10963 // Adjust minDate to the offset date
10964 minDate = minDate.getTime();
10965 minDate = new Date(minDate + getTZOffset(minDate));
10966 }
10967
10968
10969 // Iterate and add tick positions at appropriate values
10970 var time = minDate.getTime();
10971 i = 1;
10972 while (time < max) {
10973 tickPositions.push(time);
10974
10975 // if the interval is years, use Date.UTC to increase years
10976 if (interval === timeUnits.year) {
10977 time = makeTime(minYear + i * count, 0);
10978
10979 // if the interval is months, use Date.UTC to increase months
10980 } else if (interval === timeUnits.month) {
10981 time = makeTime(minYear, minMonth + i * count);
10982
10983 // if we're using global time, the interval is not fixed as it jumps
10984 // one hour at the DST crossover
10985 } else if (variableDayLength && (interval === timeUnits.day || interval === timeUnits.week)) {
10986 time = makeTime(minYear, minMonth, minDateDate +
10987 i * count * (interval === timeUnits.day ? 1 : 7));
10988
10989 } else if (variableDayLength && interval === timeUnits.hour) {
10990 time = makeTime(minYear, minMonth, minDateDate, minHours + i * count);
10991
10992 // else, the interval is fixed and we use simple addition
10993 } else {
10994 time += interval * count;
10995 }
10996
10997 i++;
10998 }
10999
11000 // push the last time
11001 tickPositions.push(time);
11002
11003
11004 // Handle higher ranks. Mark new days if the time is on midnight
11005 // (#950, #1649, #1760, #3349)
11006 if (interval <= timeUnits.hour) {
11007 each(tickPositions, function(time) {
11008 if (dateFormat('%H%M%S%L', time) === '000000000') {
11009 higherRanks[time] = 'day';
11010 }
11011 });
11012 }
11013 }
11014
11015
11016 // record information on the chosen unit - for dynamic label formatter
11017 tickPositions.info = extend(normalizedInterval, {
11018 higherRanks: higherRanks,
11019 totalRange: interval * count
11020 });
11021
11022 return tickPositions;
11023 };
11024
11025 /**
11026 * Get a normalized tick interval for dates. Returns a configuration object with
11027 * unit range (interval), count and name. Used to prepare data for getTimeTicks.
11028 * Previously this logic was part of getTimeTicks, but as getTimeTicks now runs
11029 * of segments in stock charts, the normalizing logic was extracted in order to
11030 * prevent it for running over again for each segment having the same interval.
11031 * #662, #697.
11032 */
11033 Axis.prototype.normalizeTimeTickInterval = function(tickInterval, unitsOption) {
11034 var units = unitsOption || [
11035 [
11036 'millisecond', // unit name
11037 [1, 2, 5, 10, 20, 25, 50, 100, 200, 500] // allowed multiples
11038 ],
11039 [
11040 'second', [1, 2, 5, 10, 15, 30]
11041 ],
11042 [
11043 'minute', [1, 2, 5, 10, 15, 30]
11044 ],
11045 [
11046 'hour', [1, 2, 3, 4, 6, 8, 12]
11047 ],
11048 [
11049 'day', [1, 2]
11050 ],
11051 [
11052 'week', [1, 2]
11053 ],
11054 [
11055 'month', [1, 2, 3, 4, 6]
11056 ],
11057 [
11058 'year',
11059 null
11060 ]
11061 ],
11062 unit = units[units.length - 1], // default unit is years
11063 interval = timeUnits[unit[0]],
11064 multiples = unit[1],
11065 count,
11066 i;
11067
11068 // loop through the units to find the one that best fits the tickInterval
11069 for (i = 0; i < units.length; i++) {
11070 unit = units[i];
11071 interval = timeUnits[unit[0]];
11072 multiples = unit[1];
11073
11074
11075 if (units[i + 1]) {
11076 // lessThan is in the middle between the highest multiple and the next unit.
11077 var lessThan = (interval * multiples[multiples.length - 1] +
11078 timeUnits[units[i + 1][0]]) / 2;
11079
11080 // break and keep the current unit
11081 if (tickInterval <= lessThan) {
11082 break;
11083 }
11084 }
11085 }
11086
11087 // prevent 2.5 years intervals, though 25, 250 etc. are allowed
11088 if (interval === timeUnits.year && tickInterval < 5 * interval) {
11089 multiples = [1, 2, 5];
11090 }
11091
11092 // get the count
11093 count = normalizeTickInterval(
11094 tickInterval / interval,
11095 multiples,
11096 unit[0] === 'year' ? Math.max(getMagnitude(tickInterval / interval), 1) : 1 // #1913, #2360
11097 );
11098
11099 return {
11100 unitRange: interval,
11101 count: count,
11102 unitName: unit[0]
11103 };
11104 };
11105
11106 }(Highcharts));
11107 (function(H) {
11108 /**
11109 * (c) 2010-2016 Torstein Honsi
11110 *
11111 * License: www.highcharts.com/license
11112 */
11113 'use strict';
11114 var Axis = H.Axis,
11115 getMagnitude = H.getMagnitude,
11116 map = H.map,
11117 normalizeTickInterval = H.normalizeTickInterval,
11118 pick = H.pick;
11119 /**
11120 * Methods defined on the Axis prototype
11121 */
11122
11123 /**
11124 * Set the tick positions of a logarithmic axis
11125 */
11126 Axis.prototype.getLogTickPositions = function(interval, min, max, minor) {
11127 var axis = this,
11128 options = axis.options,
11129 axisLength = axis.len,
11130 lin2log = axis.lin2log,
11131 log2lin = axis.log2lin,
11132 // Since we use this method for both major and minor ticks,
11133 // use a local variable and return the result
11134 positions = [];
11135
11136 // Reset
11137 if (!minor) {
11138 axis._minorAutoInterval = null;
11139 }
11140
11141 // First case: All ticks fall on whole logarithms: 1, 10, 100 etc.
11142 if (interval >= 0.5) {
11143 interval = Math.round(interval);
11144 positions = axis.getLinearTickPositions(interval, min, max);
11145
11146 // Second case: We need intermediary ticks. For example
11147 // 1, 2, 4, 6, 8, 10, 20, 40 etc.
11148 } else if (interval >= 0.08) {
11149 var roundedMin = Math.floor(min),
11150 intermediate,
11151 i,
11152 j,
11153 len,
11154 pos,
11155 lastPos,
11156 break2;
11157
11158 if (interval > 0.3) {
11159 intermediate = [1, 2, 4];
11160 } else if (interval > 0.15) { // 0.2 equals five minor ticks per 1, 10, 100 etc
11161 intermediate = [1, 2, 4, 6, 8];
11162 } else { // 0.1 equals ten minor ticks per 1, 10, 100 etc
11163 intermediate = [1, 2, 3, 4, 5, 6, 7, 8, 9];
11164 }
11165
11166 for (i = roundedMin; i < max + 1 && !break2; i++) {
11167 len = intermediate.length;
11168 for (j = 0; j < len && !break2; j++) {
11169 pos = log2lin(lin2log(i) * intermediate[j]);
11170 if (pos > min && (!minor || lastPos <= max) && lastPos !== undefined) { // #1670, lastPos is #3113
11171 positions.push(lastPos);
11172 }
11173
11174 if (lastPos > max) {
11175 break2 = true;
11176 }
11177 lastPos = pos;
11178 }
11179 }
11180
11181 // Third case: We are so deep in between whole logarithmic values that
11182 // we might as well handle the tick positions like a linear axis. For
11183 // example 1.01, 1.02, 1.03, 1.04.
11184 } else {
11185 var realMin = lin2log(min),
11186 realMax = lin2log(max),
11187 tickIntervalOption = options[minor ? 'minorTickInterval' : 'tickInterval'],
11188 filteredTickIntervalOption = tickIntervalOption === 'auto' ? null : tickIntervalOption,
11189 tickPixelIntervalOption = options.tickPixelInterval / (minor ? 5 : 1),
11190 totalPixelLength = minor ? axisLength / axis.tickPositions.length : axisLength;
11191
11192 interval = pick(
11193 filteredTickIntervalOption,
11194 axis._minorAutoInterval,
11195 (realMax - realMin) * tickPixelIntervalOption / (totalPixelLength || 1)
11196 );
11197
11198 interval = normalizeTickInterval(
11199 interval,
11200 null,
11201 getMagnitude(interval)
11202 );
11203
11204 positions = map(axis.getLinearTickPositions(
11205 interval,
11206 realMin,
11207 realMax
11208 ), log2lin);
11209
11210 if (!minor) {
11211 axis._minorAutoInterval = interval / 5;
11212 }
11213 }
11214
11215 // Set the axis-level tickInterval variable
11216 if (!minor) {
11217 axis.tickInterval = interval;
11218 }
11219 return positions;
11220 };
11221
11222 Axis.prototype.log2lin = function(num) {
11223 return Math.log(num) / Math.LN10;
11224 };
11225
11226 Axis.prototype.lin2log = function(num) {
11227 return Math.pow(10, num);
11228 };
11229
11230 }(Highcharts));
11231 (function(H) {
11232 /**
11233 * (c) 2010-2016 Torstein Honsi
11234 *
11235 * License: www.highcharts.com/license
11236 */
11237 'use strict';
11238 var dateFormat = H.dateFormat,
11239 each = H.each,
11240 extend = H.extend,
11241 format = H.format,
11242 isNumber = H.isNumber,
11243 map = H.map,
11244 merge = H.merge,
11245 pick = H.pick,
11246 splat = H.splat,
11247 syncTimeout = H.syncTimeout,
11248 timeUnits = H.timeUnits;
11249 /**
11250 * The tooltip object
11251 * @param {Object} chart The chart instance
11252 * @param {Object} options Tooltip options
11253 */
11254 H.Tooltip = function() {
11255 this.init.apply(this, arguments);
11256 };
11257
11258 H.Tooltip.prototype = {
11259
11260 init: function(chart, options) {
11261
11262 // Save the chart and options
11263 this.chart = chart;
11264 this.options = options;
11265
11266 // Keep track of the current series
11267 //this.currentSeries = undefined;
11268
11269 // List of crosshairs
11270 this.crosshairs = [];
11271
11272 // Current values of x and y when animating
11273 this.now = {
11274 x: 0,
11275 y: 0
11276 };
11277
11278 // The tooltip is initially hidden
11279 this.isHidden = true;
11280
11281
11282
11283 // Public property for getting the shared state.
11284 this.split = options.split && !chart.inverted;
11285 this.shared = options.shared || this.split;
11286
11287 },
11288
11289 /**
11290 * Destroy the single tooltips in a split tooltip.
11291 * If the tooltip is active then it is not destroyed, unless forced to.
11292 * @param {boolean} force Force destroy all tooltips.
11293 * @return {undefined}
11294 */
11295 cleanSplit: function(force) {
11296 each(this.chart.series, function(series) {
11297 var tt = series && series.tt;
11298 if (tt) {
11299 if (!tt.isActive || force) {
11300 series.tt = tt.destroy();
11301 } else {
11302 tt.isActive = false;
11303 }
11304 }
11305 });
11306 },
11307
11308
11309
11310
11311 /**
11312 * Create the Tooltip label element if it doesn't exist, then return the
11313 * label.
11314 */
11315 getLabel: function() {
11316
11317 var renderer = this.chart.renderer,
11318 options = this.options;
11319
11320 if (!this.label) {
11321 // Create the label
11322 if (this.split) {
11323 this.label = renderer.g('tooltip');
11324 } else {
11325 this.label = renderer.label(
11326 '',
11327 0,
11328 0,
11329 options.shape || 'callout',
11330 null,
11331 null,
11332 options.useHTML,
11333 null,
11334 'tooltip'
11335 )
11336 .attr({
11337 padding: options.padding,
11338 r: options.borderRadius
11339 });
11340
11341
11342 this.label
11343 .attr({
11344 'fill': options.backgroundColor,
11345 'stroke-width': options.borderWidth
11346 })
11347 // #2301, #2657
11348 .css(options.style)
11349 .shadow(options.shadow);
11350
11351 }
11352
11353
11354
11355 this.label
11356 .attr({
11357 zIndex: 8
11358 })
11359 .add();
11360 }
11361 return this.label;
11362 },
11363
11364 update: function(options) {
11365 this.destroy();
11366 this.init(this.chart, merge(true, this.options, options));
11367 },
11368
11369 /**
11370 * Destroy the tooltip and its elements.
11371 */
11372 destroy: function() {
11373 // Destroy and clear local variables
11374 if (this.label) {
11375 this.label = this.label.destroy();
11376 }
11377 if (this.split && this.tt) {
11378 this.cleanSplit(this.chart, true);
11379 this.tt = this.tt.destroy();
11380 }
11381 clearTimeout(this.hideTimer);
11382 clearTimeout(this.tooltipTimeout);
11383 },
11384
11385 /**
11386 * Provide a soft movement for the tooltip
11387 *
11388 * @param {Number} x
11389 * @param {Number} y
11390 * @private
11391 */
11392 move: function(x, y, anchorX, anchorY) {
11393 var tooltip = this,
11394 now = tooltip.now,
11395 animate = tooltip.options.animation !== false && !tooltip.isHidden &&
11396 // When we get close to the target position, abort animation and land on the right place (#3056)
11397 (Math.abs(x - now.x) > 1 || Math.abs(y - now.y) > 1),
11398 skipAnchor = tooltip.followPointer || tooltip.len > 1;
11399
11400 // Get intermediate values for animation
11401 extend(now, {
11402 x: animate ? (2 * now.x + x) / 3 : x,
11403 y: animate ? (now.y + y) / 2 : y,
11404 anchorX: skipAnchor ? undefined : animate ? (2 * now.anchorX + anchorX) / 3 : anchorX,
11405 anchorY: skipAnchor ? undefined : animate ? (now.anchorY + anchorY) / 2 : anchorY
11406 });
11407
11408 // Move to the intermediate value
11409 tooltip.getLabel().attr(now);
11410
11411
11412 // Run on next tick of the mouse tracker
11413 if (animate) {
11414
11415 // Never allow two timeouts
11416 clearTimeout(this.tooltipTimeout);
11417
11418 // Set the fixed interval ticking for the smooth tooltip
11419 this.tooltipTimeout = setTimeout(function() {
11420 // The interval function may still be running during destroy,
11421 // so check that the chart is really there before calling.
11422 if (tooltip) {
11423 tooltip.move(x, y, anchorX, anchorY);
11424 }
11425 }, 32);
11426
11427 }
11428 },
11429
11430 /**
11431 * Hide the tooltip
11432 */
11433 hide: function(delay) {
11434 var tooltip = this;
11435 clearTimeout(this.hideTimer); // disallow duplicate timers (#1728, #1766)
11436 delay = pick(delay, this.options.hideDelay, 500);
11437 if (!this.isHidden) {
11438 this.hideTimer = syncTimeout(function() {
11439 tooltip.getLabel()[delay ? 'fadeOut' : 'hide']();
11440 tooltip.isHidden = true;
11441 }, delay);
11442 }
11443 },
11444
11445 /**
11446 * Extendable method to get the anchor position of the tooltip
11447 * from a point or set of points
11448 */
11449 getAnchor: function(points, mouseEvent) {
11450 var ret,
11451 chart = this.chart,
11452 inverted = chart.inverted,
11453 plotTop = chart.plotTop,
11454 plotLeft = chart.plotLeft,
11455 plotX = 0,
11456 plotY = 0,
11457 yAxis,
11458 xAxis;
11459
11460 points = splat(points);
11461
11462 // Pie uses a special tooltipPos
11463 ret = points[0].tooltipPos;
11464
11465 // When tooltip follows mouse, relate the position to the mouse
11466 if (this.followPointer && mouseEvent) {
11467 if (mouseEvent.chartX === undefined) {
11468 mouseEvent = chart.pointer.normalize(mouseEvent);
11469 }
11470 ret = [
11471 mouseEvent.chartX - chart.plotLeft,
11472 mouseEvent.chartY - plotTop
11473 ];
11474 }
11475 // When shared, use the average position
11476 if (!ret) {
11477 each(points, function(point) {
11478 yAxis = point.series.yAxis;
11479 xAxis = point.series.xAxis;
11480 plotX += point.plotX + (!inverted && xAxis ? xAxis.left - plotLeft : 0);
11481 plotY += (point.plotLow ? (point.plotLow + point.plotHigh) / 2 : point.plotY) +
11482 (!inverted && yAxis ? yAxis.top - plotTop : 0); // #1151
11483 });
11484
11485 plotX /= points.length;
11486 plotY /= points.length;
11487
11488 ret = [
11489 inverted ? chart.plotWidth - plotY : plotX,
11490 this.shared && !inverted && points.length > 1 && mouseEvent ?
11491 mouseEvent.chartY - plotTop : // place shared tooltip next to the mouse (#424)
11492 inverted ? chart.plotHeight - plotX : plotY
11493 ];
11494 }
11495
11496 return map(ret, Math.round);
11497 },
11498
11499 /**
11500 * Place the tooltip in a chart without spilling over
11501 * and not covering the point it self.
11502 */
11503 getPosition: function(boxWidth, boxHeight, point) {
11504
11505 var chart = this.chart,
11506 distance = this.distance,
11507 ret = {},
11508 h = point.h || 0, // #4117
11509 swapped,
11510 first = ['y', chart.chartHeight, boxHeight,
11511 point.plotY + chart.plotTop, chart.plotTop,
11512 chart.plotTop + chart.plotHeight
11513 ],
11514 second = ['x', chart.chartWidth, boxWidth,
11515 point.plotX + chart.plotLeft, chart.plotLeft,
11516 chart.plotLeft + chart.plotWidth
11517 ],
11518 // The far side is right or bottom
11519 preferFarSide = !this.followPointer && pick(point.ttBelow, !chart.inverted === !!point.negative), // #4984
11520 /**
11521 * Handle the preferred dimension. When the preferred dimension is tooltip
11522 * on top or bottom of the point, it will look for space there.
11523 */
11524 firstDimension = function(dim, outerSize, innerSize, point, min, max) {
11525 var roomLeft = innerSize < point - distance,
11526 roomRight = point + distance + innerSize < outerSize,
11527 alignedLeft = point - distance - innerSize,
11528 alignedRight = point + distance;
11529
11530 if (preferFarSide && roomRight) {
11531 ret[dim] = alignedRight;
11532 } else if (!preferFarSide && roomLeft) {
11533 ret[dim] = alignedLeft;
11534 } else if (roomLeft) {
11535 ret[dim] = Math.min(max - innerSize, alignedLeft - h < 0 ? alignedLeft : alignedLeft - h);
11536 } else if (roomRight) {
11537 ret[dim] = Math.max(
11538 min,
11539 alignedRight + h + innerSize > outerSize ?
11540 alignedRight :
11541 alignedRight + h
11542 );
11543 } else {
11544 return false;
11545 }
11546 },
11547 /**
11548 * Handle the secondary dimension. If the preferred dimension is tooltip
11549 * on top or bottom of the point, the second dimension is to align the tooltip
11550 * above the point, trying to align center but allowing left or right
11551 * align within the chart box.
11552 */
11553 secondDimension = function(dim, outerSize, innerSize, point) {
11554 var retVal;
11555
11556 // Too close to the edge, return false and swap dimensions
11557 if (point < distance || point > outerSize - distance) {
11558 retVal = false;
11559 // Align left/top
11560 } else if (point < innerSize / 2) {
11561 ret[dim] = 1;
11562 // Align right/bottom
11563 } else if (point > outerSize - innerSize / 2) {
11564 ret[dim] = outerSize - innerSize - 2;
11565 // Align center
11566 } else {
11567 ret[dim] = point - innerSize / 2;
11568 }
11569 return retVal;
11570 },
11571 /**
11572 * Swap the dimensions
11573 */
11574 swap = function(count) {
11575 var temp = first;
11576 first = second;
11577 second = temp;
11578 swapped = count;
11579 },
11580 run = function() {
11581 if (firstDimension.apply(0, first) !== false) {
11582 if (secondDimension.apply(0, second) === false && !swapped) {
11583 swap(true);
11584 run();
11585 }
11586 } else if (!swapped) {
11587 swap(true);
11588 run();
11589 } else {
11590 ret.x = ret.y = 0;
11591 }
11592 };
11593
11594 // Under these conditions, prefer the tooltip on the side of the point
11595 if (chart.inverted || this.len > 1) {
11596 swap();
11597 }
11598 run();
11599
11600 return ret;
11601
11602 },
11603
11604 /**
11605 * In case no user defined formatter is given, this will be used. Note that the context
11606 * here is an object holding point, series, x, y etc.
11607 *
11608 * @returns {String|Array<String>}
11609 */
11610 defaultFormatter: function(tooltip) {
11611 var items = this.points || splat(this),
11612 s;
11613
11614 // Build the header
11615 s = [tooltip.tooltipFooterHeaderFormatter(items[0])];
11616
11617 // build the values
11618 s = s.concat(tooltip.bodyFormatter(items));
11619
11620 // footer
11621 s.push(tooltip.tooltipFooterHeaderFormatter(items[0], true));
11622
11623 return s;
11624 },
11625
11626 /**
11627 * Refresh the tooltip's text and position.
11628 * @param {Object} point
11629 */
11630 refresh: function(point, mouseEvent) {
11631 var tooltip = this,
11632 chart = tooltip.chart,
11633 label,
11634 options = tooltip.options,
11635 x,
11636 y,
11637 anchor,
11638 textConfig = {},
11639 text,
11640 pointConfig = [],
11641 formatter = options.formatter || tooltip.defaultFormatter,
11642 hoverPoints = chart.hoverPoints,
11643 shared = tooltip.shared,
11644 currentSeries;
11645
11646 clearTimeout(this.hideTimer);
11647
11648 // get the reference point coordinates (pie charts use tooltipPos)
11649 tooltip.followPointer = splat(point)[0].series.tooltipOptions.followPointer;
11650 anchor = tooltip.getAnchor(point, mouseEvent);
11651 x = anchor[0];
11652 y = anchor[1];
11653
11654 // shared tooltip, array is sent over
11655 if (shared && !(point.series && point.series.noSharedTooltip)) {
11656
11657 // hide previous hoverPoints and set new
11658
11659 chart.hoverPoints = point;
11660 if (hoverPoints) {
11661 each(hoverPoints, function(point) {
11662 point.setState();
11663 });
11664 }
11665
11666 each(point, function(item) {
11667 item.setState('hover');
11668
11669 pointConfig.push(item.getLabelConfig());
11670 });
11671
11672 textConfig = {
11673 x: point[0].category,
11674 y: point[0].y
11675 };
11676 textConfig.points = pointConfig;
11677 this.len = pointConfig.length;
11678 point = point[0];
11679
11680 // single point tooltip
11681 } else {
11682 textConfig = point.getLabelConfig();
11683 }
11684 text = formatter.call(textConfig, tooltip);
11685
11686 // register the current series
11687 currentSeries = point.series;
11688 this.distance = pick(currentSeries.tooltipOptions.distance, 16);
11689
11690 // update the inner HTML
11691 if (text === false) {
11692 this.hide();
11693 } else {
11694
11695 label = tooltip.getLabel();
11696
11697 // show it
11698 if (tooltip.isHidden) {
11699 label.attr({
11700 opacity: 1
11701 }).show();
11702 }
11703
11704 // update text
11705 if (tooltip.split) {
11706 this.renderSplit(text, chart.hoverPoints);
11707 } else {
11708 label.attr({
11709 text: text && text.join ? text.join('') : text
11710 });
11711
11712 // Set the stroke color of the box to reflect the point
11713 label.removeClass(/highcharts-color-[\d]+/g)
11714 .addClass('highcharts-color-' + pick(point.colorIndex, currentSeries.colorIndex));
11715
11716
11717 label.attr({
11718 stroke: options.borderColor || point.color || currentSeries.color || '#666666'
11719 });
11720
11721
11722 tooltip.updatePosition({
11723 plotX: x,
11724 plotY: y,
11725 negative: point.negative,
11726 ttBelow: point.ttBelow,
11727 h: anchor[2] || 0
11728 });
11729 }
11730
11731 this.isHidden = false;
11732 }
11733 },
11734
11735 /**
11736 * Render the split tooltip. Loops over each point's text and adds
11737 * a label next to the point, then uses the distribute function to
11738 * find best non-overlapping positions.
11739 */
11740 renderSplit: function(labels, points) {
11741 var tooltip = this,
11742 boxes = [],
11743 chart = this.chart,
11744 ren = chart.renderer,
11745 rightAligned = true,
11746 options = this.options,
11747 headerHeight,
11748 tooltipLabel = this.getLabel();
11749
11750 // Create the individual labels
11751 each(labels.slice(0, labels.length - 1), function(str, i) {
11752 var point = points[i - 1] ||
11753 // Item 0 is the header. Instead of this, we could also use the crosshair label
11754 {
11755 isHeader: true,
11756 plotX: points[0].plotX
11757 },
11758 owner = point.series || tooltip,
11759 tt = owner.tt,
11760 series = point.series || {},
11761 colorClass = 'highcharts-color-' + pick(point.colorIndex, series.colorIndex, 'none'),
11762 target,
11763 x,
11764 bBox,
11765 boxWidth;
11766
11767 // Store the tooltip referance on the series
11768 if (!tt) {
11769 owner.tt = tt = ren.label(null, null, null, 'callout')
11770 .addClass('highcharts-tooltip-box ' + colorClass)
11771 .attr({
11772 'padding': options.padding,
11773 'r': options.borderRadius,
11774
11775 'fill': options.backgroundColor,
11776 'stroke': point.color || series.color || '#333333',
11777 'stroke-width': options.borderWidth
11778
11779 })
11780 .add(tooltipLabel);
11781 }
11782
11783 tt.isActive = true;
11784 tt.attr({
11785 text: str
11786 });
11787
11788 tt.css(options.style);
11789
11790
11791 // Get X position now, so we can move all to the other side in case of overflow
11792 bBox = tt.getBBox();
11793 boxWidth = bBox.width + tt.strokeWidth();
11794 if (point.isHeader) {
11795 headerHeight = bBox.height;
11796 x = Math.max(
11797 0, // No left overflow
11798 Math.min(
11799 point.plotX + chart.plotLeft - boxWidth / 2,
11800 chart.chartWidth - boxWidth // No right overflow (#5794)
11801 )
11802 );
11803 } else {
11804 x = point.plotX + chart.plotLeft - pick(options.distance, 16) -
11805 boxWidth;
11806 }
11807
11808
11809 // If overflow left, we don't use this x in the next loop
11810 if (x < 0) {
11811 rightAligned = false;
11812 }
11813
11814 // Prepare for distribution
11815 target = (point.series && point.series.yAxis && point.series.yAxis.pos) + (point.plotY || 0);
11816 target -= chart.plotTop;
11817 boxes.push({
11818 target: point.isHeader ? chart.plotHeight + headerHeight : target,
11819 rank: point.isHeader ? 1 : 0,
11820 size: owner.tt.getBBox().height + 1,
11821 point: point,
11822 x: x,
11823 tt: tt
11824 });
11825 });
11826
11827 // Clean previous run (for missing points)
11828 this.cleanSplit();
11829
11830 // Distribute and put in place
11831 H.distribute(boxes, chart.plotHeight + headerHeight);
11832 each(boxes, function(box) {
11833 var point = box.point;
11834
11835 // Put the label in place
11836 box.tt.attr({
11837 visibility: box.pos === undefined ? 'hidden' : 'inherit',
11838 x: (rightAligned || point.isHeader ?
11839 box.x :
11840 point.plotX + chart.plotLeft + pick(options.distance, 16)),
11841 y: box.pos + chart.plotTop,
11842 anchorX: point.plotX + chart.plotLeft,
11843 anchorY: point.isHeader ?
11844 box.pos + chart.plotTop - 15 : point.plotY + chart.plotTop
11845 });
11846 });
11847 },
11848
11849 /**
11850 * Find the new position and perform the move
11851 */
11852 updatePosition: function(point) {
11853 var chart = this.chart,
11854 label = this.getLabel(),
11855 pos = (this.options.positioner || this.getPosition).call(
11856 this,
11857 label.width,
11858 label.height,
11859 point
11860 );
11861
11862 // do the move
11863 this.move(
11864 Math.round(pos.x),
11865 Math.round(pos.y || 0), // can be undefined (#3977)
11866 point.plotX + chart.plotLeft,
11867 point.plotY + chart.plotTop
11868 );
11869 },
11870
11871 /**
11872 * Get the best X date format based on the closest point range on the axis.
11873 */
11874 getXDateFormat: function(point, options, xAxis) {
11875 var xDateFormat,
11876 dateTimeLabelFormats = options.dateTimeLabelFormats,
11877 closestPointRange = xAxis && xAxis.closestPointRange,
11878 n,
11879 blank = '01-01 00:00:00.000',
11880 strpos = {
11881 millisecond: 15,
11882 second: 12,
11883 minute: 9,
11884 hour: 6,
11885 day: 3
11886 },
11887 date,
11888 lastN = 'millisecond'; // for sub-millisecond data, #4223
11889
11890 if (closestPointRange) {
11891 date = dateFormat('%m-%d %H:%M:%S.%L', point.x);
11892 for (n in timeUnits) {
11893
11894 // If the range is exactly one week and we're looking at a Sunday/Monday, go for the week format
11895 if (closestPointRange === timeUnits.week && +dateFormat('%w', point.x) === xAxis.options.startOfWeek &&
11896 date.substr(6) === blank.substr(6)) {
11897 n = 'week';
11898 break;
11899 }
11900
11901 // The first format that is too great for the range
11902 if (timeUnits[n] > closestPointRange) {
11903 n = lastN;
11904 break;
11905 }
11906
11907 // If the point is placed every day at 23:59, we need to show
11908 // the minutes as well. #2637.
11909 if (strpos[n] && date.substr(strpos[n]) !== blank.substr(strpos[n])) {
11910 break;
11911 }
11912
11913 // Weeks are outside the hierarchy, only apply them on Mondays/Sundays like in the first condition
11914 if (n !== 'week') {
11915 lastN = n;
11916 }
11917 }
11918
11919 if (n) {
11920 xDateFormat = dateTimeLabelFormats[n];
11921 }
11922 } else {
11923 xDateFormat = dateTimeLabelFormats.day;
11924 }
11925
11926 return xDateFormat || dateTimeLabelFormats.year; // #2546, 2581
11927 },
11928
11929 /**
11930 * Format the footer/header of the tooltip
11931 * #3397: abstraction to enable formatting of footer and header
11932 */
11933 tooltipFooterHeaderFormatter: function(labelConfig, isFooter) {
11934 var footOrHead = isFooter ? 'footer' : 'header',
11935 series = labelConfig.series,
11936 tooltipOptions = series.tooltipOptions,
11937 xDateFormat = tooltipOptions.xDateFormat,
11938 xAxis = series.xAxis,
11939 isDateTime = xAxis && xAxis.options.type === 'datetime' && isNumber(labelConfig.key),
11940 formatString = tooltipOptions[footOrHead + 'Format'];
11941
11942 // Guess the best date format based on the closest point distance (#568, #3418)
11943 if (isDateTime && !xDateFormat) {
11944 xDateFormat = this.getXDateFormat(labelConfig, tooltipOptions, xAxis);
11945 }
11946
11947 // Insert the footer date format if any
11948 if (isDateTime && xDateFormat) {
11949 formatString = formatString.replace('{point.key}', '{point.key:' + xDateFormat + '}');
11950 }
11951
11952 return format(formatString, {
11953 point: labelConfig,
11954 series: series
11955 });
11956 },
11957
11958 /**
11959 * Build the body (lines) of the tooltip by iterating over the items and returning one entry for each item,
11960 * abstracting this functionality allows to easily overwrite and extend it.
11961 */
11962 bodyFormatter: function(items) {
11963 return map(items, function(item) {
11964 var tooltipOptions = item.series.tooltipOptions;
11965 return (tooltipOptions.pointFormatter || item.point.tooltipFormatter)
11966 .call(item.point, tooltipOptions.pointFormat);
11967 });
11968 }
11969
11970 };
11971
11972 }(Highcharts));
11973 (function(H) {
11974 /**
11975 * (c) 2010-2016 Torstein Honsi
11976 *
11977 * License: www.highcharts.com/license
11978 */
11979 'use strict';
11980 var addEvent = H.addEvent,
11981 attr = H.attr,
11982 charts = H.charts,
11983 color = H.color,
11984 css = H.css,
11985 defined = H.defined,
11986 doc = H.doc,
11987 each = H.each,
11988 extend = H.extend,
11989 fireEvent = H.fireEvent,
11990 offset = H.offset,
11991 pick = H.pick,
11992 removeEvent = H.removeEvent,
11993 splat = H.splat,
11994 Tooltip = H.Tooltip,
11995 win = H.win;
11996
11997 /**
11998 * The mouse tracker object. All methods starting with "on" are primary DOM
11999 * event handlers. Subsequent methods should be named differently from what they
12000 * are doing.
12001 *
12002 * @constructor Pointer
12003 * @param {Object} chart The Chart instance
12004 * @param {Object} options The root options object
12005 */
12006 H.Pointer = function(chart, options) {
12007 this.init(chart, options);
12008 };
12009
12010 H.Pointer.prototype = {
12011 /**
12012 * Initialize Pointer
12013 */
12014 init: function(chart, options) {
12015
12016 // Store references
12017 this.options = options;
12018 this.chart = chart;
12019
12020 // Do we need to handle click on a touch device?
12021 this.runChartClick = options.chart.events && !!options.chart.events.click;
12022
12023 this.pinchDown = [];
12024 this.lastValidTouch = {};
12025
12026 if (Tooltip && options.tooltip.enabled) {
12027 chart.tooltip = new Tooltip(chart, options.tooltip);
12028 this.followTouchMove = pick(options.tooltip.followTouchMove, true);
12029 }
12030
12031 this.setDOMEvents();
12032 },
12033
12034 /**
12035 * Resolve the zoomType option, this is reset on all touch start and mouse
12036 * down events.
12037 */
12038 zoomOption: function(e) {
12039 var chart = this.chart,
12040 options = chart.options.chart,
12041 zoomType = options.zoomType || '',
12042 inverted = chart.inverted,
12043 zoomX,
12044 zoomY;
12045
12046 // Look for the pinchType option
12047 if (/touch/.test(e.type)) {
12048 zoomType = pick(options.pinchType, zoomType);
12049 }
12050
12051 this.zoomX = zoomX = /x/.test(zoomType);
12052 this.zoomY = zoomY = /y/.test(zoomType);
12053 this.zoomHor = (zoomX && !inverted) || (zoomY && inverted);
12054 this.zoomVert = (zoomY && !inverted) || (zoomX && inverted);
12055 this.hasZoom = zoomX || zoomY;
12056 },
12057
12058 /**
12059 * Add crossbrowser support for chartX and chartY
12060 * @param {Object} e The event object in standard browsers
12061 */
12062 normalize: function(e, chartPosition) {
12063 var chartX,
12064 chartY,
12065 ePos;
12066
12067 // IE normalizing
12068 e = e || win.event;
12069 if (!e.target) {
12070 e.target = e.srcElement;
12071 }
12072
12073 // iOS (#2757)
12074 ePos = e.touches ? (e.touches.length ? e.touches.item(0) : e.changedTouches[0]) : e;
12075
12076 // Get mouse position
12077 if (!chartPosition) {
12078 this.chartPosition = chartPosition = offset(this.chart.container);
12079 }
12080
12081 // chartX and chartY
12082 if (ePos.pageX === undefined) { // IE < 9. #886.
12083 chartX = Math.max(e.x, e.clientX - chartPosition.left); // #2005, #2129: the second case is
12084 // for IE10 quirks mode within framesets
12085 chartY = e.y;
12086 } else {
12087 chartX = ePos.pageX - chartPosition.left;
12088 chartY = ePos.pageY - chartPosition.top;
12089 }
12090
12091 return extend(e, {
12092 chartX: Math.round(chartX),
12093 chartY: Math.round(chartY)
12094 });
12095 },
12096
12097 /**
12098 * Get the click position in terms of axis values.
12099 *
12100 * @param {Object} e A pointer event
12101 */
12102 getCoordinates: function(e) {
12103 var coordinates = {
12104 xAxis: [],
12105 yAxis: []
12106 };
12107
12108 each(this.chart.axes, function(axis) {
12109 coordinates[axis.isXAxis ? 'xAxis' : 'yAxis'].push({
12110 axis: axis,
12111 value: axis.toValue(e[axis.horiz ? 'chartX' : 'chartY'])
12112 });
12113 });
12114 return coordinates;
12115 },
12116
12117 /**
12118 * With line type charts with a single tracker, get the point closest to the mouse.
12119 * Run Point.onMouseOver and display tooltip for the point or points.
12120 */
12121 runPointActions: function(e) {
12122
12123 var pointer = this,
12124 chart = pointer.chart,
12125 series = chart.series,
12126 tooltip = chart.tooltip,
12127 shared = tooltip ? tooltip.shared : false,
12128 followPointer,
12129 updatePosition = true,
12130 hoverPoint = chart.hoverPoint,
12131 hoverSeries = chart.hoverSeries,
12132 i,
12133 anchor,
12134 noSharedTooltip,
12135 stickToHoverSeries,
12136 directTouch,
12137 kdpoints = [],
12138 kdpointT;
12139
12140 // For hovering over the empty parts of the plot area (hoverSeries is undefined).
12141 // If there is one series with point tracking (combo chart), don't go to nearest neighbour.
12142 if (!shared && !hoverSeries) {
12143 for (i = 0; i < series.length; i++) {
12144 if (series[i].directTouch || !series[i].options.stickyTracking) {
12145 series = [];
12146 }
12147 }
12148 }
12149
12150 // If it has a hoverPoint and that series requires direct touch (like columns, #3899), or we're on
12151 // a noSharedTooltip series among shared tooltip series (#4546), use the hoverPoint . Otherwise,
12152 // search the k-d tree.
12153 stickToHoverSeries = hoverSeries && (shared ? hoverSeries.noSharedTooltip : hoverSeries.directTouch);
12154 if (stickToHoverSeries && hoverPoint) {
12155 kdpoints = [hoverPoint];
12156
12157 // Handle shared tooltip or cases where a series is not yet hovered
12158 } else {
12159 // When we have non-shared tooltip and sticky tracking is disabled,
12160 // search for the closest point only on hovered series: #5533, #5476
12161 if (!shared && hoverSeries && !hoverSeries.options.stickyTracking) {
12162 series = [hoverSeries];
12163 }
12164 // Find nearest points on all series
12165 each(series, function(s) {
12166 // Skip hidden series
12167 noSharedTooltip = s.noSharedTooltip && shared;
12168 directTouch = !shared && s.directTouch;
12169 if (s.visible && !noSharedTooltip && !directTouch && pick(s.options.enableMouseTracking, true)) { // #3821
12170 kdpointT = s.searchPoint(e, !noSharedTooltip && s.kdDimensions === 1); // #3828
12171 if (kdpointT && kdpointT.series) { // Point.series becomes null when reset and before redraw (#5197)
12172 kdpoints.push(kdpointT);
12173 }
12174 }
12175 });
12176
12177 // Sort kdpoints by distance to mouse pointer
12178 kdpoints.sort(function(p1, p2) {
12179 var isCloserX = p1.distX - p2.distX,
12180 isCloser = p1.dist - p2.dist,
12181 isAbove = p2.series.group.zIndex - p1.series.group.zIndex;
12182
12183 // We have two points which are not in the same place on xAxis and shared tooltip:
12184 if (isCloserX !== 0 && shared) { // #5721
12185 return isCloserX;
12186 }
12187 // Points are not exactly in the same place on x/yAxis:
12188 if (isCloser !== 0) {
12189 return isCloser;
12190 }
12191 // The same xAxis and yAxis position, sort by z-index:
12192 if (isAbove !== 0) {
12193 return isAbove;
12194 }
12195
12196 // The same zIndex, sort by array index:
12197 return p1.series.index > p2.series.index ? -1 : 1;
12198 });
12199 }
12200
12201 // Remove points with different x-positions, required for shared tooltip and crosshairs (#4645):
12202 if (shared) {
12203 i = kdpoints.length;
12204 while (i--) {
12205 if (kdpoints[i].x !== kdpoints[0].x || kdpoints[i].series.noSharedTooltip) {
12206 kdpoints.splice(i, 1);
12207 }
12208 }
12209 }
12210
12211 // Refresh tooltip for kdpoint if new hover point or tooltip was hidden // #3926, #4200
12212 if (kdpoints[0] && (kdpoints[0] !== this.prevKDPoint || (tooltip && tooltip.isHidden))) {
12213 // Draw tooltip if necessary
12214 if (shared && !kdpoints[0].series.noSharedTooltip) {
12215 // Do mouseover on all points (#3919, #3985, #4410, #5622)
12216 for (i = 0; i < kdpoints.length; i++) {
12217 kdpoints[i].onMouseOver(e, kdpoints[i] !== ((hoverSeries && hoverSeries.directTouch && hoverPoint) || kdpoints[0]));
12218 }
12219
12220 if (kdpoints.length && tooltip) {
12221 // Keep the order of series in tooltip:
12222 tooltip.refresh(kdpoints.sort(function(p1, p2) {
12223 return p1.series.index - p2.series.index;
12224 }), e);
12225 }
12226 } else {
12227 if (tooltip) {
12228 tooltip.refresh(kdpoints[0], e);
12229 }
12230 if (!hoverSeries || !hoverSeries.directTouch) { // #4448
12231 kdpoints[0].onMouseOver(e);
12232 }
12233 }
12234 this.prevKDPoint = kdpoints[0];
12235 updatePosition = false;
12236 }
12237 // Update positions (regardless of kdpoint or hoverPoint)
12238 if (updatePosition) {
12239 followPointer = hoverSeries && hoverSeries.tooltipOptions.followPointer;
12240 if (tooltip && followPointer && !tooltip.isHidden) {
12241 anchor = tooltip.getAnchor([{}], e);
12242 tooltip.updatePosition({
12243 plotX: anchor[0],
12244 plotY: anchor[1]
12245 });
12246 }
12247 }
12248
12249 // Start the event listener to pick up the tooltip and crosshairs
12250 if (!pointer.unDocMouseMove) {
12251 pointer.unDocMouseMove = addEvent(doc, 'mousemove', function(e) {
12252 if (charts[H.hoverChartIndex]) {
12253 charts[H.hoverChartIndex].pointer.onDocumentMouseMove(e);
12254 }
12255 });
12256 }
12257
12258 // Crosshair. For each hover point, loop over axes and draw cross if that point
12259 // belongs to the axis (#4927).
12260 each(shared ? kdpoints : [pick(hoverPoint, kdpoints[0])], function drawPointCrosshair(point) { // #5269
12261 each(chart.axes, function drawAxisCrosshair(axis) {
12262 // In case of snap = false, point is undefined, and we draw the crosshair anyway (#5066)
12263 if (!point || point.series && point.series[axis.coll] === axis) { // #5658
12264 axis.drawCrosshair(e, point);
12265 }
12266 });
12267 });
12268 },
12269
12270 /**
12271 * Reset the tracking by hiding the tooltip, the hover series state and the hover point
12272 *
12273 * @param allowMove {Boolean} Instead of destroying the tooltip altogether, allow moving it if possible
12274 */
12275 reset: function(allowMove, delay) {
12276 var pointer = this,
12277 chart = pointer.chart,
12278 hoverSeries = chart.hoverSeries,
12279 hoverPoint = chart.hoverPoint,
12280 hoverPoints = chart.hoverPoints,
12281 tooltip = chart.tooltip,
12282 tooltipPoints = tooltip && tooltip.shared ? hoverPoints : hoverPoint;
12283
12284 // Check if the points have moved outside the plot area (#1003, #4736, #5101)
12285 if (allowMove && tooltipPoints) {
12286 each(splat(tooltipPoints), function(point) {
12287 if (point.series.isCartesian && point.plotX === undefined) {
12288 allowMove = false;
12289 }
12290 });
12291 }
12292
12293 // Just move the tooltip, #349
12294 if (allowMove) {
12295 if (tooltip && tooltipPoints) {
12296 tooltip.refresh(tooltipPoints);
12297 if (hoverPoint) { // #2500
12298 hoverPoint.setState(hoverPoint.state, true);
12299 each(chart.axes, function(axis) {
12300 if (axis.crosshair) {
12301 axis.drawCrosshair(null, hoverPoint);
12302 }
12303 });
12304 }
12305 }
12306
12307 // Full reset
12308 } else {
12309
12310 if (hoverPoint) {
12311 hoverPoint.onMouseOut();
12312 }
12313
12314 if (hoverPoints) {
12315 each(hoverPoints, function(point) {
12316 point.setState();
12317 });
12318 }
12319
12320 if (hoverSeries) {
12321 hoverSeries.onMouseOut();
12322 }
12323
12324 if (tooltip) {
12325 tooltip.hide(delay);
12326 }
12327
12328 if (pointer.unDocMouseMove) {
12329 pointer.unDocMouseMove = pointer.unDocMouseMove();
12330 }
12331
12332 // Remove crosshairs
12333 each(chart.axes, function(axis) {
12334 axis.hideCrosshair();
12335 });
12336
12337 pointer.hoverX = pointer.prevKDPoint = chart.hoverPoints = chart.hoverPoint = null;
12338 }
12339 },
12340
12341 /**
12342 * Scale series groups to a certain scale and translation
12343 */
12344 scaleGroups: function(attribs, clip) {
12345
12346 var chart = this.chart,
12347 seriesAttribs;
12348
12349 // Scale each series
12350 each(chart.series, function(series) {
12351 seriesAttribs = attribs || series.getPlotBox(); // #1701
12352 if (series.xAxis && series.xAxis.zoomEnabled && series.group) {
12353 series.group.attr(seriesAttribs);
12354 if (series.markerGroup) {
12355 series.markerGroup.attr(seriesAttribs);
12356 series.markerGroup.clip(clip ? chart.clipRect : null);
12357 }
12358 if (series.dataLabelsGroup) {
12359 series.dataLabelsGroup.attr(seriesAttribs);
12360 }
12361 }
12362 });
12363
12364 // Clip
12365 chart.clipRect.attr(clip || chart.clipBox);
12366 },
12367
12368 /**
12369 * Start a drag operation
12370 */
12371 dragStart: function(e) {
12372 var chart = this.chart;
12373
12374 // Record the start position
12375 chart.mouseIsDown = e.type;
12376 chart.cancelClick = false;
12377 chart.mouseDownX = this.mouseDownX = e.chartX;
12378 chart.mouseDownY = this.mouseDownY = e.chartY;
12379 },
12380
12381 /**
12382 * Perform a drag operation in response to a mousemove event while the mouse is down
12383 */
12384 drag: function(e) {
12385
12386 var chart = this.chart,
12387 chartOptions = chart.options.chart,
12388 chartX = e.chartX,
12389 chartY = e.chartY,
12390 zoomHor = this.zoomHor,
12391 zoomVert = this.zoomVert,
12392 plotLeft = chart.plotLeft,
12393 plotTop = chart.plotTop,
12394 plotWidth = chart.plotWidth,
12395 plotHeight = chart.plotHeight,
12396 clickedInside,
12397 size,
12398 selectionMarker = this.selectionMarker,
12399 mouseDownX = this.mouseDownX,
12400 mouseDownY = this.mouseDownY,
12401 panKey = chartOptions.panKey && e[chartOptions.panKey + 'Key'];
12402
12403 // If the device supports both touch and mouse (like IE11), and we are touch-dragging
12404 // inside the plot area, don't handle the mouse event. #4339.
12405 if (selectionMarker && selectionMarker.touch) {
12406 return;
12407 }
12408
12409 // If the mouse is outside the plot area, adjust to cooordinates
12410 // inside to prevent the selection marker from going outside
12411 if (chartX < plotLeft) {
12412 chartX = plotLeft;
12413 } else if (chartX > plotLeft + plotWidth) {
12414 chartX = plotLeft + plotWidth;
12415 }
12416
12417 if (chartY < plotTop) {
12418 chartY = plotTop;
12419 } else if (chartY > plotTop + plotHeight) {
12420 chartY = plotTop + plotHeight;
12421 }
12422
12423 // determine if the mouse has moved more than 10px
12424 this.hasDragged = Math.sqrt(
12425 Math.pow(mouseDownX - chartX, 2) +
12426 Math.pow(mouseDownY - chartY, 2)
12427 );
12428
12429 if (this.hasDragged > 10) {
12430 clickedInside = chart.isInsidePlot(mouseDownX - plotLeft, mouseDownY - plotTop);
12431
12432 // make a selection
12433 if (chart.hasCartesianSeries && (this.zoomX || this.zoomY) && clickedInside && !panKey) {
12434 if (!selectionMarker) {
12435 this.selectionMarker = selectionMarker = chart.renderer.rect(
12436 plotLeft,
12437 plotTop,
12438 zoomHor ? 1 : plotWidth,
12439 zoomVert ? 1 : plotHeight,
12440 0
12441 )
12442 .attr({
12443
12444 fill: chartOptions.selectionMarkerFill || color('#335cad').setOpacity(0.25).get(),
12445
12446 'class': 'highcharts-selection-marker',
12447 'zIndex': 7
12448 })
12449 .add();
12450 }
12451 }
12452
12453 // adjust the width of the selection marker
12454 if (selectionMarker && zoomHor) {
12455 size = chartX - mouseDownX;
12456 selectionMarker.attr({
12457 width: Math.abs(size),
12458 x: (size > 0 ? 0 : size) + mouseDownX
12459 });
12460 }
12461 // adjust the height of the selection marker
12462 if (selectionMarker && zoomVert) {
12463 size = chartY - mouseDownY;
12464 selectionMarker.attr({
12465 height: Math.abs(size),
12466 y: (size > 0 ? 0 : size) + mouseDownY
12467 });
12468 }
12469
12470 // panning
12471 if (clickedInside && !selectionMarker && chartOptions.panning) {
12472 chart.pan(e, chartOptions.panning);
12473 }
12474 }
12475 },
12476
12477 /**
12478 * On mouse up or touch end across the entire document, drop the selection.
12479 */
12480 drop: function(e) {
12481 var pointer = this,
12482 chart = this.chart,
12483 hasPinched = this.hasPinched;
12484
12485 if (this.selectionMarker) {
12486 var selectionData = {
12487 originalEvent: e, // #4890
12488 xAxis: [],
12489 yAxis: []
12490 },
12491 selectionBox = this.selectionMarker,
12492 selectionLeft = selectionBox.attr ? selectionBox.attr('x') : selectionBox.x,
12493 selectionTop = selectionBox.attr ? selectionBox.attr('y') : selectionBox.y,
12494 selectionWidth = selectionBox.attr ? selectionBox.attr('width') : selectionBox.width,
12495 selectionHeight = selectionBox.attr ? selectionBox.attr('height') : selectionBox.height,
12496 runZoom;
12497
12498 // a selection has been made
12499 if (this.hasDragged || hasPinched) {
12500
12501 // record each axis' min and max
12502 each(chart.axes, function(axis) {
12503 if (axis.zoomEnabled && defined(axis.min) && (hasPinched || pointer[{
12504 xAxis: 'zoomX',
12505 yAxis: 'zoomY'
12506 }[axis.coll]])) { // #859, #3569
12507 var horiz = axis.horiz,
12508 minPixelPadding = e.type === 'touchend' ? axis.minPixelPadding : 0, // #1207, #3075
12509 selectionMin = axis.toValue((horiz ? selectionLeft : selectionTop) + minPixelPadding),
12510 selectionMax = axis.toValue((horiz ? selectionLeft + selectionWidth : selectionTop + selectionHeight) - minPixelPadding);
12511
12512 selectionData[axis.coll].push({
12513 axis: axis,
12514 min: Math.min(selectionMin, selectionMax), // for reversed axes
12515 max: Math.max(selectionMin, selectionMax)
12516 });
12517 runZoom = true;
12518 }
12519 });
12520 if (runZoom) {
12521 fireEvent(chart, 'selection', selectionData, function(args) {
12522 chart.zoom(extend(args, hasPinched ? {
12523 animation: false
12524 } : null));
12525 });
12526 }
12527
12528 }
12529 this.selectionMarker = this.selectionMarker.destroy();
12530
12531 // Reset scaling preview
12532 if (hasPinched) {
12533 this.scaleGroups();
12534 }
12535 }
12536
12537 // Reset all
12538 if (chart) { // it may be destroyed on mouse up - #877
12539 css(chart.container, {
12540 cursor: chart._cursor
12541 });
12542 chart.cancelClick = this.hasDragged > 10; // #370
12543 chart.mouseIsDown = this.hasDragged = this.hasPinched = false;
12544 this.pinchDown = [];
12545 }
12546 },
12547
12548 onContainerMouseDown: function(e) {
12549
12550 e = this.normalize(e);
12551
12552 this.zoomOption(e);
12553
12554 // issue #295, dragging not always working in Firefox
12555 if (e.preventDefault) {
12556 e.preventDefault();
12557 }
12558
12559 this.dragStart(e);
12560 },
12561
12562
12563
12564 onDocumentMouseUp: function(e) {
12565 if (charts[H.hoverChartIndex]) {
12566 charts[H.hoverChartIndex].pointer.drop(e);
12567 }
12568 },
12569
12570 /**
12571 * Special handler for mouse move that will hide the tooltip when the mouse leaves the plotarea.
12572 * Issue #149 workaround. The mouseleave event does not always fire.
12573 */
12574 onDocumentMouseMove: function(e) {
12575 var chart = this.chart,
12576 chartPosition = this.chartPosition;
12577
12578 e = this.normalize(e, chartPosition);
12579
12580 // If we're outside, hide the tooltip
12581 if (chartPosition && !this.inClass(e.target, 'highcharts-tracker') &&
12582 !chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) {
12583 this.reset();
12584 }
12585 },
12586
12587 /**
12588 * When mouse leaves the container, hide the tooltip.
12589 */
12590 onContainerMouseLeave: function(e) {
12591 var chart = charts[H.hoverChartIndex];
12592 if (chart && (e.relatedTarget || e.toElement)) { // #4886, MS Touch end fires mouseleave but with no related target
12593 chart.pointer.reset();
12594 chart.pointer.chartPosition = null; // also reset the chart position, used in #149 fix
12595 }
12596 },
12597
12598 // The mousemove, touchmove and touchstart event handler
12599 onContainerMouseMove: function(e) {
12600
12601 var chart = this.chart;
12602
12603 if (!defined(H.hoverChartIndex) || !charts[H.hoverChartIndex] || !charts[H.hoverChartIndex].mouseIsDown) {
12604 H.hoverChartIndex = chart.index;
12605 }
12606
12607 e = this.normalize(e);
12608 e.returnValue = false; // #2251, #3224
12609
12610 if (chart.mouseIsDown === 'mousedown') {
12611 this.drag(e);
12612 }
12613
12614 // Show the tooltip and run mouse over events (#977)
12615 if ((this.inClass(e.target, 'highcharts-tracker') ||
12616 chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) && !chart.openMenu) {
12617 this.runPointActions(e);
12618 }
12619 },
12620
12621 /**
12622 * Utility to detect whether an element has, or has a parent with, a specific
12623 * class name. Used on detection of tracker objects and on deciding whether
12624 * hovering the tooltip should cause the active series to mouse out.
12625 */
12626 inClass: function(element, className) {
12627 var elemClassName;
12628 while (element) {
12629 elemClassName = attr(element, 'class');
12630 if (elemClassName) {
12631 if (elemClassName.indexOf(className) !== -1) {
12632 return true;
12633 }
12634 if (elemClassName.indexOf('highcharts-container') !== -1) {
12635 return false;
12636 }
12637 }
12638 element = element.parentNode;
12639 }
12640 },
12641
12642 onTrackerMouseOut: function(e) {
12643 var series = this.chart.hoverSeries,
12644 relatedTarget = e.relatedTarget || e.toElement;
12645
12646 if (series && relatedTarget && !series.options.stickyTracking &&
12647 !this.inClass(relatedTarget, 'highcharts-tooltip') &&
12648 (!this.inClass(relatedTarget, 'highcharts-series-' + series.index) || // #2499, #4465
12649 !this.inClass(relatedTarget, 'highcharts-tracker') // #5553
12650 )
12651 ) {
12652 series.onMouseOut();
12653 }
12654 },
12655
12656 onContainerClick: function(e) {
12657 var chart = this.chart,
12658 hoverPoint = chart.hoverPoint,
12659 plotLeft = chart.plotLeft,
12660 plotTop = chart.plotTop;
12661
12662 e = this.normalize(e);
12663
12664 if (!chart.cancelClick) {
12665
12666 // On tracker click, fire the series and point events. #783, #1583
12667 if (hoverPoint && this.inClass(e.target, 'highcharts-tracker')) {
12668
12669 // the series click event
12670 fireEvent(hoverPoint.series, 'click', extend(e, {
12671 point: hoverPoint
12672 }));
12673
12674 // the point click event
12675 if (chart.hoverPoint) { // it may be destroyed (#1844)
12676 hoverPoint.firePointEvent('click', e);
12677 }
12678
12679 // When clicking outside a tracker, fire a chart event
12680 } else {
12681 extend(e, this.getCoordinates(e));
12682
12683 // fire a click event in the chart
12684 if (chart.isInsidePlot(e.chartX - plotLeft, e.chartY - plotTop)) {
12685 fireEvent(chart, 'click', e);
12686 }
12687 }
12688
12689
12690 }
12691 },
12692
12693 /**
12694 * Set the JS DOM events on the container and document. This method should contain
12695 * a one-to-one assignment between methods and their handlers. Any advanced logic should
12696 * be moved to the handler reflecting the event's name.
12697 */
12698 setDOMEvents: function() {
12699
12700 var pointer = this,
12701 container = pointer.chart.container;
12702
12703 container.onmousedown = function(e) {
12704 pointer.onContainerMouseDown(e);
12705 };
12706 container.onmousemove = function(e) {
12707 pointer.onContainerMouseMove(e);
12708 };
12709 container.onclick = function(e) {
12710 pointer.onContainerClick(e);
12711 };
12712 addEvent(container, 'mouseleave', pointer.onContainerMouseLeave);
12713 if (H.chartCount === 1) {
12714 addEvent(doc, 'mouseup', pointer.onDocumentMouseUp);
12715 }
12716 if (H.hasTouch) {
12717 container.ontouchstart = function(e) {
12718 pointer.onContainerTouchStart(e);
12719 };
12720 container.ontouchmove = function(e) {
12721 pointer.onContainerTouchMove(e);
12722 };
12723 if (H.chartCount === 1) {
12724 addEvent(doc, 'touchend', pointer.onDocumentTouchEnd);
12725 }
12726 }
12727
12728 },
12729
12730 /**
12731 * Destroys the Pointer object and disconnects DOM events.
12732 */
12733 destroy: function() {
12734 var prop;
12735
12736 removeEvent(this.chart.container, 'mouseleave', this.onContainerMouseLeave);
12737 if (!H.chartCount) {
12738 removeEvent(doc, 'mouseup', this.onDocumentMouseUp);
12739 removeEvent(doc, 'touchend', this.onDocumentTouchEnd);
12740 }
12741
12742 // memory and CPU leak
12743 clearInterval(this.tooltipTimeout);
12744
12745 for (prop in this) {
12746 this[prop] = null;
12747 }
12748 }
12749 };
12750
12751 }(Highcharts));
12752 (function(H) {
12753 /**
12754 * (c) 2010-2016 Torstein Honsi
12755 *
12756 * License: www.highcharts.com/license
12757 */
12758 'use strict';
12759 var charts = H.charts,
12760 each = H.each,
12761 extend = H.extend,
12762 map = H.map,
12763 noop = H.noop,
12764 pick = H.pick,
12765 Pointer = H.Pointer;
12766
12767 /* Support for touch devices */
12768 extend(Pointer.prototype, /** @lends Pointer.prototype */ {
12769
12770 /**
12771 * Run translation operations
12772 */
12773 pinchTranslate: function(pinchDown, touches, transform, selectionMarker, clip, lastValidTouch) {
12774 if (this.zoomHor) {
12775 this.pinchTranslateDirection(true, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch);
12776 }
12777 if (this.zoomVert) {
12778 this.pinchTranslateDirection(false, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch);
12779 }
12780 },
12781
12782 /**
12783 * Run translation operations for each direction (horizontal and vertical) independently
12784 */
12785 pinchTranslateDirection: function(horiz, pinchDown, touches, transform,
12786 selectionMarker, clip, lastValidTouch, forcedScale) {
12787 var chart = this.chart,
12788 xy = horiz ? 'x' : 'y',
12789 XY = horiz ? 'X' : 'Y',
12790 sChartXY = 'chart' + XY,
12791 wh = horiz ? 'width' : 'height',
12792 plotLeftTop = chart['plot' + (horiz ? 'Left' : 'Top')],
12793 selectionWH,
12794 selectionXY,
12795 clipXY,
12796 scale = forcedScale || 1,
12797 inverted = chart.inverted,
12798 bounds = chart.bounds[horiz ? 'h' : 'v'],
12799 singleTouch = pinchDown.length === 1,
12800 touch0Start = pinchDown[0][sChartXY],
12801 touch0Now = touches[0][sChartXY],
12802 touch1Start = !singleTouch && pinchDown[1][sChartXY],
12803 touch1Now = !singleTouch && touches[1][sChartXY],
12804 outOfBounds,
12805 transformScale,
12806 scaleKey,
12807 setScale = function() {
12808 // Don't zoom if fingers are too close on this axis
12809 if (!singleTouch && Math.abs(touch0Start - touch1Start) > 20) {
12810 scale = forcedScale || Math.abs(touch0Now - touch1Now) / Math.abs(touch0Start - touch1Start);
12811 }
12812
12813 clipXY = ((plotLeftTop - touch0Now) / scale) + touch0Start;
12814 selectionWH = chart['plot' + (horiz ? 'Width' : 'Height')] / scale;
12815 };
12816
12817 // Set the scale, first pass
12818 setScale();
12819
12820 selectionXY = clipXY; // the clip position (x or y) is altered if out of bounds, the selection position is not
12821
12822 // Out of bounds
12823 if (selectionXY < bounds.min) {
12824 selectionXY = bounds.min;
12825 outOfBounds = true;
12826 } else if (selectionXY + selectionWH > bounds.max) {
12827 selectionXY = bounds.max - selectionWH;
12828 outOfBounds = true;
12829 }
12830
12831 // Is the chart dragged off its bounds, determined by dataMin and dataMax?
12832 if (outOfBounds) {
12833
12834 // Modify the touchNow position in order to create an elastic drag movement. This indicates
12835 // to the user that the chart is responsive but can't be dragged further.
12836 touch0Now -= 0.8 * (touch0Now - lastValidTouch[xy][0]);
12837 if (!singleTouch) {
12838 touch1Now -= 0.8 * (touch1Now - lastValidTouch[xy][1]);
12839 }
12840
12841 // Set the scale, second pass to adapt to the modified touchNow positions
12842 setScale();
12843
12844 } else {
12845 lastValidTouch[xy] = [touch0Now, touch1Now];
12846 }
12847
12848 // Set geometry for clipping, selection and transformation
12849 if (!inverted) {
12850 clip[xy] = clipXY - plotLeftTop;
12851 clip[wh] = selectionWH;
12852 }
12853 scaleKey = inverted ? (horiz ? 'scaleY' : 'scaleX') : 'scale' + XY;
12854 transformScale = inverted ? 1 / scale : scale;
12855
12856 selectionMarker[wh] = selectionWH;
12857 selectionMarker[xy] = selectionXY;
12858 transform[scaleKey] = scale;
12859 transform['translate' + XY] = (transformScale * plotLeftTop) + (touch0Now - (transformScale * touch0Start));
12860 },
12861
12862 /**
12863 * Handle touch events with two touches
12864 */
12865 pinch: function(e) {
12866
12867 var self = this,
12868 chart = self.chart,
12869 pinchDown = self.pinchDown,
12870 touches = e.touches,
12871 touchesLength = touches.length,
12872 lastValidTouch = self.lastValidTouch,
12873 hasZoom = self.hasZoom,
12874 selectionMarker = self.selectionMarker,
12875 transform = {},
12876 fireClickEvent = touchesLength === 1 && ((self.inClass(e.target, 'highcharts-tracker') &&
12877 chart.runTrackerClick) || self.runChartClick),
12878 clip = {};
12879
12880 // Don't initiate panning until the user has pinched. This prevents us from
12881 // blocking page scrolling as users scroll down a long page (#4210).
12882 if (touchesLength > 1) {
12883 self.initiated = true;
12884 }
12885
12886 // On touch devices, only proceed to trigger click if a handler is defined
12887 if (hasZoom && self.initiated && !fireClickEvent) {
12888 e.preventDefault();
12889 }
12890
12891 // Normalize each touch
12892 map(touches, function(e) {
12893 return self.normalize(e);
12894 });
12895
12896 // Register the touch start position
12897 if (e.type === 'touchstart') {
12898 each(touches, function(e, i) {
12899 pinchDown[i] = {
12900 chartX: e.chartX,
12901 chartY: e.chartY
12902 };
12903 });
12904 lastValidTouch.x = [pinchDown[0].chartX, pinchDown[1] && pinchDown[1].chartX];
12905 lastValidTouch.y = [pinchDown[0].chartY, pinchDown[1] && pinchDown[1].chartY];
12906
12907 // Identify the data bounds in pixels
12908 each(chart.axes, function(axis) {
12909 if (axis.zoomEnabled) {
12910 var bounds = chart.bounds[axis.horiz ? 'h' : 'v'],
12911 minPixelPadding = axis.minPixelPadding,
12912 min = axis.toPixels(pick(axis.options.min, axis.dataMin)),
12913 max = axis.toPixels(pick(axis.options.max, axis.dataMax)),
12914 absMin = Math.min(min, max),
12915 absMax = Math.max(min, max);
12916
12917 // Store the bounds for use in the touchmove handler
12918 bounds.min = Math.min(axis.pos, absMin - minPixelPadding);
12919 bounds.max = Math.max(axis.pos + axis.len, absMax + minPixelPadding);
12920 }
12921 });
12922 self.res = true; // reset on next move
12923
12924 // Optionally move the tooltip on touchmove
12925 } else if (self.followTouchMove && touchesLength === 1) {
12926 this.runPointActions(self.normalize(e));
12927
12928 // Event type is touchmove, handle panning and pinching
12929 } else if (pinchDown.length) { // can be 0 when releasing, if touchend fires first
12930
12931
12932 // Set the marker
12933 if (!selectionMarker) {
12934 self.selectionMarker = selectionMarker = extend({
12935 destroy: noop,
12936 touch: true
12937 }, chart.plotBox);
12938 }
12939
12940 self.pinchTranslate(pinchDown, touches, transform, selectionMarker, clip, lastValidTouch);
12941
12942 self.hasPinched = hasZoom;
12943
12944 // Scale and translate the groups to provide visual feedback during pinching
12945 self.scaleGroups(transform, clip);
12946
12947 if (self.res) {
12948 self.res = false;
12949 this.reset(false, 0);
12950 }
12951 }
12952 },
12953
12954 /**
12955 * General touch handler shared by touchstart and touchmove.
12956 */
12957 touch: function(e, start) {
12958 var chart = this.chart,
12959 hasMoved,
12960 pinchDown,
12961 isInside;
12962
12963 if (chart.index !== H.hoverChartIndex) {
12964 this.onContainerMouseLeave({
12965 relatedTarget: true
12966 });
12967 }
12968 H.hoverChartIndex = chart.index;
12969
12970 if (e.touches.length === 1) {
12971
12972 e = this.normalize(e);
12973
12974 isInside = chart.isInsidePlot(
12975 e.chartX - chart.plotLeft,
12976 e.chartY - chart.plotTop
12977 );
12978 if (isInside && !chart.openMenu) {
12979
12980 // Run mouse events and display tooltip etc
12981 if (start) {
12982 this.runPointActions(e);
12983 }
12984
12985 // Android fires touchmove events after the touchstart even if the
12986 // finger hasn't moved, or moved only a pixel or two. In iOS however,
12987 // the touchmove doesn't fire unless the finger moves more than ~4px.
12988 // So we emulate this behaviour in Android by checking how much it
12989 // moved, and cancelling on small distances. #3450.
12990 if (e.type === 'touchmove') {
12991 pinchDown = this.pinchDown;
12992 hasMoved = pinchDown[0] ? Math.sqrt( // #5266
12993 Math.pow(pinchDown[0].chartX - e.chartX, 2) +
12994 Math.pow(pinchDown[0].chartY - e.chartY, 2)
12995 ) >= 4 : false;
12996 }
12997
12998 if (pick(hasMoved, true)) {
12999 this.pinch(e);
13000 }
13001
13002 } else if (start) {
13003 // Hide the tooltip on touching outside the plot area (#1203)
13004 this.reset();
13005 }
13006
13007 } else if (e.touches.length === 2) {
13008 this.pinch(e);
13009 }
13010 },
13011
13012 onContainerTouchStart: function(e) {
13013 this.zoomOption(e);
13014 this.touch(e, true);
13015 },
13016
13017 onContainerTouchMove: function(e) {
13018 this.touch(e);
13019 },
13020
13021 onDocumentTouchEnd: function(e) {
13022 if (charts[H.hoverChartIndex]) {
13023 charts[H.hoverChartIndex].pointer.drop(e);
13024 }
13025 }
13026
13027 });
13028
13029 }(Highcharts));
13030 (function(H) {
13031 /**
13032 * (c) 2010-2016 Torstein Honsi
13033 *
13034 * License: www.highcharts.com/license
13035 */
13036 'use strict';
13037 var addEvent = H.addEvent,
13038 charts = H.charts,
13039 css = H.css,
13040 doc = H.doc,
13041 extend = H.extend,
13042 noop = H.noop,
13043 Pointer = H.Pointer,
13044 removeEvent = H.removeEvent,
13045 win = H.win,
13046 wrap = H.wrap;
13047
13048 if (win.PointerEvent || win.MSPointerEvent) {
13049
13050 // The touches object keeps track of the points being touched at all times
13051 var touches = {},
13052 hasPointerEvent = !!win.PointerEvent,
13053 getWebkitTouches = function() {
13054 var key,
13055 fake = [];
13056 fake.item = function(i) {
13057 return this[i];
13058 };
13059 for (key in touches) {
13060 if (touches.hasOwnProperty(key)) {
13061 fake.push({
13062 pageX: touches[key].pageX,
13063 pageY: touches[key].pageY,
13064 target: touches[key].target
13065 });
13066 }
13067 }
13068 return fake;
13069 },
13070 translateMSPointer = function(e, method, wktype, func) {
13071 var p;
13072 if ((e.pointerType === 'touch' || e.pointerType === e.MSPOINTER_TYPE_TOUCH) && charts[H.hoverChartIndex]) {
13073 func(e);
13074 p = charts[H.hoverChartIndex].pointer;
13075 p[method]({
13076 type: wktype,
13077 target: e.currentTarget,
13078 preventDefault: noop,
13079 touches: getWebkitTouches()
13080 });
13081 }
13082 };
13083
13084 /**
13085 * Extend the Pointer prototype with methods for each event handler and more
13086 */
13087 extend(Pointer.prototype, /** @lends Pointer.prototype */ {
13088 onContainerPointerDown: function(e) {
13089 translateMSPointer(e, 'onContainerTouchStart', 'touchstart', function(e) {
13090 touches[e.pointerId] = {
13091 pageX: e.pageX,
13092 pageY: e.pageY,
13093 target: e.currentTarget
13094 };
13095 });
13096 },
13097 onContainerPointerMove: function(e) {
13098 translateMSPointer(e, 'onContainerTouchMove', 'touchmove', function(e) {
13099 touches[e.pointerId] = {
13100 pageX: e.pageX,
13101 pageY: e.pageY
13102 };
13103 if (!touches[e.pointerId].target) {
13104 touches[e.pointerId].target = e.currentTarget;
13105 }
13106 });
13107 },
13108 onDocumentPointerUp: function(e) {
13109 translateMSPointer(e, 'onDocumentTouchEnd', 'touchend', function(e) {
13110 delete touches[e.pointerId];
13111 });
13112 },
13113
13114 /**
13115 * Add or remove the MS Pointer specific events
13116 */
13117 batchMSEvents: function(fn) {
13118 fn(this.chart.container, hasPointerEvent ? 'pointerdown' : 'MSPointerDown', this.onContainerPointerDown);
13119 fn(this.chart.container, hasPointerEvent ? 'pointermove' : 'MSPointerMove', this.onContainerPointerMove);
13120 fn(doc, hasPointerEvent ? 'pointerup' : 'MSPointerUp', this.onDocumentPointerUp);
13121 }
13122 });
13123
13124 // Disable default IE actions for pinch and such on chart element
13125 wrap(Pointer.prototype, 'init', function(proceed, chart, options) {
13126 proceed.call(this, chart, options);
13127 if (this.hasZoom) { // #4014
13128 css(chart.container, {
13129 '-ms-touch-action': 'none',
13130 'touch-action': 'none'
13131 });
13132 }
13133 });
13134
13135 // Add IE specific touch events to chart
13136 wrap(Pointer.prototype, 'setDOMEvents', function(proceed) {
13137 proceed.apply(this);
13138 if (this.hasZoom || this.followTouchMove) {
13139 this.batchMSEvents(addEvent);
13140 }
13141 });
13142 // Destroy MS events also
13143 wrap(Pointer.prototype, 'destroy', function(proceed) {
13144 this.batchMSEvents(removeEvent);
13145 proceed.call(this);
13146 });
13147 }
13148
13149 }(Highcharts));
13150 (function(H) {
13151 /**
13152 * (c) 2010-2016 Torstein Honsi
13153 *
13154 * License: www.highcharts.com/license
13155 */
13156 'use strict';
13157 var Legend,
13158
13159 addEvent = H.addEvent,
13160 css = H.css,
13161 discardElement = H.discardElement,
13162 defined = H.defined,
13163 each = H.each,
13164 extend = H.extend,
13165 isFirefox = H.isFirefox,
13166 marginNames = H.marginNames,
13167 merge = H.merge,
13168 pick = H.pick,
13169 setAnimation = H.setAnimation,
13170 stableSort = H.stableSort,
13171 win = H.win,
13172 wrap = H.wrap;
13173 /**
13174 * The overview of the chart's series.
13175 * @class
13176 */
13177 Legend = H.Legend = function(chart, options) {
13178 this.init(chart, options);
13179 };
13180
13181 Legend.prototype = {
13182
13183 /**
13184 * Initialize the legend
13185 */
13186 init: function(chart, options) {
13187
13188 this.chart = chart;
13189
13190 this.setOptions(options);
13191
13192 if (options.enabled) {
13193
13194 // Render it
13195 this.render();
13196
13197 // move checkboxes
13198 addEvent(this.chart, 'endResize', function() {
13199 this.legend.positionCheckboxes();
13200 });
13201 }
13202 },
13203
13204 setOptions: function(options) {
13205
13206 var padding = pick(options.padding, 8);
13207
13208 this.options = options;
13209
13210
13211 this.itemStyle = options.itemStyle;
13212 this.itemHiddenStyle = merge(this.itemStyle, options.itemHiddenStyle);
13213
13214 this.itemMarginTop = options.itemMarginTop || 0;
13215 this.padding = padding;
13216 this.initialItemX = padding;
13217 this.initialItemY = padding - 5; // 5 is the number of pixels above the text
13218 this.maxItemWidth = 0;
13219 this.itemHeight = 0;
13220 this.symbolWidth = pick(options.symbolWidth, 16);
13221 this.pages = [];
13222
13223 },
13224
13225 /**
13226 * Update the legend with new options. Equivalent to running chart.update with a legend
13227 * configuration option.
13228 * @param {Object} options Legend options
13229 * @param {Boolean} redraw Whether to redraw the chart, defaults to true.
13230 */
13231 update: function(options, redraw) {
13232 var chart = this.chart;
13233
13234 this.setOptions(merge(true, this.options, options));
13235 this.destroy();
13236 chart.isDirtyLegend = chart.isDirtyBox = true;
13237 if (pick(redraw, true)) {
13238 chart.redraw();
13239 }
13240 },
13241
13242 /**
13243 * Set the colors for the legend item
13244 * @param {Object} item A Series or Point instance
13245 * @param {Object} visible Dimmed or colored
13246 */
13247 colorizeItem: function(item, visible) {
13248 item.legendGroup[visible ? 'removeClass' : 'addClass']('highcharts-legend-item-hidden');
13249
13250
13251 var legend = this,
13252 options = legend.options,
13253 legendItem = item.legendItem,
13254 legendLine = item.legendLine,
13255 legendSymbol = item.legendSymbol,
13256 hiddenColor = legend.itemHiddenStyle.color,
13257 textColor = visible ? options.itemStyle.color : hiddenColor,
13258 symbolColor = visible ? (item.color || hiddenColor) : hiddenColor,
13259 markerOptions = item.options && item.options.marker,
13260 symbolAttr = {
13261 fill: symbolColor
13262 },
13263 key;
13264
13265 if (legendItem) {
13266 legendItem.css({
13267 fill: textColor,
13268 color: textColor
13269 }); // color for #1553, oldIE
13270 }
13271 if (legendLine) {
13272 legendLine.attr({
13273 stroke: symbolColor
13274 });
13275 }
13276
13277 if (legendSymbol) {
13278
13279 // Apply marker options
13280 if (markerOptions && legendSymbol.isMarker) { // #585
13281 //symbolAttr.stroke = symbolColor;
13282 symbolAttr = item.pointAttribs();
13283 if (!visible) {
13284 for (key in symbolAttr) {
13285 symbolAttr[key] = hiddenColor;
13286 }
13287 }
13288 }
13289
13290 legendSymbol.attr(symbolAttr);
13291 }
13292
13293 },
13294
13295 /**
13296 * Position the legend item
13297 * @param {Object} item A Series or Point instance
13298 */
13299 positionItem: function(item) {
13300 var legend = this,
13301 options = legend.options,
13302 symbolPadding = options.symbolPadding,
13303 ltr = !options.rtl,
13304 legendItemPos = item._legendItemPos,
13305 itemX = legendItemPos[0],
13306 itemY = legendItemPos[1],
13307 checkbox = item.checkbox,
13308 legendGroup = item.legendGroup;
13309
13310 if (legendGroup && legendGroup.element) {
13311 legendGroup.translate(
13312 ltr ? itemX : legend.legendWidth - itemX - 2 * symbolPadding - 4,
13313 itemY
13314 );
13315 }
13316
13317 if (checkbox) {
13318 checkbox.x = itemX;
13319 checkbox.y = itemY;
13320 }
13321 },
13322
13323 /**
13324 * Destroy a single legend item
13325 * @param {Object} item The series or point
13326 */
13327 destroyItem: function(item) {
13328 var checkbox = item.checkbox;
13329
13330 // destroy SVG elements
13331 each(['legendItem', 'legendLine', 'legendSymbol', 'legendGroup'], function(key) {
13332 if (item[key]) {
13333 item[key] = item[key].destroy();
13334 }
13335 });
13336
13337 if (checkbox) {
13338 discardElement(item.checkbox);
13339 }
13340 },
13341
13342 /**
13343 * Destroys the legend.
13344 */
13345 destroy: function() {
13346 var legend = this,
13347 legendGroup = legend.group,
13348 box = legend.box;
13349
13350 if (box) {
13351 legend.box = box.destroy();
13352 }
13353
13354 // Destroy items
13355 each(this.getAllItems(), function(item) {
13356 each(['legendItem', 'legendGroup'], function(key) {
13357 if (item[key]) {
13358 item[key] = item[key].destroy();
13359 }
13360 });
13361 });
13362
13363 if (legendGroup) {
13364 legend.group = legendGroup.destroy();
13365 }
13366 legend.display = null; // Reset in .render on update.
13367 },
13368
13369 /**
13370 * Position the checkboxes after the width is determined
13371 */
13372 positionCheckboxes: function(scrollOffset) {
13373 var alignAttr = this.group && this.group.alignAttr,
13374 translateY,
13375 clipHeight = this.clipHeight || this.legendHeight,
13376 titleHeight = this.titleHeight;
13377
13378 if (alignAttr) {
13379 translateY = alignAttr.translateY;
13380 each(this.allItems, function(item) {
13381 var checkbox = item.checkbox,
13382 top;
13383
13384 if (checkbox) {
13385 top = translateY + titleHeight + checkbox.y + (scrollOffset || 0) + 3;
13386 css(checkbox, {
13387 left: (alignAttr.translateX + item.checkboxOffset + checkbox.x - 20) + 'px',
13388 top: top + 'px',
13389 display: top > translateY - 6 && top < translateY + clipHeight - 6 ? '' : 'none'
13390 });
13391 }
13392 });
13393 }
13394 },
13395
13396 /**
13397 * Render the legend title on top of the legend
13398 */
13399 renderTitle: function() {
13400 var options = this.options,
13401 padding = this.padding,
13402 titleOptions = options.title,
13403 titleHeight = 0,
13404 bBox;
13405
13406 if (titleOptions.text) {
13407 if (!this.title) {
13408 this.title = this.chart.renderer.label(titleOptions.text, padding - 3, padding - 4, null, null, null, null, null, 'legend-title')
13409 .attr({
13410 zIndex: 1
13411 })
13412
13413 .css(titleOptions.style)
13414
13415 .add(this.group);
13416 }
13417 bBox = this.title.getBBox();
13418 titleHeight = bBox.height;
13419 this.offsetWidth = bBox.width; // #1717
13420 this.contentGroup.attr({
13421 translateY: titleHeight
13422 });
13423 }
13424 this.titleHeight = titleHeight;
13425 },
13426
13427 /**
13428 * Set the legend item text
13429 */
13430 setText: function(item) {
13431 var options = this.options;
13432 item.legendItem.attr({
13433 text: options.labelFormat ? H.format(options.labelFormat, item) : options.labelFormatter.call(item)
13434 });
13435 },
13436
13437 /**
13438 * Render a single specific legend item
13439 * @param {Object} item A series or point
13440 */
13441 renderItem: function(item) {
13442 var legend = this,
13443 chart = legend.chart,
13444 renderer = chart.renderer,
13445 options = legend.options,
13446 horizontal = options.layout === 'horizontal',
13447 symbolWidth = legend.symbolWidth,
13448 symbolPadding = options.symbolPadding,
13449
13450 itemStyle = legend.itemStyle,
13451 itemHiddenStyle = legend.itemHiddenStyle,
13452
13453 padding = legend.padding,
13454 itemDistance = horizontal ? pick(options.itemDistance, 20) : 0,
13455 ltr = !options.rtl,
13456 itemHeight,
13457 widthOption = options.width,
13458 itemMarginBottom = options.itemMarginBottom || 0,
13459 itemMarginTop = legend.itemMarginTop,
13460 initialItemX = legend.initialItemX,
13461 bBox,
13462 itemWidth,
13463 li = item.legendItem,
13464 isSeries = !item.series,
13465 series = !isSeries && item.series.drawLegendSymbol ? item.series : item,
13466 seriesOptions = series.options,
13467 showCheckbox = legend.createCheckboxForItem && seriesOptions && seriesOptions.showCheckbox,
13468 useHTML = options.useHTML,
13469 fontSize = 12;
13470
13471 if (!li) { // generate it once, later move it
13472
13473 // Generate the group box
13474 // A group to hold the symbol and text. Text is to be appended in Legend class.
13475 item.legendGroup = renderer.g('legend-item')
13476 .addClass('highcharts-' + series.type + '-series highcharts-color-' + item.colorIndex +
13477 (item.options.className ? ' ' + item.options.className : '') +
13478 (isSeries ? ' highcharts-series-' + item.index : '')
13479 )
13480 .attr({
13481 zIndex: 1
13482 })
13483 .add(legend.scrollGroup);
13484
13485 // Generate the list item text and add it to the group
13486 item.legendItem = li = renderer.text(
13487 '',
13488 ltr ? symbolWidth + symbolPadding : -symbolPadding,
13489 legend.baseline || 0,
13490 useHTML
13491 )
13492
13493 .css(merge(item.visible ? itemStyle : itemHiddenStyle)) // merge to prevent modifying original (#1021)
13494
13495 .attr({
13496 align: ltr ? 'left' : 'right',
13497 zIndex: 2
13498 })
13499 .add(item.legendGroup);
13500
13501 // Get the baseline for the first item - the font size is equal for all
13502 if (!legend.baseline) {
13503
13504 fontSize = itemStyle.fontSize;
13505
13506 legend.fontMetrics = renderer.fontMetrics(
13507 fontSize,
13508 li
13509 );
13510 legend.baseline = legend.fontMetrics.f + 3 + itemMarginTop;
13511 li.attr('y', legend.baseline);
13512 }
13513
13514 // Draw the legend symbol inside the group box
13515 series.drawLegendSymbol(legend, item);
13516
13517 if (legend.setItemEvents) {
13518 legend.setItemEvents(item, li, useHTML);
13519 }
13520
13521 // add the HTML checkbox on top
13522 if (showCheckbox) {
13523 legend.createCheckboxForItem(item);
13524 }
13525 }
13526
13527 // Colorize the items
13528 legend.colorizeItem(item, item.visible);
13529
13530 // Always update the text
13531 legend.setText(item);
13532
13533 // calculate the positions for the next line
13534 bBox = li.getBBox();
13535
13536 itemWidth = item.checkboxOffset =
13537 options.itemWidth ||
13538 item.legendItemWidth ||
13539 symbolWidth + symbolPadding + bBox.width + itemDistance + (showCheckbox ? 20 : 0);
13540 legend.itemHeight = itemHeight = Math.round(item.legendItemHeight || bBox.height);
13541
13542 // if the item exceeds the width, start a new line
13543 if (horizontal && legend.itemX - initialItemX + itemWidth >
13544 (widthOption || (chart.chartWidth - 2 * padding - initialItemX - options.x))) {
13545 legend.itemX = initialItemX;
13546 legend.itemY += itemMarginTop + legend.lastLineHeight + itemMarginBottom;
13547 legend.lastLineHeight = 0; // reset for next line (#915, #3976)
13548 }
13549
13550 // If the item exceeds the height, start a new column
13551 /*if (!horizontal && legend.itemY + options.y + itemHeight > chart.chartHeight - spacingTop - spacingBottom) {
13552 legend.itemY = legend.initialItemY;
13553 legend.itemX += legend.maxItemWidth;
13554 legend.maxItemWidth = 0;
13555 }*/
13556
13557 // Set the edge positions
13558 legend.maxItemWidth = Math.max(legend.maxItemWidth, itemWidth);
13559 legend.lastItemY = itemMarginTop + legend.itemY + itemMarginBottom;
13560 legend.lastLineHeight = Math.max(itemHeight, legend.lastLineHeight); // #915
13561
13562 // cache the position of the newly generated or reordered items
13563 item._legendItemPos = [legend.itemX, legend.itemY];
13564
13565 // advance
13566 if (horizontal) {
13567 legend.itemX += itemWidth;
13568
13569 } else {
13570 legend.itemY += itemMarginTop + itemHeight + itemMarginBottom;
13571 legend.lastLineHeight = itemHeight;
13572 }
13573
13574 // the width of the widest item
13575 legend.offsetWidth = widthOption || Math.max(
13576 (horizontal ? legend.itemX - initialItemX - itemDistance : itemWidth) + padding,
13577 legend.offsetWidth
13578 );
13579 },
13580
13581 /**
13582 * Get all items, which is one item per series for normal series and one item per point
13583 * for pie series.
13584 */
13585 getAllItems: function() {
13586 var allItems = [];
13587 each(this.chart.series, function(series) {
13588 var seriesOptions = series && series.options;
13589
13590 // Handle showInLegend. If the series is linked to another series, defaults to false.
13591 if (series && pick(seriesOptions.showInLegend, !defined(seriesOptions.linkedTo) ? undefined : false, true)) {
13592
13593 // Use points or series for the legend item depending on legendType
13594 allItems = allItems.concat(
13595 series.legendItems ||
13596 (seriesOptions.legendType === 'point' ?
13597 series.data :
13598 series)
13599 );
13600 }
13601 });
13602 return allItems;
13603 },
13604
13605 /**
13606 * Adjust the chart margins by reserving space for the legend on only one side
13607 * of the chart. If the position is set to a corner, top or bottom is reserved
13608 * for horizontal legends and left or right for vertical ones.
13609 */
13610 adjustMargins: function(margin, spacing) {
13611 var chart = this.chart,
13612 options = this.options,
13613 // Use the first letter of each alignment option in order to detect the side
13614 alignment = options.align.charAt(0) + options.verticalAlign.charAt(0) + options.layout.charAt(0); // #4189 - use charAt(x) notation instead of [x] for IE7
13615
13616 if (!options.floating) {
13617
13618 each([
13619 /(lth|ct|rth)/,
13620 /(rtv|rm|rbv)/,
13621 /(rbh|cb|lbh)/,
13622 /(lbv|lm|ltv)/
13623 ], function(alignments, side) {
13624 if (alignments.test(alignment) && !defined(margin[side])) {
13625 // Now we have detected on which side of the chart we should reserve space for the legend
13626 chart[marginNames[side]] = Math.max(
13627 chart[marginNames[side]],
13628 chart.legend[(side + 1) % 2 ? 'legendHeight' : 'legendWidth'] + [1, -1, -1, 1][side] * options[(side % 2) ? 'x' : 'y'] +
13629 pick(options.margin, 12) +
13630 spacing[side]
13631 );
13632 }
13633 });
13634 }
13635 },
13636
13637 /**
13638 * Render the legend. This method can be called both before and after
13639 * chart.render. If called after, it will only rearrange items instead
13640 * of creating new ones.
13641 */
13642 render: function() {
13643 var legend = this,
13644 chart = legend.chart,
13645 renderer = chart.renderer,
13646 legendGroup = legend.group,
13647 allItems,
13648 display,
13649 legendWidth,
13650 legendHeight,
13651 box = legend.box,
13652 options = legend.options,
13653 padding = legend.padding;
13654
13655 legend.itemX = legend.initialItemX;
13656 legend.itemY = legend.initialItemY;
13657 legend.offsetWidth = 0;
13658 legend.lastItemY = 0;
13659
13660 if (!legendGroup) {
13661 legend.group = legendGroup = renderer.g('legend')
13662 .attr({
13663 zIndex: 7
13664 })
13665 .add();
13666 legend.contentGroup = renderer.g()
13667 .attr({
13668 zIndex: 1
13669 }) // above background
13670 .add(legendGroup);
13671 legend.scrollGroup = renderer.g()
13672 .add(legend.contentGroup);
13673 }
13674
13675 legend.renderTitle();
13676
13677 // add each series or point
13678 allItems = legend.getAllItems();
13679
13680 // sort by legendIndex
13681 stableSort(allItems, function(a, b) {
13682 return ((a.options && a.options.legendIndex) || 0) - ((b.options && b.options.legendIndex) || 0);
13683 });
13684
13685 // reversed legend
13686 if (options.reversed) {
13687 allItems.reverse();
13688 }
13689
13690 legend.allItems = allItems;
13691 legend.display = display = !!allItems.length;
13692
13693 // render the items
13694 legend.lastLineHeight = 0;
13695 each(allItems, function(item) {
13696 legend.renderItem(item);
13697 });
13698
13699 // Get the box
13700 legendWidth = (options.width || legend.offsetWidth) + padding;
13701 legendHeight = legend.lastItemY + legend.lastLineHeight + legend.titleHeight;
13702 legendHeight = legend.handleOverflow(legendHeight);
13703 legendHeight += padding;
13704
13705 // Draw the border and/or background
13706 if (!box) {
13707 legend.box = box = renderer.rect()
13708 .addClass('highcharts-legend-box')
13709 .attr({
13710 r: options.borderRadius
13711 })
13712 .add(legendGroup);
13713 box.isNew = true;
13714 }
13715
13716
13717 // Presentational
13718 box
13719 .attr({
13720 stroke: options.borderColor,
13721 'stroke-width': options.borderWidth || 0,
13722 fill: options.backgroundColor || 'none'
13723 })
13724 .shadow(options.shadow);
13725
13726
13727 if (legendWidth > 0 && legendHeight > 0) {
13728 box[box.isNew ? 'attr' : 'animate'](
13729 box.crisp({
13730 x: 0,
13731 y: 0,
13732 width: legendWidth,
13733 height: legendHeight
13734 }, box.strokeWidth())
13735 );
13736 box.isNew = false;
13737 }
13738
13739 // hide the border if no items
13740 box[display ? 'show' : 'hide']();
13741
13742
13743
13744 legend.legendWidth = legendWidth;
13745 legend.legendHeight = legendHeight;
13746
13747 // Now that the legend width and height are established, put the items in the
13748 // final position
13749 each(allItems, function(item) {
13750 legend.positionItem(item);
13751 });
13752
13753 // 1.x compatibility: positioning based on style
13754 /*var props = ['left', 'right', 'top', 'bottom'],
13755 prop,
13756 i = 4;
13757 while (i--) {
13758 prop = props[i];
13759 if (options.style[prop] && options.style[prop] !== 'auto') {
13760 options[i < 2 ? 'align' : 'verticalAlign'] = prop;
13761 options[i < 2 ? 'x' : 'y'] = pInt(options.style[prop]) * (i % 2 ? -1 : 1);
13762 }
13763 }*/
13764
13765 if (display) {
13766 legendGroup.align(extend({
13767 width: legendWidth,
13768 height: legendHeight
13769 }, options), true, 'spacingBox');
13770 }
13771
13772 if (!chart.isResizing) {
13773 this.positionCheckboxes();
13774 }
13775 },
13776
13777 /**
13778 * Set up the overflow handling by adding navigation with up and down arrows below the
13779 * legend.
13780 */
13781 handleOverflow: function(legendHeight) {
13782 var legend = this,
13783 chart = this.chart,
13784 renderer = chart.renderer,
13785 options = this.options,
13786 optionsY = options.y,
13787 alignTop = options.verticalAlign === 'top',
13788 spaceHeight = chart.spacingBox.height + (alignTop ? -optionsY : optionsY) - this.padding,
13789 maxHeight = options.maxHeight,
13790 clipHeight,
13791 clipRect = this.clipRect,
13792 navOptions = options.navigation,
13793 animation = pick(navOptions.animation, true),
13794 arrowSize = navOptions.arrowSize || 12,
13795 nav = this.nav,
13796 pages = this.pages,
13797 padding = this.padding,
13798 lastY,
13799 allItems = this.allItems,
13800 clipToHeight = function(height) {
13801 if (height) {
13802 clipRect.attr({
13803 height: height
13804 });
13805 } else if (clipRect) { // Reset (#5912)
13806 legend.clipRect = clipRect.destroy();
13807 legend.contentGroup.clip();
13808 }
13809
13810 // useHTML
13811 if (legend.contentGroup.div) {
13812 legend.contentGroup.div.style.clip = height ?
13813 'rect(' + padding + 'px,9999px,' +
13814 (padding + height) + 'px,0)' :
13815 'auto';
13816 }
13817 };
13818
13819
13820 // Adjust the height
13821 if (options.layout === 'horizontal' && options.verticalAlign !== 'middle' && !options.floating) {
13822 spaceHeight /= 2;
13823 }
13824 if (maxHeight) {
13825 spaceHeight = Math.min(spaceHeight, maxHeight);
13826 }
13827
13828 // Reset the legend height and adjust the clipping rectangle
13829 pages.length = 0;
13830 if (legendHeight > spaceHeight && navOptions.enabled !== false) {
13831
13832 this.clipHeight = clipHeight = Math.max(spaceHeight - 20 - this.titleHeight - padding, 0);
13833 this.currentPage = pick(this.currentPage, 1);
13834 this.fullHeight = legendHeight;
13835
13836 // Fill pages with Y positions so that the top of each a legend item defines
13837 // the scroll top for each page (#2098)
13838 each(allItems, function(item, i) {
13839 var y = item._legendItemPos[1],
13840 h = Math.round(item.legendItem.getBBox().height),
13841 len = pages.length;
13842
13843 if (!len || (y - pages[len - 1] > clipHeight && (lastY || y) !== pages[len - 1])) {
13844 pages.push(lastY || y);
13845 len++;
13846 }
13847
13848 if (i === allItems.length - 1 && y + h - pages[len - 1] > clipHeight) {
13849 pages.push(y);
13850 }
13851 if (y !== lastY) {
13852 lastY = y;
13853 }
13854 });
13855
13856 // Only apply clipping if needed. Clipping causes blurred legend in PDF export (#1787)
13857 if (!clipRect) {
13858 clipRect = legend.clipRect = renderer.clipRect(0, padding, 9999, 0);
13859 legend.contentGroup.clip(clipRect);
13860 }
13861
13862 clipToHeight(clipHeight);
13863
13864 // Add navigation elements
13865 if (!nav) {
13866 this.nav = nav = renderer.g().attr({
13867 zIndex: 1
13868 }).add(this.group);
13869 this.up = renderer.symbol('triangle', 0, 0, arrowSize, arrowSize)
13870 .on('click', function() {
13871 legend.scroll(-1, animation);
13872 })
13873 .add(nav);
13874 this.pager = renderer.text('', 15, 10)
13875 .addClass('highcharts-legend-navigation')
13876
13877 .css(navOptions.style)
13878
13879 .add(nav);
13880 this.down = renderer.symbol('triangle-down', 0, 0, arrowSize, arrowSize)
13881 .on('click', function() {
13882 legend.scroll(1, animation);
13883 })
13884 .add(nav);
13885 }
13886
13887 // Set initial position
13888 legend.scroll(0);
13889
13890 legendHeight = spaceHeight;
13891
13892 // Reset
13893 } else if (nav) {
13894 clipToHeight();
13895 nav.hide();
13896 this.scrollGroup.attr({
13897 translateY: 1
13898 });
13899 this.clipHeight = 0; // #1379
13900 }
13901
13902 return legendHeight;
13903 },
13904
13905 /**
13906 * Scroll the legend by a number of pages
13907 * @param {Object} scrollBy
13908 * @param {Object} animation
13909 */
13910 scroll: function(scrollBy, animation) {
13911 var pages = this.pages,
13912 pageCount = pages.length,
13913 currentPage = this.currentPage + scrollBy,
13914 clipHeight = this.clipHeight,
13915 navOptions = this.options.navigation,
13916 pager = this.pager,
13917 padding = this.padding,
13918 scrollOffset;
13919
13920 // When resizing while looking at the last page
13921 if (currentPage > pageCount) {
13922 currentPage = pageCount;
13923 }
13924
13925 if (currentPage > 0) {
13926
13927 if (animation !== undefined) {
13928 setAnimation(animation, this.chart);
13929 }
13930
13931 this.nav.attr({
13932 translateX: padding,
13933 translateY: clipHeight + this.padding + 7 + this.titleHeight,
13934 visibility: 'visible'
13935 });
13936 this.up.attr({
13937 'class': currentPage === 1 ? 'highcharts-legend-nav-inactive' : 'highcharts-legend-nav-active'
13938 });
13939 pager.attr({
13940 text: currentPage + '/' + pageCount
13941 });
13942 this.down.attr({
13943 'x': 18 + this.pager.getBBox().width, // adjust to text width
13944 'class': currentPage === pageCount ? 'highcharts-legend-nav-inactive' : 'highcharts-legend-nav-active'
13945 });
13946
13947
13948 this.up
13949 .attr({
13950 fill: currentPage === 1 ? navOptions.inactiveColor : navOptions.activeColor
13951 })
13952 .css({
13953 cursor: currentPage === 1 ? 'default' : 'pointer'
13954 });
13955 this.down
13956 .attr({
13957 fill: currentPage === pageCount ? navOptions.inactiveColor : navOptions.activeColor
13958 })
13959 .css({
13960 cursor: currentPage === pageCount ? 'default' : 'pointer'
13961 });
13962
13963
13964 scrollOffset = -pages[currentPage - 1] + this.initialItemY;
13965
13966 this.scrollGroup.animate({
13967 translateY: scrollOffset
13968 });
13969
13970 this.currentPage = currentPage;
13971 this.positionCheckboxes(scrollOffset);
13972 }
13973
13974 }
13975
13976 };
13977
13978 /*
13979 * LegendSymbolMixin
13980 */
13981
13982 H.LegendSymbolMixin = {
13983
13984 /**
13985 * Get the series' symbol in the legend
13986 *
13987 * @param {Object} legend The legend object
13988 * @param {Object} item The series (this) or point
13989 */
13990 drawRectangle: function(legend, item) {
13991 var options = legend.options,
13992 symbolHeight = options.symbolHeight || legend.fontMetrics.f,
13993 square = options.squareSymbol,
13994 symbolWidth = square ? symbolHeight : legend.symbolWidth;
13995
13996 item.legendSymbol = this.chart.renderer.rect(
13997 square ? (legend.symbolWidth - symbolHeight) / 2 : 0,
13998 legend.baseline - symbolHeight + 1, // #3988
13999 symbolWidth,
14000 symbolHeight,
14001 pick(legend.options.symbolRadius, symbolHeight / 2)
14002 )
14003 .addClass('highcharts-point')
14004 .attr({
14005 zIndex: 3
14006 }).add(item.legendGroup);
14007
14008 },
14009
14010 /**
14011 * Get the series' symbol in the legend. This method should be overridable to create custom
14012 * symbols through Highcharts.seriesTypes[type].prototype.drawLegendSymbols.
14013 *
14014 * @param {Object} legend The legend object
14015 */
14016 drawLineMarker: function(legend) {
14017
14018 var options = this.options,
14019 markerOptions = options.marker,
14020 radius,
14021 legendSymbol,
14022 symbolWidth = legend.symbolWidth,
14023 renderer = this.chart.renderer,
14024 legendItemGroup = this.legendGroup,
14025 verticalCenter = legend.baseline - Math.round(legend.fontMetrics.b * 0.3),
14026 attr = {};
14027
14028 // Draw the line
14029
14030 attr = {
14031 'stroke-width': options.lineWidth || 0
14032 };
14033 if (options.dashStyle) {
14034 attr.dashstyle = options.dashStyle;
14035 }
14036
14037
14038 this.legendLine = renderer.path([
14039 'M',
14040 0,
14041 verticalCenter,
14042 'L',
14043 symbolWidth,
14044 verticalCenter
14045 ])
14046 .addClass('highcharts-graph')
14047 .attr(attr)
14048 .add(legendItemGroup);
14049
14050 // Draw the marker
14051 if (markerOptions && markerOptions.enabled !== false) {
14052 radius = this.symbol.indexOf('url') === 0 ? 0 : markerOptions.radius;
14053 this.legendSymbol = legendSymbol = renderer.symbol(
14054 this.symbol,
14055 (symbolWidth / 2) - radius,
14056 verticalCenter - radius,
14057 2 * radius,
14058 2 * radius,
14059 markerOptions
14060 )
14061 .addClass('highcharts-point')
14062 .add(legendItemGroup);
14063 legendSymbol.isMarker = true;
14064 }
14065 }
14066 };
14067
14068 // Workaround for #2030, horizontal legend items not displaying in IE11 Preview,
14069 // and for #2580, a similar drawing flaw in Firefox 26.
14070 // Explore if there's a general cause for this. The problem may be related
14071 // to nested group elements, as the legend item texts are within 4 group elements.
14072 if (/Trident\/7\.0/.test(win.navigator.userAgent) || isFirefox) {
14073 wrap(Legend.prototype, 'positionItem', function(proceed, item) {
14074 var legend = this,
14075 runPositionItem = function() { // If chart destroyed in sync, this is undefined (#2030)
14076 if (item._legendItemPos) {
14077 proceed.call(legend, item);
14078 }
14079 };
14080
14081 // Do it now, for export and to get checkbox placement
14082 runPositionItem();
14083
14084 // Do it after to work around the core issue
14085 setTimeout(runPositionItem);
14086 });
14087 }
14088
14089 }(Highcharts));
14090 (function(H) {
14091 /**
14092 * (c) 2010-2016 Torstein Honsi
14093 *
14094 * License: www.highcharts.com/license
14095 */
14096 'use strict';
14097 var addEvent = H.addEvent,
14098 animate = H.animate,
14099 animObject = H.animObject,
14100 attr = H.attr,
14101 doc = H.doc,
14102 Axis = H.Axis, // @todo add as requirement
14103 createElement = H.createElement,
14104 defaultOptions = H.defaultOptions,
14105 discardElement = H.discardElement,
14106 charts = H.charts,
14107 css = H.css,
14108 defined = H.defined,
14109 each = H.each,
14110 error = H.error,
14111 extend = H.extend,
14112 fireEvent = H.fireEvent,
14113 getStyle = H.getStyle,
14114 grep = H.grep,
14115 isNumber = H.isNumber,
14116 isObject = H.isObject,
14117 isString = H.isString,
14118 Legend = H.Legend, // @todo add as requirement
14119 marginNames = H.marginNames,
14120 merge = H.merge,
14121 Pointer = H.Pointer, // @todo add as requirement
14122 pick = H.pick,
14123 pInt = H.pInt,
14124 removeEvent = H.removeEvent,
14125 seriesTypes = H.seriesTypes,
14126 splat = H.splat,
14127 svg = H.svg,
14128 syncTimeout = H.syncTimeout,
14129 win = H.win,
14130 Renderer = H.Renderer;
14131 /**
14132 * The Chart class.
14133 * @class Highcharts.Chart
14134 * @memberOf Highcharts
14135 * @param {String|HTMLDOMElement} renderTo - The DOM element to render to, or its
14136 * id.
14137 * @param {ChartOptions} options - The chart options structure.
14138 * @param {Function} callback - Function to run when the chart has loaded.
14139 */
14140 var Chart = H.Chart = function() {
14141 this.getArgs.apply(this, arguments);
14142 };
14143
14144 H.chart = function(a, b, c) {
14145 return new Chart(a, b, c);
14146 };
14147
14148 Chart.prototype = {
14149
14150 /**
14151 * Hook for modules
14152 */
14153 callbacks: [],
14154
14155 /**
14156 * Handle the arguments passed to the constructor
14157 * @returns {Array} Arguments without renderTo
14158 */
14159 getArgs: function() {
14160 var args = [].slice.call(arguments);
14161
14162 // Remove the optional first argument, renderTo, and
14163 // set it on this.
14164 if (isString(args[0]) || args[0].nodeName) {
14165 this.renderTo = args.shift();
14166 }
14167 this.init(args[0], args[1]);
14168 },
14169
14170 /**
14171 * Initialize the chart
14172 */
14173 init: function(userOptions, callback) {
14174
14175 // Handle regular options
14176 var options,
14177 seriesOptions = userOptions.series; // skip merging data points to increase performance
14178
14179 userOptions.series = null;
14180 options = merge(defaultOptions, userOptions); // do the merge
14181 options.series = userOptions.series = seriesOptions; // set back the series data
14182 this.userOptions = userOptions;
14183 this.respRules = [];
14184
14185 var optionsChart = options.chart;
14186
14187 var chartEvents = optionsChart.events;
14188
14189 this.margin = [];
14190 this.spacing = [];
14191
14192 //this.runChartClick = chartEvents && !!chartEvents.click;
14193 this.bounds = {
14194 h: {},
14195 v: {}
14196 }; // Pixel data bounds for touch zoom
14197
14198 this.callback = callback;
14199 this.isResizing = 0;
14200 this.options = options;
14201 //chartTitleOptions = undefined;
14202 //chartSubtitleOptions = undefined;
14203
14204 this.axes = [];
14205 this.series = [];
14206 this.hasCartesianSeries = optionsChart.showAxes;
14207 //this.axisOffset = undefined;
14208 //this.inverted = undefined;
14209 //this.loadingShown = undefined;
14210 //this.container = undefined;
14211 //this.chartWidth = undefined;
14212 //this.chartHeight = undefined;
14213 //this.marginRight = undefined;
14214 //this.marginBottom = undefined;
14215 //this.containerWidth = undefined;
14216 //this.containerHeight = undefined;
14217 //this.oldChartWidth = undefined;
14218 //this.oldChartHeight = undefined;
14219
14220 //this.renderTo = undefined;
14221 //this.renderToClone = undefined;
14222
14223 //this.spacingBox = undefined
14224
14225 //this.legend = undefined;
14226
14227 // Elements
14228 //this.chartBackground = undefined;
14229 //this.plotBackground = undefined;
14230 //this.plotBGImage = undefined;
14231 //this.plotBorder = undefined;
14232 //this.loadingDiv = undefined;
14233 //this.loadingSpan = undefined;
14234
14235 var chart = this,
14236 eventType;
14237
14238 // Add the chart to the global lookup
14239 chart.index = charts.length;
14240 charts.push(chart);
14241 H.chartCount++;
14242
14243 // Chart event handlers
14244 if (chartEvents) {
14245 for (eventType in chartEvents) {
14246 addEvent(chart, eventType, chartEvents[eventType]);
14247 }
14248 }
14249
14250 chart.xAxis = [];
14251 chart.yAxis = [];
14252
14253 chart.pointCount = chart.colorCounter = chart.symbolCounter = 0;
14254
14255 chart.firstRender();
14256 },
14257
14258 /**
14259 * Initialize an individual series, called internally before render time
14260 */
14261 initSeries: function(options) {
14262 var chart = this,
14263 optionsChart = chart.options.chart,
14264 type = options.type || optionsChart.type || optionsChart.defaultSeriesType,
14265 series,
14266 Constr = seriesTypes[type];
14267
14268 // No such series type
14269 if (!Constr) {
14270 error(17, true);
14271 }
14272
14273 series = new Constr();
14274 series.init(this, options);
14275 return series;
14276 },
14277
14278 /**
14279 * Check whether a given point is within the plot area
14280 *
14281 * @param {Number} plotX Pixel x relative to the plot area
14282 * @param {Number} plotY Pixel y relative to the plot area
14283 * @param {Boolean} inverted Whether the chart is inverted
14284 */
14285 isInsidePlot: function(plotX, plotY, inverted) {
14286 var x = inverted ? plotY : plotX,
14287 y = inverted ? plotX : plotY;
14288
14289 return x >= 0 &&
14290 x <= this.plotWidth &&
14291 y >= 0 &&
14292 y <= this.plotHeight;
14293 },
14294
14295 /**
14296 * Redraw legend, axes or series based on updated data
14297 *
14298 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
14299 * configuration
14300 */
14301 redraw: function(animation) {
14302 var chart = this,
14303 axes = chart.axes,
14304 series = chart.series,
14305 pointer = chart.pointer,
14306 legend = chart.legend,
14307 redrawLegend = chart.isDirtyLegend,
14308 hasStackedSeries,
14309 hasDirtyStacks,
14310 hasCartesianSeries = chart.hasCartesianSeries,
14311 isDirtyBox = chart.isDirtyBox,
14312 seriesLength = series.length,
14313 i = seriesLength,
14314 serie,
14315 renderer = chart.renderer,
14316 isHiddenChart = renderer.isHidden(),
14317 afterRedraw = [];
14318
14319 H.setAnimation(animation, chart);
14320
14321 if (isHiddenChart) {
14322 chart.cloneRenderTo();
14323 }
14324
14325 // Adjust title layout (reflow multiline text)
14326 chart.layOutTitles();
14327
14328 // link stacked series
14329 while (i--) {
14330 serie = series[i];
14331
14332 if (serie.options.stacking) {
14333 hasStackedSeries = true;
14334
14335 if (serie.isDirty) {
14336 hasDirtyStacks = true;
14337 break;
14338 }
14339 }
14340 }
14341 if (hasDirtyStacks) { // mark others as dirty
14342 i = seriesLength;
14343 while (i--) {
14344 serie = series[i];
14345 if (serie.options.stacking) {
14346 serie.isDirty = true;
14347 }
14348 }
14349 }
14350
14351 // Handle updated data in the series
14352 each(series, function(serie) {
14353 if (serie.isDirty) {
14354 if (serie.options.legendType === 'point') {
14355 if (serie.updateTotals) {
14356 serie.updateTotals();
14357 }
14358 redrawLegend = true;
14359 }
14360 }
14361 if (serie.isDirtyData) {
14362 fireEvent(serie, 'updatedData');
14363 }
14364 });
14365
14366 // handle added or removed series
14367 if (redrawLegend && legend.options.enabled) { // series or pie points are added or removed
14368 // draw legend graphics
14369 legend.render();
14370
14371 chart.isDirtyLegend = false;
14372 }
14373
14374 // reset stacks
14375 if (hasStackedSeries) {
14376 chart.getStacks();
14377 }
14378
14379
14380 if (hasCartesianSeries) {
14381 // set axes scales
14382 each(axes, function(axis) {
14383 axis.updateNames();
14384 axis.setScale();
14385 });
14386 }
14387
14388 chart.getMargins(); // #3098
14389
14390 if (hasCartesianSeries) {
14391 // If one axis is dirty, all axes must be redrawn (#792, #2169)
14392 each(axes, function(axis) {
14393 if (axis.isDirty) {
14394 isDirtyBox = true;
14395 }
14396 });
14397
14398 // redraw axes
14399 each(axes, function(axis) {
14400
14401 // Fire 'afterSetExtremes' only if extremes are set
14402 var key = axis.min + ',' + axis.max;
14403 if (axis.extKey !== key) { // #821, #4452
14404 axis.extKey = key;
14405 afterRedraw.push(function() { // prevent a recursive call to chart.redraw() (#1119)
14406 fireEvent(axis, 'afterSetExtremes', extend(axis.eventArgs, axis.getExtremes())); // #747, #751
14407 delete axis.eventArgs;
14408 });
14409 }
14410 if (isDirtyBox || hasStackedSeries) {
14411 axis.redraw();
14412 }
14413 });
14414 }
14415
14416 // the plot areas size has changed
14417 if (isDirtyBox) {
14418 chart.drawChartBox();
14419 }
14420
14421
14422 // redraw affected series
14423 each(series, function(serie) {
14424 if ((isDirtyBox || serie.isDirty) && serie.visible) {
14425 serie.redraw();
14426 }
14427 });
14428
14429 // move tooltip or reset
14430 if (pointer) {
14431 pointer.reset(true);
14432 }
14433
14434 // redraw if canvas
14435 renderer.draw();
14436
14437 // fire the event
14438 fireEvent(chart, 'redraw');
14439
14440 if (isHiddenChart) {
14441 chart.cloneRenderTo(true);
14442 }
14443
14444 // Fire callbacks that are put on hold until after the redraw
14445 each(afterRedraw, function(callback) {
14446 callback.call();
14447 });
14448 },
14449
14450 /**
14451 * Get an axis, series or point object by id.
14452 * @param id {String} The id as given in the configuration options
14453 */
14454 get: function(id) {
14455 var chart = this,
14456 axes = chart.axes,
14457 series = chart.series;
14458
14459 var i,
14460 j,
14461 points;
14462
14463 // search axes
14464 for (i = 0; i < axes.length; i++) {
14465 if (axes[i].options.id === id) {
14466 return axes[i];
14467 }
14468 }
14469
14470 // search series
14471 for (i = 0; i < series.length; i++) {
14472 if (series[i].options.id === id) {
14473 return series[i];
14474 }
14475 }
14476
14477 // search points
14478 for (i = 0; i < series.length; i++) {
14479 points = series[i].points || [];
14480 for (j = 0; j < points.length; j++) {
14481 if (points[j].id === id) {
14482 return points[j];
14483 }
14484 }
14485 }
14486 return null;
14487 },
14488
14489 /**
14490 * Create the Axis instances based on the config options
14491 */
14492 getAxes: function() {
14493 var chart = this,
14494 options = this.options,
14495 xAxisOptions = options.xAxis = splat(options.xAxis || {}),
14496 yAxisOptions = options.yAxis = splat(options.yAxis || {}),
14497 optionsArray;
14498
14499 // make sure the options are arrays and add some members
14500 each(xAxisOptions, function(axis, i) {
14501 axis.index = i;
14502 axis.isX = true;
14503 });
14504
14505 each(yAxisOptions, function(axis, i) {
14506 axis.index = i;
14507 });
14508
14509 // concatenate all axis options into one array
14510 optionsArray = xAxisOptions.concat(yAxisOptions);
14511
14512 each(optionsArray, function(axisOptions) {
14513 new Axis(chart, axisOptions); // eslint-disable-line no-new
14514 });
14515 },
14516
14517
14518 /**
14519 * Get the currently selected points from all series
14520 */
14521 getSelectedPoints: function() {
14522 var points = [];
14523 each(this.series, function(serie) {
14524 points = points.concat(grep(serie.points || [], function(point) {
14525 return point.selected;
14526 }));
14527 });
14528 return points;
14529 },
14530
14531 /**
14532 * Get the currently selected series
14533 */
14534 getSelectedSeries: function() {
14535 return grep(this.series, function(serie) {
14536 return serie.selected;
14537 });
14538 },
14539
14540 /**
14541 * Show the title and subtitle of the chart
14542 *
14543 * @param titleOptions {Object} New title options
14544 * @param subtitleOptions {Object} New subtitle options
14545 *
14546 */
14547 setTitle: function(titleOptions, subtitleOptions, redraw) {
14548 var chart = this,
14549 options = chart.options,
14550 chartTitleOptions,
14551 chartSubtitleOptions;
14552
14553 chartTitleOptions = options.title = merge(
14554
14555 // Default styles
14556 {
14557 style: {
14558 color: '#333333',
14559 fontSize: options.isStock ? '16px' : '18px' // #2944
14560 }
14561 },
14562
14563 options.title,
14564 titleOptions
14565 );
14566 chartSubtitleOptions = options.subtitle = merge(
14567
14568 // Default styles
14569 {
14570 style: {
14571 color: '#666666'
14572 }
14573 },
14574
14575 options.subtitle,
14576 subtitleOptions
14577 );
14578
14579 // add title and subtitle
14580 each([
14581 ['title', titleOptions, chartTitleOptions],
14582 ['subtitle', subtitleOptions, chartSubtitleOptions]
14583 ], function(arr, i) {
14584 var name = arr[0],
14585 title = chart[name],
14586 titleOptions = arr[1],
14587 chartTitleOptions = arr[2];
14588
14589 if (title && titleOptions) {
14590 chart[name] = title = title.destroy(); // remove old
14591 }
14592
14593 if (chartTitleOptions && chartTitleOptions.text && !title) {
14594 chart[name] = chart.renderer.text(
14595 chartTitleOptions.text,
14596 0,
14597 0,
14598 chartTitleOptions.useHTML
14599 )
14600 .attr({
14601 align: chartTitleOptions.align,
14602 'class': 'highcharts-' + name,
14603 zIndex: chartTitleOptions.zIndex || 4
14604 })
14605 .add();
14606
14607 // Update methods, shortcut to Chart.setTitle
14608 chart[name].update = function(o) {
14609 chart.setTitle(!i && o, i && o);
14610 };
14611
14612
14613 // Presentational
14614 chart[name].css(chartTitleOptions.style);
14615
14616
14617 }
14618 });
14619 chart.layOutTitles(redraw);
14620 },
14621
14622 /**
14623 * Lay out the chart titles and cache the full offset height for use in getMargins
14624 */
14625 layOutTitles: function(redraw) {
14626 var titleOffset = 0,
14627 requiresDirtyBox,
14628 renderer = this.renderer,
14629 spacingBox = this.spacingBox;
14630
14631 // Lay out the title and the subtitle respectively
14632 each(['title', 'subtitle'], function(key) {
14633 var title = this[key],
14634 titleOptions = this.options[key],
14635 titleSize;
14636
14637 if (title) {
14638
14639 titleSize = titleOptions.style.fontSize;
14640
14641 titleSize = renderer.fontMetrics(titleSize, title).b;
14642
14643 title
14644 .css({
14645 width: (titleOptions.width || spacingBox.width + titleOptions.widthAdjust) + 'px'
14646 })
14647 .align(extend({
14648 y: titleOffset + titleSize + (key === 'title' ? -3 : 2)
14649 }, titleOptions), false, 'spacingBox');
14650
14651 if (!titleOptions.floating && !titleOptions.verticalAlign) {
14652 titleOffset = Math.ceil(titleOffset + title.getBBox().height);
14653 }
14654 }
14655 }, this);
14656
14657 requiresDirtyBox = this.titleOffset !== titleOffset;
14658 this.titleOffset = titleOffset; // used in getMargins
14659
14660 if (!this.isDirtyBox && requiresDirtyBox) {
14661 this.isDirtyBox = requiresDirtyBox;
14662 // Redraw if necessary (#2719, #2744)
14663 if (this.hasRendered && pick(redraw, true) && this.isDirtyBox) {
14664 this.redraw();
14665 }
14666 }
14667 },
14668
14669 /**
14670 * Get chart width and height according to options and container size
14671 */
14672 getChartSize: function() {
14673 var chart = this,
14674 optionsChart = chart.options.chart,
14675 widthOption = optionsChart.width,
14676 heightOption = optionsChart.height,
14677 renderTo = chart.renderToClone || chart.renderTo;
14678
14679 // Get inner width and height
14680 if (!defined(widthOption)) {
14681 chart.containerWidth = getStyle(renderTo, 'width');
14682 }
14683 if (!defined(heightOption)) {
14684 chart.containerHeight = getStyle(renderTo, 'height');
14685 }
14686
14687 chart.chartWidth = Math.max(0, widthOption || chart.containerWidth || 600); // #1393, 1460
14688 chart.chartHeight = Math.max(0, pick(heightOption,
14689 // the offsetHeight of an empty container is 0 in standard browsers, but 19 in IE7:
14690 chart.containerHeight > 19 ? chart.containerHeight : 400));
14691 },
14692
14693 /**
14694 * Create a clone of the chart's renderTo div and place it outside the viewport to allow
14695 * size computation on chart.render and chart.redraw
14696 */
14697 cloneRenderTo: function(revert) {
14698 var clone = this.renderToClone,
14699 container = this.container;
14700
14701 // Destroy the clone and bring the container back to the real renderTo div
14702 if (revert) {
14703 if (clone) {
14704 while (clone.childNodes.length) { // #5231
14705 this.renderTo.appendChild(clone.firstChild);
14706 }
14707 discardElement(clone);
14708 delete this.renderToClone;
14709 }
14710
14711 // Set up the clone
14712 } else {
14713 if (container && container.parentNode === this.renderTo) {
14714 this.renderTo.removeChild(container); // do not clone this
14715 }
14716 this.renderToClone = clone = this.renderTo.cloneNode(0);
14717 css(clone, {
14718 position: 'absolute',
14719 top: '-9999px',
14720 display: 'block' // #833
14721 });
14722 if (clone.style.setProperty) { // #2631
14723 clone.style.setProperty('display', 'block', 'important');
14724 }
14725 doc.body.appendChild(clone);
14726 if (container) {
14727 clone.appendChild(container);
14728 }
14729 }
14730 },
14731
14732 /**
14733 * Setter for the chart class name
14734 */
14735 setClassName: function(className) {
14736 this.container.className = 'highcharts-container ' + (className || '');
14737 },
14738
14739 /**
14740 * Get the containing element, determine the size and create the inner container
14741 * div to hold the chart
14742 */
14743 getContainer: function() {
14744 var chart = this,
14745 container,
14746 options = chart.options,
14747 optionsChart = options.chart,
14748 chartWidth,
14749 chartHeight,
14750 renderTo = chart.renderTo,
14751 indexAttrName = 'data-highcharts-chart',
14752 oldChartIndex,
14753 Ren,
14754 containerId = H.uniqueKey(),
14755 containerStyle,
14756 key;
14757
14758 if (!renderTo) {
14759 chart.renderTo = renderTo = optionsChart.renderTo;
14760 }
14761
14762 if (isString(renderTo)) {
14763 chart.renderTo = renderTo = doc.getElementById(renderTo);
14764 }
14765
14766 // Display an error if the renderTo is wrong
14767 if (!renderTo) {
14768 error(13, true);
14769 }
14770
14771 // If the container already holds a chart, destroy it. The check for hasRendered is there
14772 // because web pages that are saved to disk from the browser, will preserve the data-highcharts-chart
14773 // attribute and the SVG contents, but not an interactive chart. So in this case,
14774 // charts[oldChartIndex] will point to the wrong chart if any (#2609).
14775 oldChartIndex = pInt(attr(renderTo, indexAttrName));
14776 if (isNumber(oldChartIndex) && charts[oldChartIndex] && charts[oldChartIndex].hasRendered) {
14777 charts[oldChartIndex].destroy();
14778 }
14779
14780 // Make a reference to the chart from the div
14781 attr(renderTo, indexAttrName, chart.index);
14782
14783 // remove previous chart
14784 renderTo.innerHTML = '';
14785
14786 // If the container doesn't have an offsetWidth, it has or is a child of
14787 // a node that has display:none. We need to temporarily move it out to a
14788 // visible state to determine the size, else the legend and tooltips
14789 // won't render properly. The skipClone option is used in sparklines as
14790 // a micro optimization, saving about 1-2 ms each chart.
14791 if (!optionsChart.skipClone && !renderTo.offsetWidth) {
14792 chart.cloneRenderTo();
14793 }
14794
14795 // get the width and height
14796 chart.getChartSize();
14797 chartWidth = chart.chartWidth;
14798 chartHeight = chart.chartHeight;
14799
14800 // Create the inner container
14801
14802 containerStyle = extend({
14803 position: 'relative',
14804 overflow: 'hidden', // needed for context menu (avoid scrollbars) and
14805 // content overflow in IE
14806 width: chartWidth + 'px',
14807 height: chartHeight + 'px',
14808 textAlign: 'left',
14809 lineHeight: 'normal', // #427
14810 zIndex: 0, // #1072
14811 '-webkit-tap-highlight-color': 'rgba(0,0,0,0)'
14812 }, optionsChart.style);
14813
14814 chart.container = container = createElement(
14815 'div', {
14816 id: containerId
14817 },
14818 containerStyle,
14819 chart.renderToClone || renderTo
14820 );
14821
14822 // cache the cursor (#1650)
14823 chart._cursor = container.style.cursor;
14824
14825 // Initialize the renderer
14826 Ren = H[optionsChart.renderer] || Renderer;
14827 chart.renderer = new Ren(
14828 container,
14829 chartWidth,
14830 chartHeight,
14831 null,
14832 optionsChart.forExport,
14833 options.exporting && options.exporting.allowHTML
14834 );
14835
14836
14837 chart.setClassName(optionsChart.className);
14838
14839 chart.renderer.setStyle(optionsChart.style);
14840
14841
14842 // Add a reference to the charts index
14843 chart.renderer.chartIndex = chart.index;
14844 },
14845
14846 /**
14847 * Calculate margins by rendering axis labels in a preliminary position. Title,
14848 * subtitle and legend have already been rendered at this stage, but will be
14849 * moved into their final positions
14850 */
14851 getMargins: function(skipAxes) {
14852 var chart = this,
14853 spacing = chart.spacing,
14854 margin = chart.margin,
14855 titleOffset = chart.titleOffset;
14856
14857 chart.resetMargins();
14858
14859 // Adjust for title and subtitle
14860 if (titleOffset && !defined(margin[0])) {
14861 chart.plotTop = Math.max(chart.plotTop, titleOffset + chart.options.title.margin + spacing[0]);
14862 }
14863
14864 // Adjust for legend
14865 if (chart.legend.display) {
14866 chart.legend.adjustMargins(margin, spacing);
14867 }
14868
14869 // adjust for scroller
14870 if (chart.extraBottomMargin) {
14871 chart.marginBottom += chart.extraBottomMargin;
14872 }
14873 if (chart.extraTopMargin) {
14874 chart.plotTop += chart.extraTopMargin;
14875 }
14876 if (!skipAxes) {
14877 this.getAxisMargins();
14878 }
14879 },
14880
14881 getAxisMargins: function() {
14882
14883 var chart = this,
14884 axisOffset = chart.axisOffset = [0, 0, 0, 0], // top, right, bottom, left
14885 margin = chart.margin;
14886
14887 // pre-render axes to get labels offset width
14888 if (chart.hasCartesianSeries) {
14889 each(chart.axes, function(axis) {
14890 if (axis.visible) {
14891 axis.getOffset();
14892 }
14893 });
14894 }
14895
14896 // Add the axis offsets
14897 each(marginNames, function(m, side) {
14898 if (!defined(margin[side])) {
14899 chart[m] += axisOffset[side];
14900 }
14901 });
14902
14903 chart.setChartSize();
14904
14905 },
14906
14907 /**
14908 * Resize the chart to its container if size is not explicitly set
14909 */
14910 reflow: function(e) {
14911 var chart = this,
14912 optionsChart = chart.options.chart,
14913 renderTo = chart.renderTo,
14914 hasUserWidth = defined(optionsChart.width),
14915 width = optionsChart.width || getStyle(renderTo, 'width'),
14916 height = optionsChart.height || getStyle(renderTo, 'height'),
14917 target = e ? e.target : win;
14918
14919 // Width and height checks for display:none. Target is doc in IE8 and Opera,
14920 // win in Firefox, Chrome and IE9.
14921 if (!hasUserWidth && !chart.isPrinting && width && height && (target === win || target === doc)) { // #1093
14922 if (width !== chart.containerWidth || height !== chart.containerHeight) {
14923 clearTimeout(chart.reflowTimeout);
14924 // When called from window.resize, e is set, else it's called directly (#2224)
14925 chart.reflowTimeout = syncTimeout(function() {
14926 if (chart.container) { // It may have been destroyed in the meantime (#1257)
14927 chart.setSize(undefined, undefined, false);
14928 }
14929 }, e ? 100 : 0);
14930 }
14931 chart.containerWidth = width;
14932 chart.containerHeight = height;
14933 }
14934 },
14935
14936 /**
14937 * Add the event handlers necessary for auto resizing
14938 */
14939 initReflow: function() {
14940 var chart = this,
14941 unbind;
14942
14943 unbind = addEvent(win, 'resize', function(e) {
14944 chart.reflow(e);
14945 });
14946 addEvent(chart, 'destroy', unbind);
14947
14948 // The following will add listeners to re-fit the chart before and after
14949 // printing (#2284). However it only works in WebKit. Should have worked
14950 // in Firefox, but not supported in IE.
14951 /*
14952 if (win.matchMedia) {
14953 win.matchMedia('print').addListener(function reflow() {
14954 chart.reflow();
14955 });
14956 }
14957 */
14958 },
14959
14960 /**
14961 * Resize the chart to a given width and height
14962 * @param {Number} width
14963 * @param {Number} height
14964 * @param {Object|Boolean} animation
14965 */
14966 setSize: function(width, height, animation) {
14967 var chart = this,
14968 renderer = chart.renderer,
14969 globalAnimation;
14970
14971 // Handle the isResizing counter
14972 chart.isResizing += 1;
14973
14974 // set the animation for the current process
14975 H.setAnimation(animation, chart);
14976
14977 chart.oldChartHeight = chart.chartHeight;
14978 chart.oldChartWidth = chart.chartWidth;
14979 if (width !== undefined) {
14980 chart.options.chart.width = width;
14981 }
14982 if (height !== undefined) {
14983 chart.options.chart.height = height;
14984 }
14985 chart.getChartSize();
14986
14987 // Resize the container with the global animation applied if enabled (#2503)
14988
14989 globalAnimation = renderer.globalAnimation;
14990 (globalAnimation ? animate : css)(chart.container, {
14991 width: chart.chartWidth + 'px',
14992 height: chart.chartHeight + 'px'
14993 }, globalAnimation);
14994
14995
14996 chart.setChartSize(true);
14997 renderer.setSize(chart.chartWidth, chart.chartHeight, animation);
14998
14999 // handle axes
15000 each(chart.axes, function(axis) {
15001 axis.isDirty = true;
15002 axis.setScale();
15003 });
15004
15005 chart.isDirtyLegend = true; // force legend redraw
15006 chart.isDirtyBox = true; // force redraw of plot and chart border
15007
15008 chart.layOutTitles(); // #2857
15009 chart.getMargins();
15010
15011 if (chart.setResponsive) {
15012 chart.setResponsive(false);
15013 }
15014 chart.redraw(animation);
15015
15016
15017 chart.oldChartHeight = null;
15018 fireEvent(chart, 'resize');
15019
15020 // Fire endResize and set isResizing back. If animation is disabled, fire without delay
15021 syncTimeout(function() {
15022 if (chart) {
15023 fireEvent(chart, 'endResize', null, function() {
15024 chart.isResizing -= 1;
15025 });
15026 }
15027 }, animObject(globalAnimation).duration);
15028 },
15029
15030 /**
15031 * Set the public chart properties. This is done before and after the pre-render
15032 * to determine margin sizes
15033 */
15034 setChartSize: function(skipAxes) {
15035 var chart = this,
15036 inverted = chart.inverted,
15037 renderer = chart.renderer,
15038 chartWidth = chart.chartWidth,
15039 chartHeight = chart.chartHeight,
15040 optionsChart = chart.options.chart,
15041 spacing = chart.spacing,
15042 clipOffset = chart.clipOffset,
15043 clipX,
15044 clipY,
15045 plotLeft,
15046 plotTop,
15047 plotWidth,
15048 plotHeight,
15049 plotBorderWidth;
15050
15051 chart.plotLeft = plotLeft = Math.round(chart.plotLeft);
15052 chart.plotTop = plotTop = Math.round(chart.plotTop);
15053 chart.plotWidth = plotWidth = Math.max(0, Math.round(chartWidth - plotLeft - chart.marginRight));
15054 chart.plotHeight = plotHeight = Math.max(0, Math.round(chartHeight - plotTop - chart.marginBottom));
15055
15056 chart.plotSizeX = inverted ? plotHeight : plotWidth;
15057 chart.plotSizeY = inverted ? plotWidth : plotHeight;
15058
15059 chart.plotBorderWidth = optionsChart.plotBorderWidth || 0;
15060
15061 // Set boxes used for alignment
15062 chart.spacingBox = renderer.spacingBox = {
15063 x: spacing[3],
15064 y: spacing[0],
15065 width: chartWidth - spacing[3] - spacing[1],
15066 height: chartHeight - spacing[0] - spacing[2]
15067 };
15068 chart.plotBox = renderer.plotBox = {
15069 x: plotLeft,
15070 y: plotTop,
15071 width: plotWidth,
15072 height: plotHeight
15073 };
15074
15075 plotBorderWidth = 2 * Math.floor(chart.plotBorderWidth / 2);
15076 clipX = Math.ceil(Math.max(plotBorderWidth, clipOffset[3]) / 2);
15077 clipY = Math.ceil(Math.max(plotBorderWidth, clipOffset[0]) / 2);
15078 chart.clipBox = {
15079 x: clipX,
15080 y: clipY,
15081 width: Math.floor(chart.plotSizeX - Math.max(plotBorderWidth, clipOffset[1]) / 2 - clipX),
15082 height: Math.max(0, Math.floor(chart.plotSizeY - Math.max(plotBorderWidth, clipOffset[2]) / 2 - clipY))
15083 };
15084
15085 if (!skipAxes) {
15086 each(chart.axes, function(axis) {
15087 axis.setAxisSize();
15088 axis.setAxisTranslation();
15089 });
15090 }
15091 },
15092
15093 /**
15094 * Initial margins before auto size margins are applied
15095 */
15096 resetMargins: function() {
15097 var chart = this,
15098 chartOptions = chart.options.chart;
15099
15100 // Create margin and spacing array
15101 each(['margin', 'spacing'], function splashArrays(target) {
15102 var value = chartOptions[target],
15103 values = isObject(value) ? value : [value, value, value, value];
15104
15105 each(['Top', 'Right', 'Bottom', 'Left'], function(sideName, side) {
15106 chart[target][side] = pick(chartOptions[target + sideName], values[side]);
15107 });
15108 });
15109
15110 // Set margin names like chart.plotTop, chart.plotLeft, chart.marginRight, chart.marginBottom.
15111 each(marginNames, function(m, side) {
15112 chart[m] = pick(chart.margin[side], chart.spacing[side]);
15113 });
15114 chart.axisOffset = [0, 0, 0, 0]; // top, right, bottom, left
15115 chart.clipOffset = [0, 0, 0, 0];
15116 },
15117
15118 /**
15119 * Draw the borders and backgrounds for chart and plot area
15120 */
15121 drawChartBox: function() {
15122 var chart = this,
15123 optionsChart = chart.options.chart,
15124 renderer = chart.renderer,
15125 chartWidth = chart.chartWidth,
15126 chartHeight = chart.chartHeight,
15127 chartBackground = chart.chartBackground,
15128 plotBackground = chart.plotBackground,
15129 plotBorder = chart.plotBorder,
15130 chartBorderWidth,
15131
15132 plotBGImage = chart.plotBGImage,
15133 chartBackgroundColor = optionsChart.backgroundColor,
15134 plotBackgroundColor = optionsChart.plotBackgroundColor,
15135 plotBackgroundImage = optionsChart.plotBackgroundImage,
15136
15137 mgn,
15138 bgAttr,
15139 plotLeft = chart.plotLeft,
15140 plotTop = chart.plotTop,
15141 plotWidth = chart.plotWidth,
15142 plotHeight = chart.plotHeight,
15143 plotBox = chart.plotBox,
15144 clipRect = chart.clipRect,
15145 clipBox = chart.clipBox,
15146 verb = 'animate';
15147
15148 // Chart area
15149 if (!chartBackground) {
15150 chart.chartBackground = chartBackground = renderer.rect()
15151 .addClass('highcharts-background')
15152 .add();
15153 verb = 'attr';
15154 }
15155
15156
15157 // Presentational
15158 chartBorderWidth = optionsChart.borderWidth || 0;
15159 mgn = chartBorderWidth + (optionsChart.shadow ? 8 : 0);
15160
15161 bgAttr = {
15162 fill: chartBackgroundColor || 'none'
15163 };
15164
15165 if (chartBorderWidth || chartBackground['stroke-width']) { // #980
15166 bgAttr.stroke = optionsChart.borderColor;
15167 bgAttr['stroke-width'] = chartBorderWidth;
15168 }
15169 chartBackground
15170 .attr(bgAttr)
15171 .shadow(optionsChart.shadow);
15172
15173 chartBackground[verb]({
15174 x: mgn / 2,
15175 y: mgn / 2,
15176 width: chartWidth - mgn - chartBorderWidth % 2,
15177 height: chartHeight - mgn - chartBorderWidth % 2,
15178 r: optionsChart.borderRadius
15179 });
15180
15181 // Plot background
15182 verb = 'animate';
15183 if (!plotBackground) {
15184 verb = 'attr';
15185 chart.plotBackground = plotBackground = renderer.rect()
15186 .addClass('highcharts-plot-background')
15187 .add();
15188 }
15189 plotBackground[verb](plotBox);
15190
15191
15192 // Presentational attributes for the background
15193 plotBackground
15194 .attr({
15195 fill: plotBackgroundColor || 'none'
15196 })
15197 .shadow(optionsChart.plotShadow);
15198
15199 // Create the background image
15200 if (plotBackgroundImage) {
15201 if (!plotBGImage) {
15202 chart.plotBGImage = renderer.image(plotBackgroundImage, plotLeft, plotTop, plotWidth, plotHeight)
15203 .add();
15204 } else {
15205 plotBGImage.animate(plotBox);
15206 }
15207 }
15208
15209
15210 // Plot clip
15211 if (!clipRect) {
15212 chart.clipRect = renderer.clipRect(clipBox);
15213 } else {
15214 clipRect.animate({
15215 width: clipBox.width,
15216 height: clipBox.height
15217 });
15218 }
15219
15220 // Plot area border
15221 verb = 'animate';
15222 if (!plotBorder) {
15223 verb = 'attr';
15224 chart.plotBorder = plotBorder = renderer.rect()
15225 .addClass('highcharts-plot-border')
15226 .attr({
15227 zIndex: 1 // Above the grid
15228 })
15229 .add();
15230 }
15231
15232
15233 // Presentational
15234 plotBorder.attr({
15235 stroke: optionsChart.plotBorderColor,
15236 'stroke-width': optionsChart.plotBorderWidth || 0,
15237 fill: 'none'
15238 });
15239
15240
15241 plotBorder[verb](plotBorder.crisp({
15242 x: plotLeft,
15243 y: plotTop,
15244 width: plotWidth,
15245 height: plotHeight
15246 }, -plotBorder.strokeWidth())); //#3282 plotBorder should be negative;
15247
15248 // reset
15249 chart.isDirtyBox = false;
15250 },
15251
15252 /**
15253 * Detect whether a certain chart property is needed based on inspecting its options
15254 * and series. This mainly applies to the chart.inverted property, and in extensions to
15255 * the chart.angular and chart.polar properties.
15256 */
15257 propFromSeries: function() {
15258 var chart = this,
15259 optionsChart = chart.options.chart,
15260 klass,
15261 seriesOptions = chart.options.series,
15262 i,
15263 value;
15264
15265
15266 each(['inverted', 'angular', 'polar'], function(key) {
15267
15268 // The default series type's class
15269 klass = seriesTypes[optionsChart.type || optionsChart.defaultSeriesType];
15270
15271 // Get the value from available chart-wide properties
15272 value =
15273 optionsChart[key] || // It is set in the options
15274 (klass && klass.prototype[key]); // The default series class requires it
15275
15276 // 4. Check if any the chart's series require it
15277 i = seriesOptions && seriesOptions.length;
15278 while (!value && i--) {
15279 klass = seriesTypes[seriesOptions[i].type];
15280 if (klass && klass.prototype[key]) {
15281 value = true;
15282 }
15283 }
15284
15285 // Set the chart property
15286 chart[key] = value;
15287 });
15288
15289 },
15290
15291 /**
15292 * Link two or more series together. This is done initially from Chart.render,
15293 * and after Chart.addSeries and Series.remove.
15294 */
15295 linkSeries: function() {
15296 var chart = this,
15297 chartSeries = chart.series;
15298
15299 // Reset links
15300 each(chartSeries, function(series) {
15301 series.linkedSeries.length = 0;
15302 });
15303
15304 // Apply new links
15305 each(chartSeries, function(series) {
15306 var linkedTo = series.options.linkedTo;
15307 if (isString(linkedTo)) {
15308 if (linkedTo === ':previous') {
15309 linkedTo = chart.series[series.index - 1];
15310 } else {
15311 linkedTo = chart.get(linkedTo);
15312 }
15313 if (linkedTo && linkedTo.linkedParent !== series) { // #3341 avoid mutual linking
15314 linkedTo.linkedSeries.push(series);
15315 series.linkedParent = linkedTo;
15316 series.visible = pick(series.options.visible, linkedTo.options.visible, series.visible); // #3879
15317 }
15318 }
15319 });
15320 },
15321
15322 /**
15323 * Render series for the chart
15324 */
15325 renderSeries: function() {
15326 each(this.series, function(serie) {
15327 serie.translate();
15328 serie.render();
15329 });
15330 },
15331
15332 /**
15333 * Render labels for the chart
15334 */
15335 renderLabels: function() {
15336 var chart = this,
15337 labels = chart.options.labels;
15338 if (labels.items) {
15339 each(labels.items, function(label) {
15340 var style = extend(labels.style, label.style),
15341 x = pInt(style.left) + chart.plotLeft,
15342 y = pInt(style.top) + chart.plotTop + 12;
15343
15344 // delete to prevent rewriting in IE
15345 delete style.left;
15346 delete style.top;
15347
15348 chart.renderer.text(
15349 label.html,
15350 x,
15351 y
15352 )
15353 .attr({
15354 zIndex: 2
15355 })
15356 .css(style)
15357 .add();
15358
15359 });
15360 }
15361 },
15362
15363 /**
15364 * Render all graphics for the chart
15365 */
15366 render: function() {
15367 var chart = this,
15368 axes = chart.axes,
15369 renderer = chart.renderer,
15370 options = chart.options,
15371 tempWidth,
15372 tempHeight,
15373 redoHorizontal,
15374 redoVertical;
15375
15376 // Title
15377 chart.setTitle();
15378
15379
15380 // Legend
15381 chart.legend = new Legend(chart, options.legend);
15382
15383 // Get stacks
15384 if (chart.getStacks) {
15385 chart.getStacks();
15386 }
15387
15388 // Get chart margins
15389 chart.getMargins(true);
15390 chart.setChartSize();
15391
15392 // Record preliminary dimensions for later comparison
15393 tempWidth = chart.plotWidth;
15394 tempHeight = chart.plotHeight = chart.plotHeight - 21; // 21 is the most common correction for X axis labels
15395
15396 // Get margins by pre-rendering axes
15397 each(axes, function(axis) {
15398 axis.setScale();
15399 });
15400 chart.getAxisMargins();
15401
15402 // If the plot area size has changed significantly, calculate tick positions again
15403 redoHorizontal = tempWidth / chart.plotWidth > 1.1;
15404 redoVertical = tempHeight / chart.plotHeight > 1.05; // Height is more sensitive
15405
15406 if (redoHorizontal || redoVertical) {
15407
15408 each(axes, function(axis) {
15409 if ((axis.horiz && redoHorizontal) || (!axis.horiz && redoVertical)) {
15410 axis.setTickInterval(true); // update to reflect the new margins
15411 }
15412 });
15413 chart.getMargins(); // second pass to check for new labels
15414 }
15415
15416 // Draw the borders and backgrounds
15417 chart.drawChartBox();
15418
15419
15420 // Axes
15421 if (chart.hasCartesianSeries) {
15422 each(axes, function(axis) {
15423 if (axis.visible) {
15424 axis.render();
15425 }
15426 });
15427 }
15428
15429 // The series
15430 if (!chart.seriesGroup) {
15431 chart.seriesGroup = renderer.g('series-group')
15432 .attr({
15433 zIndex: 3
15434 })
15435 .add();
15436 }
15437 chart.renderSeries();
15438
15439 // Labels
15440 chart.renderLabels();
15441
15442 // Credits
15443 chart.addCredits();
15444
15445 // Handle responsiveness
15446 if (chart.setResponsive) {
15447 chart.setResponsive();
15448 }
15449
15450 // Set flag
15451 chart.hasRendered = true;
15452
15453 },
15454
15455 /**
15456 * Show chart credits based on config options
15457 */
15458 addCredits: function(credits) {
15459 var chart = this;
15460
15461 credits = merge(true, this.options.credits, credits);
15462 if (credits.enabled && !this.credits) {
15463 this.credits = this.renderer.text(
15464 credits.text + (this.mapCredits || ''),
15465 0,
15466 0
15467 )
15468 .addClass('highcharts-credits')
15469 .on('click', function() {
15470 if (credits.href) {
15471 win.location.href = credits.href;
15472 }
15473 })
15474 .attr({
15475 align: credits.position.align,
15476 zIndex: 8
15477 })
15478
15479 .css(credits.style)
15480
15481 .add()
15482 .align(credits.position);
15483
15484 // Dynamically update
15485 this.credits.update = function(options) {
15486 chart.credits = chart.credits.destroy();
15487 chart.addCredits(options);
15488 };
15489 }
15490 },
15491
15492 /**
15493 * Clean up memory usage
15494 */
15495 destroy: function() {
15496 var chart = this,
15497 axes = chart.axes,
15498 series = chart.series,
15499 container = chart.container,
15500 i,
15501 parentNode = container && container.parentNode;
15502
15503 // fire the chart.destoy event
15504 fireEvent(chart, 'destroy');
15505
15506 // Delete the chart from charts lookup array
15507 charts[chart.index] = undefined;
15508 H.chartCount--;
15509 chart.renderTo.removeAttribute('data-highcharts-chart');
15510
15511 // remove events
15512 removeEvent(chart);
15513
15514 // ==== Destroy collections:
15515 // Destroy axes
15516 i = axes.length;
15517 while (i--) {
15518 axes[i] = axes[i].destroy();
15519 }
15520
15521 // Destroy scroller & scroller series before destroying base series
15522 if (this.scroller && this.scroller.destroy) {
15523 this.scroller.destroy();
15524 }
15525
15526 // Destroy each series
15527 i = series.length;
15528 while (i--) {
15529 series[i] = series[i].destroy();
15530 }
15531
15532 // ==== Destroy chart properties:
15533 each(['title', 'subtitle', 'chartBackground', 'plotBackground', 'plotBGImage',
15534 'plotBorder', 'seriesGroup', 'clipRect', 'credits', 'pointer',
15535 'rangeSelector', 'legend', 'resetZoomButton', 'tooltip', 'renderer'
15536 ], function(name) {
15537 var prop = chart[name];
15538
15539 if (prop && prop.destroy) {
15540 chart[name] = prop.destroy();
15541 }
15542 });
15543
15544 // remove container and all SVG
15545 if (container) { // can break in IE when destroyed before finished loading
15546 container.innerHTML = '';
15547 removeEvent(container);
15548 if (parentNode) {
15549 discardElement(container);
15550 }
15551
15552 }
15553
15554 // clean it all up
15555 for (i in chart) {
15556 delete chart[i];
15557 }
15558
15559 },
15560
15561
15562 /**
15563 * VML namespaces can't be added until after complete. Listening
15564 * for Perini's doScroll hack is not enough.
15565 */
15566 isReadyToRender: function() {
15567 var chart = this;
15568
15569 // Note: win == win.top is required
15570 if ((!svg && (win == win.top && doc.readyState !== 'complete'))) { // eslint-disable-line eqeqeq
15571 doc.attachEvent('onreadystatechange', function() {
15572 doc.detachEvent('onreadystatechange', chart.firstRender);
15573 if (doc.readyState === 'complete') {
15574 chart.firstRender();
15575 }
15576 });
15577 return false;
15578 }
15579 return true;
15580 },
15581
15582 /**
15583 * Prepare for first rendering after all data are loaded
15584 */
15585 firstRender: function() {
15586 var chart = this,
15587 options = chart.options;
15588
15589 // Check whether the chart is ready to render
15590 if (!chart.isReadyToRender()) {
15591 return;
15592 }
15593
15594 // Create the container
15595 chart.getContainer();
15596
15597 // Run an early event after the container and renderer are established
15598 fireEvent(chart, 'init');
15599
15600
15601 chart.resetMargins();
15602 chart.setChartSize();
15603
15604 // Set the common chart properties (mainly invert) from the given series
15605 chart.propFromSeries();
15606
15607 // get axes
15608 chart.getAxes();
15609
15610 // Initialize the series
15611 each(options.series || [], function(serieOptions) {
15612 chart.initSeries(serieOptions);
15613 });
15614
15615 chart.linkSeries();
15616
15617 // Run an event after axes and series are initialized, but before render. At this stage,
15618 // the series data is indexed and cached in the xData and yData arrays, so we can access
15619 // those before rendering. Used in Highstock.
15620 fireEvent(chart, 'beforeRender');
15621
15622 // depends on inverted and on margins being set
15623 if (Pointer) {
15624 chart.pointer = new Pointer(chart, options);
15625 }
15626
15627 chart.render();
15628
15629 // add canvas
15630 chart.renderer.draw();
15631
15632 // Fire the load event if there are no external images
15633 if (!chart.renderer.imgCount && chart.onload) {
15634 chart.onload();
15635 }
15636
15637 // If the chart was rendered outside the top container, put it back in (#3679)
15638 chart.cloneRenderTo(true);
15639
15640 },
15641
15642 /**
15643 * On chart load
15644 */
15645 onload: function() {
15646
15647 // Run callbacks
15648 each([this.callback].concat(this.callbacks), function(fn) {
15649 if (fn && this.index !== undefined) { // Chart destroyed in its own callback (#3600)
15650 fn.apply(this, [this]);
15651 }
15652 }, this);
15653
15654 fireEvent(this, 'load');
15655
15656 // Set up auto resize
15657 if (this.options.chart.reflow !== false) {
15658 this.initReflow();
15659 }
15660
15661 // Don't run again
15662 this.onload = null;
15663 }
15664
15665 }; // end Chart
15666
15667 }(Highcharts));
15668 (function(H) {
15669 /**
15670 * (c) 2010-2016 Torstein Honsi
15671 *
15672 * License: www.highcharts.com/license
15673 */
15674 'use strict';
15675 var Point,
15676
15677 each = H.each,
15678 extend = H.extend,
15679 erase = H.erase,
15680 fireEvent = H.fireEvent,
15681 format = H.format,
15682 isArray = H.isArray,
15683 isNumber = H.isNumber,
15684 pick = H.pick,
15685 removeEvent = H.removeEvent;
15686
15687 /**
15688 * The Point object. The point objects are generated from the series.data
15689 * configuration objects or raw numbers. They can be accessed from the
15690 * Series.points array.
15691 * @constructor Point
15692 */
15693 Point = H.Point = function() {};
15694 Point.prototype = {
15695
15696 /**
15697 * Initialize the point. Called internally based on the series.data option.
15698 * @function #init
15699 * @memberOf Point
15700 * @param {Object} series The series object containing this point.
15701 * @param {Object} options The data in either number, array or object
15702 * format.
15703 * @param {Number} x Optionally, the X value of the.
15704 * @returns {Object} The Point instance.
15705 */
15706 init: function(series, options, x) {
15707
15708 var point = this,
15709 colors,
15710 colorCount = series.chart.options.chart.colorCount,
15711 colorIndex;
15712
15713 point.series = series;
15714
15715 point.color = series.color; // #3445
15716
15717 point.applyOptions(options, x);
15718
15719 if (series.options.colorByPoint) {
15720
15721 colors = series.options.colors || series.chart.options.colors;
15722 point.color = point.color || colors[series.colorCounter];
15723 colorCount = colors.length;
15724
15725 colorIndex = series.colorCounter;
15726 series.colorCounter++;
15727 // loop back to zero
15728 if (series.colorCounter === colorCount) {
15729 series.colorCounter = 0;
15730 }
15731 } else {
15732 colorIndex = series.colorIndex;
15733 }
15734 point.colorIndex = pick(point.colorIndex, colorIndex);
15735
15736 series.chart.pointCount++;
15737 return point;
15738 },
15739 /**
15740 * Apply the options containing the x and y data and possible some extra
15741 * properties. Called on point init or from point.update.
15742 *
15743 * @function #applyOptions
15744 * @memberOf Point
15745 * @param {Object} options The point options as defined in series.data.
15746 * @param {Number} x Optionally, the X value.
15747 * @returns {Object} The Point instance.
15748 */
15749 applyOptions: function(options, x) {
15750 var point = this,
15751 series = point.series,
15752 pointValKey = series.options.pointValKey || series.pointValKey;
15753
15754 options = Point.prototype.optionsToObject.call(this, options);
15755
15756 // copy options directly to point
15757 extend(point, options);
15758 point.options = point.options ? extend(point.options, options) : options;
15759
15760 // Since options are copied into the Point instance, some accidental options must be shielded (#5681)
15761 if (options.group) {
15762 delete point.group;
15763 }
15764
15765 // For higher dimension series types. For instance, for ranges, point.y is mapped to point.low.
15766 if (pointValKey) {
15767 point.y = point[pointValKey];
15768 }
15769 point.isNull = pick(
15770 point.isValid && !point.isValid(),
15771 point.x === null || !isNumber(point.y, true)
15772 ); // #3571, check for NaN
15773
15774 // The point is initially selected by options (#5777)
15775 if (point.selected) {
15776 point.state = 'select';
15777 }
15778
15779 // If no x is set by now, get auto incremented value. All points must have an
15780 // x value, however the y value can be null to create a gap in the series
15781 if ('name' in point && x === undefined && series.xAxis && series.xAxis.hasNames) {
15782 point.x = series.xAxis.nameToX(point);
15783 }
15784 if (point.x === undefined && series) {
15785 if (x === undefined) {
15786 point.x = series.autoIncrement(point);
15787 } else {
15788 point.x = x;
15789 }
15790 }
15791
15792 return point;
15793 },
15794
15795 /**
15796 * Transform number or array configs into objects
15797 */
15798 optionsToObject: function(options) {
15799 var ret = {},
15800 series = this.series,
15801 keys = series.options.keys,
15802 pointArrayMap = keys || series.pointArrayMap || ['y'],
15803 valueCount = pointArrayMap.length,
15804 firstItemType,
15805 i = 0,
15806 j = 0;
15807
15808 if (isNumber(options) || options === null) {
15809 ret[pointArrayMap[0]] = options;
15810
15811 } else if (isArray(options)) {
15812 // with leading x value
15813 if (!keys && options.length > valueCount) {
15814 firstItemType = typeof options[0];
15815 if (firstItemType === 'string') {
15816 ret.name = options[0];
15817 } else if (firstItemType === 'number') {
15818 ret.x = options[0];
15819 }
15820 i++;
15821 }
15822 while (j < valueCount) {
15823 if (!keys || options[i] !== undefined) { // Skip undefined positions for keys
15824 ret[pointArrayMap[j]] = options[i];
15825 }
15826 i++;
15827 j++;
15828 }
15829 } else if (typeof options === 'object') {
15830 ret = options;
15831
15832 // This is the fastest way to detect if there are individual point dataLabels that need
15833 // to be considered in drawDataLabels. These can only occur in object configs.
15834 if (options.dataLabels) {
15835 series._hasPointLabels = true;
15836 }
15837
15838 // Same approach as above for markers
15839 if (options.marker) {
15840 series._hasPointMarkers = true;
15841 }
15842 }
15843 return ret;
15844 },
15845
15846 /**
15847 * Get the CSS class names for individual points
15848 * @returns {String} The class name
15849 */
15850 getClassName: function() {
15851 return 'highcharts-point' +
15852 (this.selected ? ' highcharts-point-select' : '') +
15853 (this.negative ? ' highcharts-negative' : '') +
15854 (this.isNull ? ' highcharts-null-point' : '') +
15855 (this.colorIndex !== undefined ? ' highcharts-color-' + this.colorIndex : '') +
15856 (this.options.className ? ' ' + this.options.className : '');
15857 },
15858
15859 /**
15860 * Return the zone that the point belongs to
15861 */
15862 getZone: function() {
15863 var series = this.series,
15864 zones = series.zones,
15865 zoneAxis = series.zoneAxis || 'y',
15866 i = 0,
15867 zone;
15868
15869 zone = zones[i];
15870 while (this[zoneAxis] >= zone.value) {
15871 zone = zones[++i];
15872 }
15873
15874 if (zone && zone.color && !this.options.color) {
15875 this.color = zone.color;
15876 }
15877
15878 return zone;
15879 },
15880
15881 /**
15882 * Destroy a point to clear memory. Its reference still stays in series.data.
15883 */
15884 destroy: function() {
15885 var point = this,
15886 series = point.series,
15887 chart = series.chart,
15888 hoverPoints = chart.hoverPoints,
15889 prop;
15890
15891 chart.pointCount--;
15892
15893 if (hoverPoints) {
15894 point.setState();
15895 erase(hoverPoints, point);
15896 if (!hoverPoints.length) {
15897 chart.hoverPoints = null;
15898 }
15899
15900 }
15901 if (point === chart.hoverPoint) {
15902 point.onMouseOut();
15903 }
15904
15905 // remove all events
15906 if (point.graphic || point.dataLabel) { // removeEvent and destroyElements are performance expensive
15907 removeEvent(point);
15908 point.destroyElements();
15909 }
15910
15911 if (point.legendItem) { // pies have legend items
15912 chart.legend.destroyItem(point);
15913 }
15914
15915 for (prop in point) {
15916 point[prop] = null;
15917 }
15918
15919
15920 },
15921
15922 /**
15923 * Destroy SVG elements associated with the point
15924 */
15925 destroyElements: function() {
15926 var point = this,
15927 props = ['graphic', 'dataLabel', 'dataLabelUpper', 'connector', 'shadowGroup'],
15928 prop,
15929 i = 6;
15930 while (i--) {
15931 prop = props[i];
15932 if (point[prop]) {
15933 point[prop] = point[prop].destroy();
15934 }
15935 }
15936 },
15937
15938 /**
15939 * Return the configuration hash needed for the data label and tooltip formatters
15940 */
15941 getLabelConfig: function() {
15942 return {
15943 x: this.category,
15944 y: this.y,
15945 color: this.color,
15946 key: this.name || this.category,
15947 series: this.series,
15948 point: this,
15949 percentage: this.percentage,
15950 total: this.total || this.stackTotal
15951 };
15952 },
15953
15954 /**
15955 * Extendable method for formatting each point's tooltip line
15956 *
15957 * @return {String} A string to be concatenated in to the common tooltip text
15958 */
15959 tooltipFormatter: function(pointFormat) {
15960
15961 // Insert options for valueDecimals, valuePrefix, and valueSuffix
15962 var series = this.series,
15963 seriesTooltipOptions = series.tooltipOptions,
15964 valueDecimals = pick(seriesTooltipOptions.valueDecimals, ''),
15965 valuePrefix = seriesTooltipOptions.valuePrefix || '',
15966 valueSuffix = seriesTooltipOptions.valueSuffix || '';
15967
15968 // Loop over the point array map and replace unformatted values with sprintf formatting markup
15969 each(series.pointArrayMap || ['y'], function(key) {
15970 key = '{point.' + key; // without the closing bracket
15971 if (valuePrefix || valueSuffix) {
15972 pointFormat = pointFormat.replace(key + '}', valuePrefix + key + '}' + valueSuffix);
15973 }
15974 pointFormat = pointFormat.replace(key + '}', key + ':,.' + valueDecimals + 'f}');
15975 });
15976
15977 return format(pointFormat, {
15978 point: this,
15979 series: this.series
15980 });
15981 },
15982
15983 /**
15984 * Fire an event on the Point object.
15985 * @param {String} eventType
15986 * @param {Object} eventArgs Additional event arguments
15987 * @param {Function} defaultFunction Default event handler
15988 */
15989 firePointEvent: function(eventType, eventArgs, defaultFunction) {
15990 var point = this,
15991 series = this.series,
15992 seriesOptions = series.options;
15993
15994 // load event handlers on demand to save time on mouseover/out
15995 if (seriesOptions.point.events[eventType] || (point.options && point.options.events && point.options.events[eventType])) {
15996 this.importEvents();
15997 }
15998
15999 // add default handler if in selection mode
16000 if (eventType === 'click' && seriesOptions.allowPointSelect) {
16001 defaultFunction = function(event) {
16002 // Control key is for Windows, meta (= Cmd key) for Mac, Shift for Opera
16003 if (point.select) { // Could be destroyed by prior event handlers (#2911)
16004 point.select(null, event.ctrlKey || event.metaKey || event.shiftKey);
16005 }
16006 };
16007 }
16008
16009 fireEvent(this, eventType, eventArgs, defaultFunction);
16010 },
16011 visible: true
16012 };
16013
16014 }(Highcharts));
16015 (function(H) {
16016 /**
16017 * (c) 2010-2016 Torstein Honsi
16018 *
16019 * License: www.highcharts.com/license
16020 */
16021 'use strict';
16022 var addEvent = H.addEvent,
16023 animObject = H.animObject,
16024 arrayMax = H.arrayMax,
16025 arrayMin = H.arrayMin,
16026 correctFloat = H.correctFloat,
16027 Date = H.Date,
16028 defaultOptions = H.defaultOptions,
16029 defaultPlotOptions = H.defaultPlotOptions,
16030 defined = H.defined,
16031 each = H.each,
16032 erase = H.erase,
16033 error = H.error,
16034 extend = H.extend,
16035 fireEvent = H.fireEvent,
16036 grep = H.grep,
16037 isArray = H.isArray,
16038 isNumber = H.isNumber,
16039 isString = H.isString,
16040 LegendSymbolMixin = H.LegendSymbolMixin, // @todo add as a requirement
16041 merge = H.merge,
16042 pick = H.pick,
16043 Point = H.Point, // @todo add as a requirement
16044 removeEvent = H.removeEvent,
16045 splat = H.splat,
16046 stableSort = H.stableSort,
16047 SVGElement = H.SVGElement,
16048 syncTimeout = H.syncTimeout,
16049 win = H.win;
16050
16051 /**
16052 * The base function which all other series types inherit from. The data in the series is stored
16053 * in various arrays.
16054 *
16055 * - First, series.options.data contains all the original config options for
16056 * each point whether added by options or methods like series.addPoint.
16057 * - Next, series.data contains those values converted to points, but in case the series data length
16058 * exceeds the cropThreshold, or if the data is grouped, series.data doesn't contain all the points. It
16059 * only contains the points that have been created on demand.
16060 * - Then there's series.points that contains all currently visible point objects. In case of cropping,
16061 * the cropped-away points are not part of this array. The series.points array starts at series.cropStart
16062 * compared to series.data and series.options.data. If however the series data is grouped, these can't
16063 * be correlated one to one.
16064 * - series.xData and series.processedXData contain clean x values, equivalent to series.data and series.points.
16065 * - series.yData and series.processedYData contain clean y values, equivalent to series.data and series.points.
16066 *
16067 * @constructor Series
16068 * @param {Object} chart - The chart instance.
16069 * @param {Object} options - The series options.
16070 */
16071 H.Series = H.seriesType('line', null, { // base series options
16072
16073 //cursor: 'default',
16074 //dashStyle: null,
16075 //linecap: 'round',
16076 lineWidth: 2,
16077 //shadow: false,
16078
16079 allowPointSelect: false,
16080 showCheckbox: false,
16081 animation: {
16082 duration: 1000
16083 },
16084 //clip: true,
16085 //connectNulls: false,
16086 //enableMouseTracking: true,
16087 events: {},
16088 //legendIndex: 0,
16089 // stacking: null,
16090 marker: {
16091
16092 lineWidth: 0,
16093 lineColor: '#ffffff',
16094 //fillColor: null,
16095
16096 //enabled: true,
16097 //symbol: null,
16098 radius: 4,
16099 states: { // states for a single point
16100 hover: {
16101 animation: {
16102 duration: 50
16103 },
16104 enabled: true,
16105 radiusPlus: 2,
16106
16107 lineWidthPlus: 1
16108
16109 },
16110
16111 select: {
16112 fillColor: '#cccccc',
16113 lineColor: '#000000',
16114 lineWidth: 2
16115 }
16116
16117 }
16118 },
16119 point: {
16120 events: {}
16121 },
16122 dataLabels: {
16123 align: 'center',
16124 // defer: true,
16125 // enabled: false,
16126 formatter: function() {
16127 return this.y === null ? '' : H.numberFormat(this.y, -1);
16128 },
16129
16130 style: {
16131 fontSize: '11px',
16132 fontWeight: 'bold',
16133 color: 'contrast',
16134 textOutline: '1px contrast'
16135 },
16136 // backgroundColor: undefined,
16137 // borderColor: undefined,
16138 // borderWidth: undefined,
16139 // shadow: false
16140
16141 verticalAlign: 'bottom', // above singular point
16142 x: 0,
16143 y: 0,
16144 // borderRadius: undefined,
16145 padding: 5
16146 },
16147 cropThreshold: 300, // draw points outside the plot area when the number of points is less than this
16148 pointRange: 0,
16149 //pointStart: 0,
16150 //pointInterval: 1,
16151 //showInLegend: null, // auto: true for standalone series, false for linked series
16152 softThreshold: true,
16153 states: { // states for the entire series
16154 hover: {
16155 //enabled: false,
16156 lineWidthPlus: 1,
16157 marker: {
16158 // lineWidth: base + 1,
16159 // radius: base + 1
16160 },
16161 halo: {
16162 size: 10,
16163
16164 opacity: 0.25
16165
16166 }
16167 },
16168 select: {
16169 marker: {}
16170 }
16171 },
16172 stickyTracking: true,
16173 //tooltip: {
16174 //pointFormat: '<span style="color:{point.color}">\u25CF</span> {series.name}: <b>{point.y}</b>'
16175 //valueDecimals: null,
16176 //xDateFormat: '%A, %b %e, %Y',
16177 //valuePrefix: '',
16178 //ySuffix: ''
16179 //}
16180 turboThreshold: 1000
16181 // zIndex: null
16182
16183
16184 }, /** @lends Series.prototype */ {
16185 isCartesian: true,
16186 pointClass: Point,
16187 sorted: true, // requires the data to be sorted
16188 requireSorting: true,
16189 directTouch: false,
16190 axisTypes: ['xAxis', 'yAxis'],
16191 colorCounter: 0,
16192 parallelArrays: ['x', 'y'], // each point's x and y values are stored in this.xData and this.yData
16193 coll: 'series',
16194 init: function(chart, options) {
16195 var series = this,
16196 eventType,
16197 events,
16198 chartSeries = chart.series,
16199 lastSeries,
16200 sortByIndex = function(a, b) {
16201 return pick(a.options.index, a._i) - pick(b.options.index, b._i);
16202 };
16203
16204 series.chart = chart;
16205 series.options = options = series.setOptions(options); // merge with plotOptions
16206 series.linkedSeries = [];
16207
16208 // bind the axes
16209 series.bindAxes();
16210
16211 // set some variables
16212 extend(series, {
16213 name: options.name,
16214 state: '',
16215 visible: options.visible !== false, // true by default
16216 selected: options.selected === true // false by default
16217 });
16218
16219 // register event listeners
16220 events = options.events;
16221 for (eventType in events) {
16222 addEvent(series, eventType, events[eventType]);
16223 }
16224 if (
16225 (events && events.click) ||
16226 (options.point && options.point.events && options.point.events.click) ||
16227 options.allowPointSelect
16228 ) {
16229 chart.runTrackerClick = true;
16230 }
16231
16232 series.getColor();
16233 series.getSymbol();
16234
16235 // Set the data
16236 each(series.parallelArrays, function(key) {
16237 series[key + 'Data'] = [];
16238 });
16239 series.setData(options.data, false);
16240
16241 // Mark cartesian
16242 if (series.isCartesian) {
16243 chart.hasCartesianSeries = true;
16244 }
16245
16246 // Get the index and register the series in the chart. The index is one
16247 // more than the current latest series index (#5960).
16248 if (chartSeries.length) {
16249 lastSeries = chartSeries[chartSeries.length - 1];
16250 }
16251 series._i = pick(lastSeries && lastSeries._i, -1) + 1;
16252 chartSeries.push(series);
16253
16254 // Sort series according to index option (#248, #1123, #2456)
16255 stableSort(chartSeries, sortByIndex);
16256 if (this.yAxis) {
16257 stableSort(this.yAxis.series, sortByIndex);
16258 }
16259
16260 each(chartSeries, function(series, i) {
16261 series.index = i;
16262 series.name = series.name || 'Series ' + (i + 1);
16263 });
16264
16265 },
16266
16267 /**
16268 * Set the xAxis and yAxis properties of cartesian series, and register the
16269 * series in the `axis.series` array.
16270 *
16271 * @function #bindAxes
16272 * @memberOf Series
16273 * @returns {void}
16274 */
16275 bindAxes: function() {
16276 var series = this,
16277 seriesOptions = series.options,
16278 chart = series.chart,
16279 axisOptions;
16280
16281 each(series.axisTypes || [], function(AXIS) { // repeat for xAxis and yAxis
16282
16283 each(chart[AXIS], function(axis) { // loop through the chart's axis objects
16284 axisOptions = axis.options;
16285
16286 // apply if the series xAxis or yAxis option mathches the number of the
16287 // axis, or if undefined, use the first axis
16288 if ((seriesOptions[AXIS] === axisOptions.index) ||
16289 (seriesOptions[AXIS] !== undefined && seriesOptions[AXIS] === axisOptions.id) ||
16290 (seriesOptions[AXIS] === undefined && axisOptions.index === 0)) {
16291
16292 // register this series in the axis.series lookup
16293 axis.series.push(series);
16294
16295 // set this series.xAxis or series.yAxis reference
16296 series[AXIS] = axis;
16297
16298 // mark dirty for redraw
16299 axis.isDirty = true;
16300 }
16301 });
16302
16303 // The series needs an X and an Y axis
16304 if (!series[AXIS] && series.optionalAxis !== AXIS) {
16305 error(18, true);
16306 }
16307
16308 });
16309 },
16310
16311 /**
16312 * For simple series types like line and column, the data values are held in arrays like
16313 * xData and yData for quick lookup to find extremes and more. For multidimensional series
16314 * like bubble and map, this can be extended with arrays like zData and valueData by
16315 * adding to the series.parallelArrays array.
16316 */
16317 updateParallelArrays: function(point, i) {
16318 var series = point.series,
16319 args = arguments,
16320 fn = isNumber(i) ?
16321 // Insert the value in the given position
16322 function(key) {
16323 var val = key === 'y' && series.toYData ? series.toYData(point) : point[key];
16324 series[key + 'Data'][i] = val;
16325 } :
16326 // Apply the method specified in i with the following arguments as arguments
16327 function(key) {
16328 Array.prototype[i].apply(series[key + 'Data'], Array.prototype.slice.call(args, 2));
16329 };
16330
16331 each(series.parallelArrays, fn);
16332 },
16333
16334 /**
16335 * Return an auto incremented x value based on the pointStart and pointInterval options.
16336 * This is only used if an x value is not given for the point that calls autoIncrement.
16337 */
16338 autoIncrement: function() {
16339
16340 var options = this.options,
16341 xIncrement = this.xIncrement,
16342 date,
16343 pointInterval,
16344 pointIntervalUnit = options.pointIntervalUnit;
16345
16346 xIncrement = pick(xIncrement, options.pointStart, 0);
16347
16348 this.pointInterval = pointInterval = pick(this.pointInterval, options.pointInterval, 1);
16349
16350 // Added code for pointInterval strings
16351 if (pointIntervalUnit) {
16352 date = new Date(xIncrement);
16353
16354 if (pointIntervalUnit === 'day') {
16355 date = +date[Date.hcSetDate](date[Date.hcGetDate]() + pointInterval);
16356 } else if (pointIntervalUnit === 'month') {
16357 date = +date[Date.hcSetMonth](date[Date.hcGetMonth]() + pointInterval);
16358 } else if (pointIntervalUnit === 'year') {
16359 date = +date[Date.hcSetFullYear](date[Date.hcGetFullYear]() + pointInterval);
16360 }
16361 pointInterval = date - xIncrement;
16362
16363 }
16364
16365 this.xIncrement = xIncrement + pointInterval;
16366 return xIncrement;
16367 },
16368
16369 /**
16370 * Set the series options by merging from the options tree
16371 * @param {Object} itemOptions
16372 */
16373 setOptions: function(itemOptions) {
16374 var chart = this.chart,
16375 chartOptions = chart.options,
16376 plotOptions = chartOptions.plotOptions,
16377 userOptions = chart.userOptions || {},
16378 userPlotOptions = userOptions.plotOptions || {},
16379 typeOptions = plotOptions[this.type],
16380 options,
16381 zones;
16382
16383 this.userOptions = itemOptions;
16384
16385 // General series options take precedence over type options because otherwise, default
16386 // type options like column.animation would be overwritten by the general option.
16387 // But issues have been raised here (#3881), and the solution may be to distinguish
16388 // between default option and userOptions like in the tooltip below.
16389 options = merge(
16390 typeOptions,
16391 plotOptions.series,
16392 itemOptions
16393 );
16394
16395 // The tooltip options are merged between global and series specific options
16396 this.tooltipOptions = merge(
16397 defaultOptions.tooltip,
16398 defaultOptions.plotOptions[this.type].tooltip,
16399 userOptions.tooltip,
16400 userPlotOptions.series && userPlotOptions.series.tooltip,
16401 userPlotOptions[this.type] && userPlotOptions[this.type].tooltip,
16402 itemOptions.tooltip
16403 );
16404
16405 // Delete marker object if not allowed (#1125)
16406 if (typeOptions.marker === null) {
16407 delete options.marker;
16408 }
16409
16410 // Handle color zones
16411 this.zoneAxis = options.zoneAxis;
16412 zones = this.zones = (options.zones || []).slice();
16413 if ((options.negativeColor || options.negativeFillColor) && !options.zones) {
16414 zones.push({
16415 value: options[this.zoneAxis + 'Threshold'] || options.threshold || 0,
16416 className: 'highcharts-negative',
16417
16418 color: options.negativeColor,
16419 fillColor: options.negativeFillColor
16420
16421 });
16422 }
16423 if (zones.length) { // Push one extra zone for the rest
16424 if (defined(zones[zones.length - 1].value)) {
16425 zones.push({
16426
16427 color: this.color,
16428 fillColor: this.fillColor
16429
16430 });
16431 }
16432 }
16433 return options;
16434 },
16435
16436 getCyclic: function(prop, value, defaults) {
16437 var i,
16438 userOptions = this.userOptions,
16439 indexName = prop + 'Index',
16440 counterName = prop + 'Counter',
16441 len = defaults ? defaults.length : pick(this.chart.options.chart[prop + 'Count'], this.chart[prop + 'Count']),
16442 setting;
16443
16444 if (!value) {
16445 // Pick up either the colorIndex option, or the _colorIndex after Series.update()
16446 setting = pick(userOptions[indexName], userOptions['_' + indexName]);
16447 if (defined(setting)) { // after Series.update()
16448 i = setting;
16449 } else {
16450 userOptions['_' + indexName] = i = this.chart[counterName] % len;
16451 this.chart[counterName] += 1;
16452 }
16453 if (defaults) {
16454 value = defaults[i];
16455 }
16456 }
16457 // Set the colorIndex
16458 if (i !== undefined) {
16459 this[indexName] = i;
16460 }
16461 this[prop] = value;
16462 },
16463
16464 /**
16465 * Get the series' color
16466 */
16467
16468 getColor: function() {
16469 if (this.options.colorByPoint) {
16470 this.options.color = null; // #4359, selected slice got series.color even when colorByPoint was set.
16471 } else {
16472 this.getCyclic('color', this.options.color || defaultPlotOptions[this.type].color, this.chart.options.colors);
16473 }
16474 },
16475
16476 /**
16477 * Get the series' symbol
16478 */
16479 getSymbol: function() {
16480 var seriesMarkerOption = this.options.marker;
16481
16482 this.getCyclic('symbol', seriesMarkerOption.symbol, this.chart.options.symbols);
16483 },
16484
16485 drawLegendSymbol: LegendSymbolMixin.drawLineMarker,
16486
16487 /**
16488 * Replace the series data with a new set of data
16489 * @param {Object} data
16490 * @param {Object} redraw
16491 */
16492 setData: function(data, redraw, animation, updatePoints) {
16493 var series = this,
16494 oldData = series.points,
16495 oldDataLength = (oldData && oldData.length) || 0,
16496 dataLength,
16497 options = series.options,
16498 chart = series.chart,
16499 firstPoint = null,
16500 xAxis = series.xAxis,
16501 i,
16502 turboThreshold = options.turboThreshold,
16503 pt,
16504 xData = this.xData,
16505 yData = this.yData,
16506 pointArrayMap = series.pointArrayMap,
16507 valueCount = pointArrayMap && pointArrayMap.length;
16508
16509 data = data || [];
16510 dataLength = data.length;
16511 redraw = pick(redraw, true);
16512
16513 // If the point count is the same as is was, just run Point.update which is
16514 // cheaper, allows animation, and keeps references to points.
16515 if (updatePoints !== false && dataLength && oldDataLength === dataLength && !series.cropped && !series.hasGroupedData && series.visible) {
16516 each(data, function(point, i) {
16517 // .update doesn't exist on a linked, hidden series (#3709)
16518 if (oldData[i].update && point !== options.data[i]) {
16519 oldData[i].update(point, false, null, false);
16520 }
16521 });
16522
16523 } else {
16524
16525 // Reset properties
16526 series.xIncrement = null;
16527
16528 series.colorCounter = 0; // for series with colorByPoint (#1547)
16529
16530 // Update parallel arrays
16531 each(this.parallelArrays, function(key) {
16532 series[key + 'Data'].length = 0;
16533 });
16534
16535 // In turbo mode, only one- or twodimensional arrays of numbers are allowed. The
16536 // first value is tested, and we assume that all the rest are defined the same
16537 // way. Although the 'for' loops are similar, they are repeated inside each
16538 // if-else conditional for max performance.
16539 if (turboThreshold && dataLength > turboThreshold) {
16540
16541 // find the first non-null point
16542 i = 0;
16543 while (firstPoint === null && i < dataLength) {
16544 firstPoint = data[i];
16545 i++;
16546 }
16547
16548
16549 if (isNumber(firstPoint)) { // assume all points are numbers
16550 for (i = 0; i < dataLength; i++) {
16551 xData[i] = this.autoIncrement();
16552 yData[i] = data[i];
16553 }
16554 } else if (isArray(firstPoint)) { // assume all points are arrays
16555 if (valueCount) { // [x, low, high] or [x, o, h, l, c]
16556 for (i = 0; i < dataLength; i++) {
16557 pt = data[i];
16558 xData[i] = pt[0];
16559 yData[i] = pt.slice(1, valueCount + 1);
16560 }
16561 } else { // [x, y]
16562 for (i = 0; i < dataLength; i++) {
16563 pt = data[i];
16564 xData[i] = pt[0];
16565 yData[i] = pt[1];
16566 }
16567 }
16568 } else {
16569 error(12); // Highcharts expects configs to be numbers or arrays in turbo mode
16570 }
16571 } else {
16572 for (i = 0; i < dataLength; i++) {
16573 if (data[i] !== undefined) { // stray commas in oldIE
16574 pt = {
16575 series: series
16576 };
16577 series.pointClass.prototype.applyOptions.apply(pt, [data[i]]);
16578 series.updateParallelArrays(pt, i);
16579 }
16580 }
16581 }
16582
16583 // Forgetting to cast strings to numbers is a common caveat when handling CSV or JSON
16584 if (isString(yData[0])) {
16585 error(14, true);
16586 }
16587
16588 series.data = [];
16589 series.options.data = series.userOptions.data = data;
16590
16591 // destroy old points
16592 i = oldDataLength;
16593 while (i--) {
16594 if (oldData[i] && oldData[i].destroy) {
16595 oldData[i].destroy();
16596 }
16597 }
16598
16599 // reset minRange (#878)
16600 if (xAxis) {
16601 xAxis.minRange = xAxis.userMinRange;
16602 }
16603
16604 // redraw
16605 series.isDirty = chart.isDirtyBox = true;
16606 series.isDirtyData = !!oldData;
16607 animation = false;
16608 }
16609
16610 // Typically for pie series, points need to be processed and generated
16611 // prior to rendering the legend
16612 if (options.legendType === 'point') {
16613 this.processData();
16614 this.generatePoints();
16615 }
16616
16617 if (redraw) {
16618 chart.redraw(animation);
16619 }
16620 },
16621
16622 /**
16623 * Process the data by cropping away unused data points if the series is longer
16624 * than the crop threshold. This saves computing time for lage series.
16625 */
16626 processData: function(force) {
16627 var series = this,
16628 processedXData = series.xData, // copied during slice operation below
16629 processedYData = series.yData,
16630 dataLength = processedXData.length,
16631 croppedData,
16632 cropStart = 0,
16633 cropped,
16634 distance,
16635 closestPointRange,
16636 xAxis = series.xAxis,
16637 i, // loop variable
16638 options = series.options,
16639 cropThreshold = options.cropThreshold,
16640 getExtremesFromAll = series.getExtremesFromAll || options.getExtremesFromAll, // #4599
16641 isCartesian = series.isCartesian,
16642 xExtremes,
16643 val2lin = xAxis && xAxis.val2lin,
16644 isLog = xAxis && xAxis.isLog,
16645 min,
16646 max;
16647
16648 // If the series data or axes haven't changed, don't go through this. Return false to pass
16649 // the message on to override methods like in data grouping.
16650 if (isCartesian && !series.isDirty && !xAxis.isDirty && !series.yAxis.isDirty && !force) {
16651 return false;
16652 }
16653
16654 if (xAxis) {
16655 xExtremes = xAxis.getExtremes(); // corrected for log axis (#3053)
16656 min = xExtremes.min;
16657 max = xExtremes.max;
16658 }
16659
16660 // optionally filter out points outside the plot area
16661 if (isCartesian && series.sorted && !getExtremesFromAll && (!cropThreshold || dataLength > cropThreshold || series.forceCrop)) {
16662
16663 // it's outside current extremes
16664 if (processedXData[dataLength - 1] < min || processedXData[0] > max) {
16665 processedXData = [];
16666 processedYData = [];
16667
16668 // only crop if it's actually spilling out
16669 } else if (processedXData[0] < min || processedXData[dataLength - 1] > max) {
16670 croppedData = this.cropData(series.xData, series.yData, min, max);
16671 processedXData = croppedData.xData;
16672 processedYData = croppedData.yData;
16673 cropStart = croppedData.start;
16674 cropped = true;
16675 }
16676 }
16677
16678
16679 // Find the closest distance between processed points
16680 i = processedXData.length || 1;
16681 while (--i) {
16682 distance = isLog ?
16683 val2lin(processedXData[i]) - val2lin(processedXData[i - 1]) :
16684 processedXData[i] - processedXData[i - 1];
16685
16686 if (distance > 0 && (closestPointRange === undefined || distance < closestPointRange)) {
16687 closestPointRange = distance;
16688
16689 // Unsorted data is not supported by the line tooltip, as well as data grouping and
16690 // navigation in Stock charts (#725) and width calculation of columns (#1900)
16691 } else if (distance < 0 && series.requireSorting) {
16692 error(15);
16693 }
16694 }
16695
16696 // Record the properties
16697 series.cropped = cropped; // undefined or true
16698 series.cropStart = cropStart;
16699 series.processedXData = processedXData;
16700 series.processedYData = processedYData;
16701
16702 series.closestPointRange = closestPointRange;
16703
16704 },
16705
16706 /**
16707 * Iterate over xData and crop values between min and max. Returns object containing crop start/end
16708 * cropped xData with corresponding part of yData, dataMin and dataMax within the cropped range
16709 */
16710 cropData: function(xData, yData, min, max) {
16711 var dataLength = xData.length,
16712 cropStart = 0,
16713 cropEnd = dataLength,
16714 cropShoulder = pick(this.cropShoulder, 1), // line-type series need one point outside
16715 i,
16716 j;
16717
16718 // iterate up to find slice start
16719 for (i = 0; i < dataLength; i++) {
16720 if (xData[i] >= min) {
16721 cropStart = Math.max(0, i - cropShoulder);
16722 break;
16723 }
16724 }
16725
16726 // proceed to find slice end
16727 for (j = i; j < dataLength; j++) {
16728 if (xData[j] > max) {
16729 cropEnd = j + cropShoulder;
16730 break;
16731 }
16732 }
16733
16734 return {
16735 xData: xData.slice(cropStart, cropEnd),
16736 yData: yData.slice(cropStart, cropEnd),
16737 start: cropStart,
16738 end: cropEnd
16739 };
16740 },
16741
16742
16743 /**
16744 * Generate the data point after the data has been processed by cropping away
16745 * unused points and optionally grouped in Highcharts Stock.
16746 */
16747 generatePoints: function() {
16748 var series = this,
16749 options = series.options,
16750 dataOptions = options.data,
16751 data = series.data,
16752 dataLength,
16753 processedXData = series.processedXData,
16754 processedYData = series.processedYData,
16755 PointClass = series.pointClass,
16756 processedDataLength = processedXData.length,
16757 cropStart = series.cropStart || 0,
16758 cursor,
16759 hasGroupedData = series.hasGroupedData,
16760 point,
16761 points = [],
16762 i;
16763
16764 if (!data && !hasGroupedData) {
16765 var arr = [];
16766 arr.length = dataOptions.length;
16767 data = series.data = arr;
16768 }
16769
16770 for (i = 0; i < processedDataLength; i++) {
16771 cursor = cropStart + i;
16772 if (!hasGroupedData) {
16773 point = data[cursor];
16774 if (!point && dataOptions[cursor] !== undefined) { // #970
16775 data[cursor] = point = (new PointClass()).init(series, dataOptions[cursor], processedXData[i]);
16776 }
16777 } else {
16778 // splat the y data in case of ohlc data array
16779 point = (new PointClass()).init(series, [processedXData[i]].concat(splat(processedYData[i])));
16780 point.dataGroup = series.groupMap[i];
16781 }
16782 point.index = cursor; // For faster access in Point.update
16783 points[i] = point;
16784 }
16785
16786 // Hide cropped-away points - this only runs when the number of points is above cropThreshold, or when
16787 // swithching view from non-grouped data to grouped data (#637)
16788 if (data && (processedDataLength !== (dataLength = data.length) || hasGroupedData)) {
16789 for (i = 0; i < dataLength; i++) {
16790 if (i === cropStart && !hasGroupedData) { // when has grouped data, clear all points
16791 i += processedDataLength;
16792 }
16793 if (data[i]) {
16794 data[i].destroyElements();
16795 data[i].plotX = undefined; // #1003
16796 }
16797 }
16798 }
16799
16800 series.data = data;
16801 series.points = points;
16802 },
16803
16804 /**
16805 * Calculate Y extremes for visible data
16806 */
16807 getExtremes: function(yData) {
16808 var xAxis = this.xAxis,
16809 yAxis = this.yAxis,
16810 xData = this.processedXData,
16811 yDataLength,
16812 activeYData = [],
16813 activeCounter = 0,
16814 xExtremes = xAxis.getExtremes(), // #2117, need to compensate for log X axis
16815 xMin = xExtremes.min,
16816 xMax = xExtremes.max,
16817 validValue,
16818 withinRange,
16819 x,
16820 y,
16821 i,
16822 j;
16823
16824 yData = yData || this.stackedYData || this.processedYData || [];
16825 yDataLength = yData.length;
16826
16827 for (i = 0; i < yDataLength; i++) {
16828
16829 x = xData[i];
16830 y = yData[i];
16831
16832 // For points within the visible range, including the first point outside the
16833 // visible range, consider y extremes
16834 validValue = (isNumber(y, true) || isArray(y)) && (!yAxis.isLog || (y.length || y > 0));
16835 withinRange = this.getExtremesFromAll || this.options.getExtremesFromAll || this.cropped ||
16836 ((xData[i + 1] || x) >= xMin && (xData[i - 1] || x) <= xMax);
16837
16838 if (validValue && withinRange) {
16839
16840 j = y.length;
16841 if (j) { // array, like ohlc or range data
16842 while (j--) {
16843 if (y[j] !== null) {
16844 activeYData[activeCounter++] = y[j];
16845 }
16846 }
16847 } else {
16848 activeYData[activeCounter++] = y;
16849 }
16850 }
16851 }
16852 this.dataMin = arrayMin(activeYData);
16853 this.dataMax = arrayMax(activeYData);
16854 },
16855
16856 /**
16857 * Translate data points from raw data values to chart specific positioning
16858 * data needed later in drawPoints, drawGraph and drawTracker.
16859 *
16860 * @function #translate
16861 * @memberOf Series
16862 * @returns {void}
16863 */
16864 translate: function() {
16865 if (!this.processedXData) { // hidden series
16866 this.processData();
16867 }
16868 this.generatePoints();
16869 var series = this,
16870 options = series.options,
16871 stacking = options.stacking,
16872 xAxis = series.xAxis,
16873 categories = xAxis.categories,
16874 yAxis = series.yAxis,
16875 points = series.points,
16876 dataLength = points.length,
16877 hasModifyValue = !!series.modifyValue,
16878 i,
16879 pointPlacement = options.pointPlacement,
16880 dynamicallyPlaced = pointPlacement === 'between' || isNumber(pointPlacement),
16881 threshold = options.threshold,
16882 stackThreshold = options.startFromThreshold ? threshold : 0,
16883 plotX,
16884 plotY,
16885 lastPlotX,
16886 stackIndicator,
16887 closestPointRangePx = Number.MAX_VALUE;
16888
16889 // Point placement is relative to each series pointRange (#5889)
16890 if (pointPlacement === 'between') {
16891 pointPlacement = 0.5;
16892 }
16893 if (isNumber(pointPlacement)) {
16894 pointPlacement *= pick(options.pointRange || xAxis.pointRange);
16895 }
16896
16897 // Translate each point
16898 for (i = 0; i < dataLength; i++) {
16899 var point = points[i],
16900 xValue = point.x,
16901 yValue = point.y,
16902 yBottom = point.low,
16903 stack = stacking && yAxis.stacks[(series.negStacks && yValue < (stackThreshold ? 0 : threshold) ? '-' : '') + series.stackKey],
16904 pointStack,
16905 stackValues;
16906
16907 // Discard disallowed y values for log axes (#3434)
16908 if (yAxis.isLog && yValue !== null && yValue <= 0) {
16909 point.isNull = true;
16910 }
16911
16912 // Get the plotX translation
16913 point.plotX = plotX = correctFloat( // #5236
16914 Math.min(Math.max(-1e5, xAxis.translate(
16915 xValue,
16916 0,
16917 0,
16918 0,
16919 1,
16920 pointPlacement,
16921 this.type === 'flags'
16922 )), 1e5) // #3923
16923 );
16924
16925 // Calculate the bottom y value for stacked series
16926 if (stacking && series.visible && !point.isNull && stack && stack[xValue]) {
16927 stackIndicator = series.getStackIndicator(stackIndicator, xValue, series.index);
16928 pointStack = stack[xValue];
16929 stackValues = pointStack.points[stackIndicator.key];
16930 yBottom = stackValues[0];
16931 yValue = stackValues[1];
16932
16933 if (yBottom === stackThreshold && stackIndicator.key === stack[xValue].base) {
16934 yBottom = pick(threshold, yAxis.min);
16935 }
16936 if (yAxis.isLog && yBottom <= 0) { // #1200, #1232
16937 yBottom = null;
16938 }
16939
16940 point.total = point.stackTotal = pointStack.total;
16941 point.percentage = pointStack.total && (point.y / pointStack.total * 100);
16942 point.stackY = yValue;
16943
16944 // Place the stack label
16945 pointStack.setOffset(series.pointXOffset || 0, series.barW || 0);
16946
16947 }
16948
16949 // Set translated yBottom or remove it
16950 point.yBottom = defined(yBottom) ?
16951 yAxis.translate(yBottom, 0, 1, 0, 1) :
16952 null;
16953
16954 // general hook, used for Highstock compare mode
16955 if (hasModifyValue) {
16956 yValue = series.modifyValue(yValue, point);
16957 }
16958
16959 // Set the the plotY value, reset it for redraws
16960 point.plotY = plotY = (typeof yValue === 'number' && yValue !== Infinity) ?
16961 Math.min(Math.max(-1e5, yAxis.translate(yValue, 0, 1, 0, 1)), 1e5) : // #3201
16962 undefined;
16963
16964 point.isInside = plotY !== undefined && plotY >= 0 && plotY <= yAxis.len && // #3519
16965 plotX >= 0 && plotX <= xAxis.len;
16966
16967
16968 // Set client related positions for mouse tracking
16969 point.clientX = dynamicallyPlaced ? correctFloat(xAxis.translate(xValue, 0, 0, 0, 1, pointPlacement)) : plotX; // #1514, #5383, #5518
16970
16971 point.negative = point.y < (threshold || 0);
16972
16973 // some API data
16974 point.category = categories && categories[point.x] !== undefined ?
16975 categories[point.x] : point.x;
16976
16977 // Determine auto enabling of markers (#3635, #5099)
16978 if (!point.isNull) {
16979 if (lastPlotX !== undefined) {
16980 closestPointRangePx = Math.min(closestPointRangePx, Math.abs(plotX - lastPlotX));
16981 }
16982 lastPlotX = plotX;
16983 }
16984
16985 }
16986 series.closestPointRangePx = closestPointRangePx;
16987 },
16988
16989 /**
16990 * Return the series points with null points filtered out
16991 */
16992 getValidPoints: function(points, insideOnly) {
16993 var chart = this.chart;
16994 return grep(points || this.points || [], function isValidPoint(point) { // #3916, #5029
16995 if (insideOnly && !chart.isInsidePlot(point.plotX, point.plotY, chart.inverted)) { // #5085
16996 return false;
16997 }
16998 return !point.isNull;
16999 });
17000 },
17001
17002 /**
17003 * Set the clipping for the series. For animated series it is called twice, first to initiate
17004 * animating the clip then the second time without the animation to set the final clip.
17005 */
17006 setClip: function(animation) {
17007 var chart = this.chart,
17008 options = this.options,
17009 renderer = chart.renderer,
17010 inverted = chart.inverted,
17011 seriesClipBox = this.clipBox,
17012 clipBox = seriesClipBox || chart.clipBox,
17013 sharedClipKey = this.sharedClipKey || ['_sharedClip', animation && animation.duration, animation && animation.easing, clipBox.height, options.xAxis, options.yAxis].join(','), // #4526
17014 clipRect = chart[sharedClipKey],
17015 markerClipRect = chart[sharedClipKey + 'm'];
17016
17017 // If a clipping rectangle with the same properties is currently present in the chart, use that.
17018 if (!clipRect) {
17019
17020 // When animation is set, prepare the initial positions
17021 if (animation) {
17022 clipBox.width = 0;
17023
17024 chart[sharedClipKey + 'm'] = markerClipRect = renderer.clipRect(-99, // include the width of the first marker
17025 inverted ? -chart.plotLeft : -chart.plotTop,
17026 99,
17027 inverted ? chart.chartWidth : chart.chartHeight
17028 );
17029 }
17030 chart[sharedClipKey] = clipRect = renderer.clipRect(clipBox);
17031 // Create hashmap for series indexes
17032 clipRect.count = {
17033 length: 0
17034 };
17035
17036 }
17037 if (animation) {
17038 if (!clipRect.count[this.index]) {
17039 clipRect.count[this.index] = true;
17040 clipRect.count.length += 1;
17041 }
17042 }
17043
17044 if (options.clip !== false) {
17045 this.group.clip(animation || seriesClipBox ? clipRect : chart.clipRect);
17046 this.markerGroup.clip(markerClipRect);
17047 this.sharedClipKey = sharedClipKey;
17048 }
17049
17050 // Remove the shared clipping rectangle when all series are shown
17051 if (!animation) {
17052 if (clipRect.count[this.index]) {
17053 delete clipRect.count[this.index];
17054 clipRect.count.length -= 1;
17055 }
17056
17057 if (clipRect.count.length === 0 && sharedClipKey && chart[sharedClipKey]) {
17058 if (!seriesClipBox) {
17059 chart[sharedClipKey] = chart[sharedClipKey].destroy();
17060 }
17061 if (chart[sharedClipKey + 'm']) {
17062 chart[sharedClipKey + 'm'] = chart[sharedClipKey + 'm'].destroy();
17063 }
17064 }
17065 }
17066 },
17067
17068 /**
17069 * Animate in the series
17070 */
17071 animate: function(init) {
17072 var series = this,
17073 chart = series.chart,
17074 clipRect,
17075 animation = animObject(series.options.animation),
17076 sharedClipKey;
17077
17078 // Initialize the animation. Set up the clipping rectangle.
17079 if (init) {
17080
17081 series.setClip(animation);
17082
17083 // Run the animation
17084 } else {
17085 sharedClipKey = this.sharedClipKey;
17086 clipRect = chart[sharedClipKey];
17087 if (clipRect) {
17088 clipRect.animate({
17089 width: chart.plotSizeX
17090 }, animation);
17091 }
17092 if (chart[sharedClipKey + 'm']) {
17093 chart[sharedClipKey + 'm'].animate({
17094 width: chart.plotSizeX + 99
17095 }, animation);
17096 }
17097
17098 // Delete this function to allow it only once
17099 series.animate = null;
17100
17101 }
17102 },
17103
17104 /**
17105 * This runs after animation to land on the final plot clipping
17106 */
17107 afterAnimate: function() {
17108 this.setClip();
17109 fireEvent(this, 'afterAnimate');
17110 },
17111
17112 /**
17113 * Draw the markers.
17114 *
17115 * @function #drawPoints
17116 * @memberOf Series
17117 * @returns {void}
17118 */
17119 drawPoints: function() {
17120 var series = this,
17121 points = series.points,
17122 chart = series.chart,
17123 plotY,
17124 i,
17125 point,
17126 symbol,
17127 graphic,
17128 options = series.options,
17129 seriesMarkerOptions = options.marker,
17130 pointMarkerOptions,
17131 hasPointMarker,
17132 enabled,
17133 isInside,
17134 markerGroup = series.markerGroup,
17135 xAxis = series.xAxis,
17136 markerAttribs,
17137 globallyEnabled = pick(
17138 seriesMarkerOptions.enabled,
17139 xAxis.isRadial ? true : null,
17140 series.closestPointRangePx > 2 * seriesMarkerOptions.radius
17141 );
17142
17143 if (seriesMarkerOptions.enabled !== false || series._hasPointMarkers) {
17144
17145 i = points.length;
17146 while (i--) {
17147 point = points[i];
17148 plotY = point.plotY;
17149 graphic = point.graphic;
17150 pointMarkerOptions = point.marker || {};
17151 hasPointMarker = !!point.marker;
17152 enabled = (globallyEnabled && pointMarkerOptions.enabled === undefined) || pointMarkerOptions.enabled;
17153 isInside = point.isInside;
17154
17155 // only draw the point if y is defined
17156 if (enabled && isNumber(plotY) && point.y !== null) {
17157
17158 // Shortcuts
17159 symbol = pick(pointMarkerOptions.symbol, series.symbol);
17160 point.hasImage = symbol.indexOf('url') === 0;
17161
17162 markerAttribs = series.markerAttribs(
17163 point,
17164 point.selected && 'select'
17165 );
17166
17167 if (graphic) { // update
17168 graphic[isInside ? 'show' : 'hide'](true) // Since the marker group isn't clipped, each individual marker must be toggled
17169 .animate(markerAttribs);
17170 } else if (isInside && (markerAttribs.width > 0 || point.hasImage)) {
17171 point.graphic = graphic = chart.renderer.symbol(
17172 symbol,
17173 markerAttribs.x,
17174 markerAttribs.y,
17175 markerAttribs.width,
17176 markerAttribs.height,
17177 hasPointMarker ? pointMarkerOptions : seriesMarkerOptions
17178 )
17179 .add(markerGroup);
17180 }
17181
17182
17183 // Presentational attributes
17184 if (graphic) {
17185 graphic.attr(series.pointAttribs(point, point.selected && 'select'));
17186 }
17187
17188
17189 if (graphic) {
17190 graphic.addClass(point.getClassName(), true);
17191 }
17192
17193 } else if (graphic) {
17194 point.graphic = graphic.destroy(); // #1269
17195 }
17196 }
17197 }
17198
17199 },
17200
17201 /**
17202 * Get non-presentational attributes for the point.
17203 */
17204 markerAttribs: function(point, state) {
17205 var seriesMarkerOptions = this.options.marker,
17206 seriesStateOptions,
17207 pointOptions = point && point.options,
17208 pointMarkerOptions = (pointOptions && pointOptions.marker) || {},
17209 pointStateOptions,
17210 radius = pick(
17211 pointMarkerOptions.radius,
17212 seriesMarkerOptions.radius
17213 ),
17214 attribs;
17215
17216 // Handle hover and select states
17217 if (state) {
17218 seriesStateOptions = seriesMarkerOptions.states[state];
17219 pointStateOptions = pointMarkerOptions.states &&
17220 pointMarkerOptions.states[state];
17221
17222 radius = pick(
17223 pointStateOptions && pointStateOptions.radius,
17224 seriesStateOptions && seriesStateOptions.radius,
17225 radius + (seriesStateOptions && seriesStateOptions.radiusPlus || 0)
17226 );
17227 }
17228
17229 if (point.hasImage) {
17230 radius = 0; // and subsequently width and height is not set
17231 }
17232
17233 attribs = {
17234 x: Math.floor(point.plotX) - radius, // Math.floor for #1843
17235 y: point.plotY - radius
17236 };
17237
17238 if (radius) {
17239 attribs.width = attribs.height = 2 * radius;
17240 }
17241
17242 return attribs;
17243
17244 },
17245
17246
17247 /**
17248 * Get presentational attributes for marker-based series (line, spline, scatter, bubble, mappoint...)
17249 */
17250 pointAttribs: function(point, state) {
17251 var seriesMarkerOptions = this.options.marker,
17252 seriesStateOptions,
17253 pointOptions = point && point.options,
17254 pointMarkerOptions = (pointOptions && pointOptions.marker) || {},
17255 pointStateOptions,
17256 color = this.color,
17257 pointColorOption = pointOptions && pointOptions.color,
17258 pointColor = point && point.color,
17259 strokeWidth = pick(
17260 pointMarkerOptions.lineWidth,
17261 seriesMarkerOptions.lineWidth
17262 ),
17263 zoneColor,
17264 fill,
17265 stroke,
17266 zone;
17267
17268 if (point && this.zones.length) {
17269 zone = point.getZone();
17270 if (zone && zone.color) {
17271 zoneColor = zone.color;
17272 }
17273 }
17274
17275 color = pointColorOption || zoneColor || pointColor || color;
17276 fill = pointMarkerOptions.fillColor || seriesMarkerOptions.fillColor || color;
17277 stroke = pointMarkerOptions.lineColor || seriesMarkerOptions.lineColor || color;
17278
17279 // Handle hover and select states
17280 if (state) {
17281 seriesStateOptions = seriesMarkerOptions.states[state];
17282 pointStateOptions = (pointMarkerOptions.states && pointMarkerOptions.states[state]) || {};
17283 strokeWidth = pick(
17284 pointStateOptions.lineWidth,
17285 seriesStateOptions.lineWidth,
17286 strokeWidth + pick(
17287 pointStateOptions.lineWidthPlus,
17288 seriesStateOptions.lineWidthPlus,
17289 0
17290 )
17291 );
17292 fill = pointStateOptions.fillColor || seriesStateOptions.fillColor || fill;
17293 stroke = pointStateOptions.lineColor || seriesStateOptions.lineColor || stroke;
17294 }
17295
17296 return {
17297 'stroke': stroke,
17298 'stroke-width': strokeWidth,
17299 'fill': fill
17300 };
17301 },
17302
17303 /**
17304 * Clear DOM objects and free up memory
17305 */
17306 destroy: function() {
17307 var series = this,
17308 chart = series.chart,
17309 issue134 = /AppleWebKit\/533/.test(win.navigator.userAgent),
17310 destroy,
17311 i,
17312 data = series.data || [],
17313 point,
17314 prop,
17315 axis;
17316
17317 // add event hook
17318 fireEvent(series, 'destroy');
17319
17320 // remove all events
17321 removeEvent(series);
17322
17323 // erase from axes
17324 each(series.axisTypes || [], function(AXIS) {
17325 axis = series[AXIS];
17326 if (axis && axis.series) {
17327 erase(axis.series, series);
17328 axis.isDirty = axis.forceRedraw = true;
17329 }
17330 });
17331
17332 // remove legend items
17333 if (series.legendItem) {
17334 series.chart.legend.destroyItem(series);
17335 }
17336
17337 // destroy all points with their elements
17338 i = data.length;
17339 while (i--) {
17340 point = data[i];
17341 if (point && point.destroy) {
17342 point.destroy();
17343 }
17344 }
17345 series.points = null;
17346
17347 // Clear the animation timeout if we are destroying the series during initial animation
17348 clearTimeout(series.animationTimeout);
17349
17350 // Destroy all SVGElements associated to the series
17351 for (prop in series) {
17352 if (series[prop] instanceof SVGElement && !series[prop].survive) { // Survive provides a hook for not destroying
17353
17354 // issue 134 workaround
17355 destroy = issue134 && prop === 'group' ?
17356 'hide' :
17357 'destroy';
17358
17359 series[prop][destroy]();
17360 }
17361 }
17362
17363 // remove from hoverSeries
17364 if (chart.hoverSeries === series) {
17365 chart.hoverSeries = null;
17366 }
17367 erase(chart.series, series);
17368
17369 // clear all members
17370 for (prop in series) {
17371 delete series[prop];
17372 }
17373 },
17374
17375 /**
17376 * Get the graph path
17377 */
17378 getGraphPath: function(points, nullsAsZeroes, connectCliffs) {
17379 var series = this,
17380 options = series.options,
17381 step = options.step,
17382 reversed,
17383 graphPath = [],
17384 xMap = [],
17385 gap;
17386
17387 points = points || series.points;
17388
17389 // Bottom of a stack is reversed
17390 reversed = points.reversed;
17391 if (reversed) {
17392 points.reverse();
17393 }
17394 // Reverse the steps (#5004)
17395 step = {
17396 right: 1,
17397 center: 2
17398 }[step] || (step && 3);
17399 if (step && reversed) {
17400 step = 4 - step;
17401 }
17402
17403 // Remove invalid points, especially in spline (#5015)
17404 if (options.connectNulls && !nullsAsZeroes && !connectCliffs) {
17405 points = this.getValidPoints(points);
17406 }
17407
17408 // Build the line
17409 each(points, function(point, i) {
17410
17411 var plotX = point.plotX,
17412 plotY = point.plotY,
17413 lastPoint = points[i - 1],
17414 pathToPoint; // the path to this point from the previous
17415
17416 if ((point.leftCliff || (lastPoint && lastPoint.rightCliff)) && !connectCliffs) {
17417 gap = true; // ... and continue
17418 }
17419
17420 // Line series, nullsAsZeroes is not handled
17421 if (point.isNull && !defined(nullsAsZeroes) && i > 0) {
17422 gap = !options.connectNulls;
17423
17424 // Area series, nullsAsZeroes is set
17425 } else if (point.isNull && !nullsAsZeroes) {
17426 gap = true;
17427
17428 } else {
17429
17430 if (i === 0 || gap) {
17431 pathToPoint = ['M', point.plotX, point.plotY];
17432
17433 } else if (series.getPointSpline) { // generate the spline as defined in the SplineSeries object
17434
17435 pathToPoint = series.getPointSpline(points, point, i);
17436
17437 } else if (step) {
17438
17439 if (step === 1) { // right
17440 pathToPoint = [
17441 'L',
17442 lastPoint.plotX,
17443 plotY
17444 ];
17445
17446 } else if (step === 2) { // center
17447 pathToPoint = [
17448 'L',
17449 (lastPoint.plotX + plotX) / 2,
17450 lastPoint.plotY,
17451 'L',
17452 (lastPoint.plotX + plotX) / 2,
17453 plotY
17454 ];
17455
17456 } else {
17457 pathToPoint = [
17458 'L',
17459 plotX,
17460 lastPoint.plotY
17461 ];
17462 }
17463 pathToPoint.push('L', plotX, plotY);
17464
17465 } else {
17466 // normal line to next point
17467 pathToPoint = [
17468 'L',
17469 plotX,
17470 plotY
17471 ];
17472 }
17473
17474 // Prepare for animation. When step is enabled, there are two path nodes for each x value.
17475 xMap.push(point.x);
17476 if (step) {
17477 xMap.push(point.x);
17478 }
17479
17480 graphPath.push.apply(graphPath, pathToPoint);
17481 gap = false;
17482 }
17483 });
17484
17485 graphPath.xMap = xMap;
17486 series.graphPath = graphPath;
17487
17488 return graphPath;
17489
17490 },
17491
17492 /**
17493 * Draw the actual graph
17494 */
17495 drawGraph: function() {
17496 var series = this,
17497 options = this.options,
17498 graphPath = (this.gappedPath || this.getGraphPath).call(this),
17499 props = [
17500 [
17501 'graph',
17502 'highcharts-graph',
17503
17504 options.lineColor || this.color,
17505 options.dashStyle
17506
17507 ]
17508 ];
17509
17510 // Add the zone properties if any
17511 each(this.zones, function(zone, i) {
17512 props.push([
17513 'zone-graph-' + i,
17514 'highcharts-graph highcharts-zone-graph-' + i + ' ' + (zone.className || ''),
17515
17516 zone.color || series.color,
17517 zone.dashStyle || options.dashStyle
17518
17519 ]);
17520 });
17521
17522 // Draw the graph
17523 each(props, function(prop, i) {
17524 var graphKey = prop[0],
17525 graph = series[graphKey],
17526 attribs;
17527
17528 if (graph) {
17529 graph.endX = graphPath.xMap;
17530 graph.animate({
17531 d: graphPath
17532 });
17533
17534 } else if (graphPath.length) { // #1487
17535
17536 series[graphKey] = series.chart.renderer.path(graphPath)
17537 .addClass(prop[1])
17538 .attr({
17539 zIndex: 1
17540 }) // #1069
17541 .add(series.group);
17542
17543
17544 attribs = {
17545 'stroke': prop[2],
17546 'stroke-width': options.lineWidth,
17547 'fill': (series.fillGraph && series.color) || 'none' // Polygon series use filled graph
17548 };
17549
17550 if (prop[3]) {
17551 attribs.dashstyle = prop[3];
17552 } else if (options.linecap !== 'square') {
17553 attribs['stroke-linecap'] = attribs['stroke-linejoin'] = 'round';
17554 }
17555
17556 graph = series[graphKey]
17557 .attr(attribs)
17558 .shadow((i < 2) && options.shadow); // add shadow to normal series (0) or to first zone (1) #3932
17559
17560 }
17561
17562 // Helpers for animation
17563 if (graph) {
17564 graph.startX = graphPath.xMap;
17565 //graph.shiftUnit = options.step ? 2 : 1;
17566 graph.isArea = graphPath.isArea; // For arearange animation
17567 }
17568 });
17569 },
17570
17571 /**
17572 * Clip the graphs into the positive and negative coloured graphs
17573 */
17574 applyZones: function() {
17575 var series = this,
17576 chart = this.chart,
17577 renderer = chart.renderer,
17578 zones = this.zones,
17579 translatedFrom,
17580 translatedTo,
17581 clips = this.clips || [],
17582 clipAttr,
17583 graph = this.graph,
17584 area = this.area,
17585 chartSizeMax = Math.max(chart.chartWidth, chart.chartHeight),
17586 axis = this[(this.zoneAxis || 'y') + 'Axis'],
17587 extremes,
17588 reversed,
17589 inverted = chart.inverted,
17590 horiz,
17591 pxRange,
17592 pxPosMin,
17593 pxPosMax,
17594 ignoreZones = false;
17595
17596 if (zones.length && (graph || area) && axis && axis.min !== undefined) {
17597 reversed = axis.reversed;
17598 horiz = axis.horiz;
17599 // The use of the Color Threshold assumes there are no gaps
17600 // so it is safe to hide the original graph and area
17601 if (graph) {
17602 graph.hide();
17603 }
17604 if (area) {
17605 area.hide();
17606 }
17607
17608 // Create the clips
17609 extremes = axis.getExtremes();
17610 each(zones, function(threshold, i) {
17611
17612 translatedFrom = reversed ?
17613 (horiz ? chart.plotWidth : 0) :
17614 (horiz ? 0 : axis.toPixels(extremes.min));
17615 translatedFrom = Math.min(Math.max(pick(translatedTo, translatedFrom), 0), chartSizeMax);
17616 translatedTo = Math.min(Math.max(Math.round(axis.toPixels(pick(threshold.value, extremes.max), true)), 0), chartSizeMax);
17617
17618 if (ignoreZones) {
17619 translatedFrom = translatedTo = axis.toPixels(extremes.max);
17620 }
17621
17622 pxRange = Math.abs(translatedFrom - translatedTo);
17623 pxPosMin = Math.min(translatedFrom, translatedTo);
17624 pxPosMax = Math.max(translatedFrom, translatedTo);
17625 if (axis.isXAxis) {
17626 clipAttr = {
17627 x: inverted ? pxPosMax : pxPosMin,
17628 y: 0,
17629 width: pxRange,
17630 height: chartSizeMax
17631 };
17632 if (!horiz) {
17633 clipAttr.x = chart.plotHeight - clipAttr.x;
17634 }
17635 } else {
17636 clipAttr = {
17637 x: 0,
17638 y: inverted ? pxPosMax : pxPosMin,
17639 width: chartSizeMax,
17640 height: pxRange
17641 };
17642 if (horiz) {
17643 clipAttr.y = chart.plotWidth - clipAttr.y;
17644 }
17645 }
17646
17647
17648 /// VML SUPPPORT
17649 if (inverted && renderer.isVML) {
17650 if (axis.isXAxis) {
17651 clipAttr = {
17652 x: 0,
17653 y: reversed ? pxPosMin : pxPosMax,
17654 height: clipAttr.width,
17655 width: chart.chartWidth
17656 };
17657 } else {
17658 clipAttr = {
17659 x: clipAttr.y - chart.plotLeft - chart.spacingBox.x,
17660 y: 0,
17661 width: clipAttr.height,
17662 height: chart.chartHeight
17663 };
17664 }
17665 }
17666 /// END OF VML SUPPORT
17667
17668
17669 if (clips[i]) {
17670 clips[i].animate(clipAttr);
17671 } else {
17672 clips[i] = renderer.clipRect(clipAttr);
17673
17674 if (graph) {
17675 series['zone-graph-' + i].clip(clips[i]);
17676 }
17677
17678 if (area) {
17679 series['zone-area-' + i].clip(clips[i]);
17680 }
17681 }
17682 // if this zone extends out of the axis, ignore the others
17683 ignoreZones = threshold.value > extremes.max;
17684 });
17685 this.clips = clips;
17686 }
17687 },
17688
17689 /**
17690 * Initialize and perform group inversion on series.group and series.markerGroup
17691 */
17692 invertGroups: function(inverted) {
17693 var series = this,
17694 chart = series.chart,
17695 remover;
17696
17697 function setInvert() {
17698 var size = {
17699 width: series.yAxis.len,
17700 height: series.xAxis.len
17701 };
17702
17703 each(['group', 'markerGroup'], function(groupName) {
17704 if (series[groupName]) {
17705 series[groupName].attr(size).invert(inverted);
17706 }
17707 });
17708 }
17709
17710 // Pie, go away (#1736)
17711 if (!series.xAxis) {
17712 return;
17713 }
17714
17715 // A fixed size is needed for inversion to work
17716 remover = addEvent(chart, 'resize', setInvert);
17717 addEvent(series, 'destroy', remover);
17718
17719 // Do it now
17720 setInvert(inverted); // do it now
17721
17722 // On subsequent render and redraw, just do setInvert without setting up events again
17723 series.invertGroups = setInvert;
17724 },
17725
17726 /**
17727 * General abstraction for creating plot groups like series.group, series.dataLabelsGroup and
17728 * series.markerGroup. On subsequent calls, the group will only be adjusted to the updated plot size.
17729 */
17730 plotGroup: function(prop, name, visibility, zIndex, parent) {
17731 var group = this[prop],
17732 isNew = !group;
17733
17734 // Generate it on first call
17735 if (isNew) {
17736 this[prop] = group = this.chart.renderer.g(name)
17737 .attr({
17738 zIndex: zIndex || 0.1 // IE8 and pointer logic use this
17739 })
17740 .add(parent);
17741
17742 group.addClass('highcharts-series-' + this.index + ' highcharts-' + this.type + '-series highcharts-color-' + this.colorIndex +
17743 ' ' + (this.options.className || ''));
17744 }
17745
17746 // Place it on first and subsequent (redraw) calls
17747 group.attr({
17748 visibility: visibility
17749 })[isNew ? 'attr' : 'animate'](this.getPlotBox());
17750 return group;
17751 },
17752
17753 /**
17754 * Get the translation and scale for the plot area of this series
17755 */
17756 getPlotBox: function() {
17757 var chart = this.chart,
17758 xAxis = this.xAxis,
17759 yAxis = this.yAxis;
17760
17761 // Swap axes for inverted (#2339)
17762 if (chart.inverted) {
17763 xAxis = yAxis;
17764 yAxis = this.xAxis;
17765 }
17766 return {
17767 translateX: xAxis ? xAxis.left : chart.plotLeft,
17768 translateY: yAxis ? yAxis.top : chart.plotTop,
17769 scaleX: 1, // #1623
17770 scaleY: 1
17771 };
17772 },
17773
17774 /**
17775 * Render the graph and markers
17776 */
17777 render: function() {
17778 var series = this,
17779 chart = series.chart,
17780 group,
17781 options = series.options,
17782 // Animation doesn't work in IE8 quirks when the group div is hidden,
17783 // and looks bad in other oldIE
17784 animDuration = !!series.animate && chart.renderer.isSVG && animObject(options.animation).duration,
17785 visibility = series.visible ? 'inherit' : 'hidden', // #2597
17786 zIndex = options.zIndex,
17787 hasRendered = series.hasRendered,
17788 chartSeriesGroup = chart.seriesGroup,
17789 inverted = chart.inverted;
17790
17791 // the group
17792 group = series.plotGroup(
17793 'group',
17794 'series',
17795 visibility,
17796 zIndex,
17797 chartSeriesGroup
17798 );
17799
17800 series.markerGroup = series.plotGroup(
17801 'markerGroup',
17802 'markers',
17803 visibility,
17804 zIndex,
17805 chartSeriesGroup
17806 );
17807
17808 // initiate the animation
17809 if (animDuration) {
17810 series.animate(true);
17811 }
17812
17813 // SVGRenderer needs to know this before drawing elements (#1089, #1795)
17814 group.inverted = series.isCartesian ? inverted : false;
17815
17816 // draw the graph if any
17817 if (series.drawGraph) {
17818 series.drawGraph();
17819 series.applyZones();
17820 }
17821
17822 /* each(series.points, function (point) {
17823 if (point.redraw) {
17824 point.redraw();
17825 }
17826 });*/
17827
17828 // draw the data labels (inn pies they go before the points)
17829 if (series.drawDataLabels) {
17830 series.drawDataLabels();
17831 }
17832
17833 // draw the points
17834 if (series.visible) {
17835 series.drawPoints();
17836 }
17837
17838
17839 // draw the mouse tracking area
17840 if (series.drawTracker && series.options.enableMouseTracking !== false) {
17841 series.drawTracker();
17842 }
17843
17844 // Handle inverted series and tracker groups
17845 series.invertGroups(inverted);
17846
17847 // Initial clipping, must be defined after inverting groups for VML. Applies to columns etc. (#3839).
17848 if (options.clip !== false && !series.sharedClipKey && !hasRendered) {
17849 group.clip(chart.clipRect);
17850 }
17851
17852 // Run the animation
17853 if (animDuration) {
17854 series.animate();
17855 }
17856
17857 // Call the afterAnimate function on animation complete (but don't overwrite the animation.complete option
17858 // which should be available to the user).
17859 if (!hasRendered) {
17860 series.animationTimeout = syncTimeout(function() {
17861 series.afterAnimate();
17862 }, animDuration);
17863 }
17864
17865 series.isDirty = series.isDirtyData = false; // means data is in accordance with what you see
17866 // (See #322) series.isDirty = series.isDirtyData = false; // means data is in accordance with what you see
17867 series.hasRendered = true;
17868 },
17869
17870 /**
17871 * Redraw the series after an update in the axes.
17872 */
17873 redraw: function() {
17874 var series = this,
17875 chart = series.chart,
17876 wasDirty = series.isDirty || series.isDirtyData, // cache it here as it is set to false in render, but used after
17877 group = series.group,
17878 xAxis = series.xAxis,
17879 yAxis = series.yAxis;
17880
17881 // reposition on resize
17882 if (group) {
17883 if (chart.inverted) {
17884 group.attr({
17885 width: chart.plotWidth,
17886 height: chart.plotHeight
17887 });
17888 }
17889
17890 group.animate({
17891 translateX: pick(xAxis && xAxis.left, chart.plotLeft),
17892 translateY: pick(yAxis && yAxis.top, chart.plotTop)
17893 });
17894 }
17895
17896 series.translate();
17897 series.render();
17898 if (wasDirty) { // #3868, #3945
17899 delete this.kdTree;
17900 }
17901 },
17902
17903 /**
17904 * KD Tree && PointSearching Implementation
17905 */
17906
17907 kdDimensions: 1,
17908 kdAxisArray: ['clientX', 'plotY'],
17909
17910 searchPoint: function(e, compareX) {
17911 var series = this,
17912 xAxis = series.xAxis,
17913 yAxis = series.yAxis,
17914 inverted = series.chart.inverted;
17915
17916 return this.searchKDTree({
17917 clientX: inverted ? xAxis.len - e.chartY + xAxis.pos : e.chartX - xAxis.pos,
17918 plotY: inverted ? yAxis.len - e.chartX + yAxis.pos : e.chartY - yAxis.pos
17919 }, compareX);
17920 },
17921
17922 buildKDTree: function() {
17923 var series = this,
17924 dimensions = series.kdDimensions;
17925
17926 // Internal function
17927 function _kdtree(points, depth, dimensions) {
17928 var axis,
17929 median,
17930 length = points && points.length;
17931
17932 if (length) {
17933
17934 // alternate between the axis
17935 axis = series.kdAxisArray[depth % dimensions];
17936
17937 // sort point array
17938 points.sort(function(a, b) {
17939 return a[axis] - b[axis];
17940 });
17941
17942 median = Math.floor(length / 2);
17943
17944 // build and return nod
17945 return {
17946 point: points[median],
17947 left: _kdtree(points.slice(0, median), depth + 1, dimensions),
17948 right: _kdtree(points.slice(median + 1), depth + 1, dimensions)
17949 };
17950
17951 }
17952 }
17953
17954 // Start the recursive build process with a clone of the points array and null points filtered out (#3873)
17955 function startRecursive() {
17956 series.kdTree = _kdtree(
17957 series.getValidPoints(
17958 null, !series.directTouch // For line-type series restrict to plot area, but column-type series not (#3916, #4511)
17959 ),
17960 dimensions,
17961 dimensions
17962 );
17963 }
17964 delete series.kdTree;
17965
17966 // For testing tooltips, don't build async
17967 syncTimeout(startRecursive, series.options.kdNow ? 0 : 1);
17968 },
17969
17970 searchKDTree: function(point, compareX) {
17971 var series = this,
17972 kdX = this.kdAxisArray[0],
17973 kdY = this.kdAxisArray[1],
17974 kdComparer = compareX ? 'distX' : 'dist';
17975
17976 // Set the one and two dimensional distance on the point object
17977 function setDistance(p1, p2) {
17978 var x = (defined(p1[kdX]) && defined(p2[kdX])) ? Math.pow(p1[kdX] - p2[kdX], 2) : null,
17979 y = (defined(p1[kdY]) && defined(p2[kdY])) ? Math.pow(p1[kdY] - p2[kdY], 2) : null,
17980 r = (x || 0) + (y || 0);
17981
17982 p2.dist = defined(r) ? Math.sqrt(r) : Number.MAX_VALUE;
17983 p2.distX = defined(x) ? Math.sqrt(x) : Number.MAX_VALUE;
17984 }
17985
17986 function _search(search, tree, depth, dimensions) {
17987 var point = tree.point,
17988 axis = series.kdAxisArray[depth % dimensions],
17989 tdist,
17990 sideA,
17991 sideB,
17992 ret = point,
17993 nPoint1,
17994 nPoint2;
17995
17996 setDistance(search, point);
17997
17998 // Pick side based on distance to splitting point
17999 tdist = search[axis] - point[axis];
18000 sideA = tdist < 0 ? 'left' : 'right';
18001 sideB = tdist < 0 ? 'right' : 'left';
18002
18003 // End of tree
18004 if (tree[sideA]) {
18005 nPoint1 = _search(search, tree[sideA], depth + 1, dimensions);
18006
18007 ret = (nPoint1[kdComparer] < ret[kdComparer] ? nPoint1 : point);
18008 }
18009 if (tree[sideB]) {
18010 // compare distance to current best to splitting point to decide wether to check side B or not
18011 if (Math.sqrt(tdist * tdist) < ret[kdComparer]) {
18012 nPoint2 = _search(search, tree[sideB], depth + 1, dimensions);
18013 ret = (nPoint2[kdComparer] < ret[kdComparer] ? nPoint2 : ret);
18014 }
18015 }
18016
18017 return ret;
18018 }
18019
18020 if (!this.kdTree) {
18021 this.buildKDTree();
18022 }
18023
18024 if (this.kdTree) {
18025 return _search(point,
18026 this.kdTree, this.kdDimensions, this.kdDimensions);
18027 }
18028 }
18029
18030 }); // end Series prototype
18031
18032 }(Highcharts));
18033 (function(H) {
18034 /**
18035 * (c) 2010-2016 Torstein Honsi
18036 *
18037 * License: www.highcharts.com/license
18038 */
18039 'use strict';
18040 var Axis = H.Axis,
18041 Chart = H.Chart,
18042 correctFloat = H.correctFloat,
18043 defined = H.defined,
18044 destroyObjectProperties = H.destroyObjectProperties,
18045 each = H.each,
18046 format = H.format,
18047 pick = H.pick,
18048 Series = H.Series;
18049
18050 /**
18051 * The class for stacks. Each stack, on a specific X value and either negative
18052 * or positive, has its own stack item.
18053 *
18054 * @class
18055 */
18056 function StackItem(axis, options, isNegative, x, stackOption) {
18057
18058 var inverted = axis.chart.inverted;
18059
18060 this.axis = axis;
18061
18062 // Tells if the stack is negative
18063 this.isNegative = isNegative;
18064
18065 // Save the options to be able to style the label
18066 this.options = options;
18067
18068 // Save the x value to be able to position the label later
18069 this.x = x;
18070
18071 // Initialize total value
18072 this.total = null;
18073
18074 // This will keep each points' extremes stored by series.index and point
18075 // index
18076 this.points = {};
18077
18078 // Save the stack option on the series configuration object, and whether to
18079 // treat it as percent
18080 this.stack = stackOption;
18081 this.leftCliff = 0;
18082 this.rightCliff = 0;
18083
18084 // The align options and text align varies on whether the stack is negative
18085 // and if the chart is inverted or not.
18086 // First test the user supplied value, then use the dynamic.
18087 this.alignOptions = {
18088 align: options.align ||
18089 (inverted ? (isNegative ? 'left' : 'right') : 'center'),
18090 verticalAlign: options.verticalAlign ||
18091 (inverted ? 'middle' : (isNegative ? 'bottom' : 'top')),
18092 y: pick(options.y, inverted ? 4 : (isNegative ? 14 : -6)),
18093 x: pick(options.x, inverted ? (isNegative ? -6 : 6) : 0)
18094 };
18095
18096 this.textAlign = options.textAlign ||
18097 (inverted ? (isNegative ? 'right' : 'left') : 'center');
18098 }
18099
18100 StackItem.prototype = {
18101 destroy: function() {
18102 destroyObjectProperties(this, this.axis);
18103 },
18104
18105 /**
18106 * Renders the stack total label and adds it to the stack label group.
18107 */
18108 render: function(group) {
18109 var options = this.options,
18110 formatOption = options.format,
18111 str = formatOption ?
18112 format(formatOption, this) :
18113 options.formatter.call(this); // format the text in the label
18114
18115 // Change the text to reflect the new total and set visibility to hidden
18116 // in case the serie is hidden
18117 if (this.label) {
18118 this.label.attr({
18119 text: str,
18120 visibility: 'hidden'
18121 });
18122 // Create new label
18123 } else {
18124 this.label =
18125 this.axis.chart.renderer.text(str, null, null, options.useHTML)
18126 .css(options.style)
18127 .attr({
18128 align: this.textAlign,
18129 rotation: options.rotation,
18130 visibility: 'hidden' // hidden until setOffset is called
18131 })
18132 .add(group); // add to the labels-group
18133 }
18134 },
18135
18136 /**
18137 * Sets the offset that the stack has from the x value and repositions the
18138 * label.
18139 */
18140 setOffset: function(xOffset, xWidth) {
18141 var stackItem = this,
18142 axis = stackItem.axis,
18143 chart = axis.chart,
18144 inverted = chart.inverted,
18145 reversed = axis.reversed,
18146 neg = (this.isNegative && !reversed) ||
18147 (!this.isNegative && reversed), // #4056
18148 // stack value translated mapped to chart coordinates
18149 y = axis.translate(
18150 axis.usePercentage ? 100 : this.total,
18151 0,
18152 0,
18153 0,
18154 1
18155 ),
18156 yZero = axis.translate(0), // stack origin
18157 h = Math.abs(y - yZero), // stack height
18158 x = chart.xAxis[0].translate(this.x) + xOffset, // stack x position
18159 plotHeight = chart.plotHeight,
18160 stackBox = { // this is the box for the complete stack
18161 x: inverted ? (neg ? y : y - h) : x,
18162 y: inverted ?
18163 plotHeight - x - xWidth : (neg ? (plotHeight - y - h) :
18164 plotHeight - y),
18165 width: inverted ? h : xWidth,
18166 height: inverted ? xWidth : h
18167 },
18168 label = this.label,
18169 alignAttr;
18170
18171 if (label) {
18172 // Align the label to the box
18173 label.align(this.alignOptions, null, stackBox);
18174
18175 // Set visibility (#678)
18176 alignAttr = label.alignAttr;
18177 label[
18178 this.options.crop === false || chart.isInsidePlot(
18179 alignAttr.x,
18180 alignAttr.y
18181 ) ? 'show' : 'hide'](true);
18182 }
18183 }
18184 };
18185
18186 /**
18187 * Generate stacks for each series and calculate stacks total values
18188 */
18189 Chart.prototype.getStacks = function() {
18190 var chart = this;
18191
18192 // reset stacks for each yAxis
18193 each(chart.yAxis, function(axis) {
18194 if (axis.stacks && axis.hasVisibleSeries) {
18195 axis.oldStacks = axis.stacks;
18196 }
18197 });
18198
18199 each(chart.series, function(series) {
18200 if (series.options.stacking && (series.visible === true ||
18201 chart.options.chart.ignoreHiddenSeries === false)) {
18202 series.stackKey = series.type + pick(series.options.stack, '');
18203 }
18204 });
18205 };
18206
18207
18208 // Stacking methods defined on the Axis prototype
18209
18210 /**
18211 * Build the stacks from top down
18212 */
18213 Axis.prototype.buildStacks = function() {
18214 var axisSeries = this.series,
18215 series,
18216 reversedStacks = pick(this.options.reversedStacks, true),
18217 len = axisSeries.length,
18218 i;
18219 if (!this.isXAxis) {
18220 this.usePercentage = false;
18221 i = len;
18222 while (i--) {
18223 axisSeries[reversedStacks ? i : len - i - 1].setStackedPoints();
18224 }
18225
18226 i = len;
18227 while (i--) {
18228 series = axisSeries[reversedStacks ? i : len - i - 1];
18229 if (series.setStackCliffs) {
18230 series.setStackCliffs();
18231 }
18232 }
18233 // Loop up again to compute percent stack
18234 if (this.usePercentage) {
18235 for (i = 0; i < len; i++) {
18236 axisSeries[i].setPercentStacks();
18237 }
18238 }
18239 }
18240 };
18241
18242 Axis.prototype.renderStackTotals = function() {
18243 var axis = this,
18244 chart = axis.chart,
18245 renderer = chart.renderer,
18246 stacks = axis.stacks,
18247 stackKey,
18248 oneStack,
18249 stackCategory,
18250 stackTotalGroup = axis.stackTotalGroup;
18251
18252 // Create a separate group for the stack total labels
18253 if (!stackTotalGroup) {
18254 axis.stackTotalGroup = stackTotalGroup =
18255 renderer.g('stack-labels')
18256 .attr({
18257 visibility: 'visible',
18258 zIndex: 6
18259 })
18260 .add();
18261 }
18262
18263 // plotLeft/Top will change when y axis gets wider so we need to translate
18264 // the stackTotalGroup at every render call. See bug #506 and #516
18265 stackTotalGroup.translate(chart.plotLeft, chart.plotTop);
18266
18267 // Render each stack total
18268 for (stackKey in stacks) {
18269 oneStack = stacks[stackKey];
18270 for (stackCategory in oneStack) {
18271 oneStack[stackCategory].render(stackTotalGroup);
18272 }
18273 }
18274 };
18275
18276 /**
18277 * Set all the stacks to initial states and destroy unused ones.
18278 */
18279 Axis.prototype.resetStacks = function() {
18280 var stacks = this.stacks,
18281 type,
18282 i;
18283 if (!this.isXAxis) {
18284 for (type in stacks) {
18285 for (i in stacks[type]) {
18286
18287 // Clean up memory after point deletion (#1044, #4320)
18288 if (stacks[type][i].touched < this.stacksTouched) {
18289 stacks[type][i].destroy();
18290 delete stacks[type][i];
18291
18292 // Reset stacks
18293 } else {
18294 stacks[type][i].total = null;
18295 stacks[type][i].cum = null;
18296 }
18297 }
18298 }
18299 }
18300 };
18301
18302 Axis.prototype.cleanStacks = function() {
18303 var stacks, type, i;
18304
18305 if (!this.isXAxis) {
18306 if (this.oldStacks) {
18307 stacks = this.stacks = this.oldStacks;
18308 }
18309
18310 // reset stacks
18311 for (type in stacks) {
18312 for (i in stacks[type]) {
18313 stacks[type][i].cum = stacks[type][i].total;
18314 }
18315 }
18316 }
18317 };
18318
18319
18320 // Stacking methods defnied for Series prototype
18321
18322 /**
18323 * Adds series' points value to corresponding stack
18324 */
18325 Series.prototype.setStackedPoints = function() {
18326 if (!this.options.stacking || (this.visible !== true &&
18327 this.chart.options.chart.ignoreHiddenSeries !== false)) {
18328 return;
18329 }
18330
18331 var series = this,
18332 xData = series.processedXData,
18333 yData = series.processedYData,
18334 stackedYData = [],
18335 yDataLength = yData.length,
18336 seriesOptions = series.options,
18337 threshold = seriesOptions.threshold,
18338 stackThreshold = seriesOptions.startFromThreshold ? threshold : 0,
18339 stackOption = seriesOptions.stack,
18340 stacking = seriesOptions.stacking,
18341 stackKey = series.stackKey,
18342 negKey = '-' + stackKey,
18343 negStacks = series.negStacks,
18344 yAxis = series.yAxis,
18345 stacks = yAxis.stacks,
18346 oldStacks = yAxis.oldStacks,
18347 stackIndicator,
18348 isNegative,
18349 stack,
18350 other,
18351 key,
18352 pointKey,
18353 i,
18354 x,
18355 y;
18356
18357
18358 yAxis.stacksTouched += 1;
18359
18360 // loop over the non-null y values and read them into a local array
18361 for (i = 0; i < yDataLength; i++) {
18362 x = xData[i];
18363 y = yData[i];
18364 stackIndicator = series.getStackIndicator(
18365 stackIndicator,
18366 x,
18367 series.index
18368 );
18369 pointKey = stackIndicator.key;
18370 // Read stacked values into a stack based on the x value,
18371 // the sign of y and the stack key. Stacking is also handled for null
18372 // values (#739)
18373 isNegative = negStacks && y < (stackThreshold ? 0 : threshold);
18374 key = isNegative ? negKey : stackKey;
18375
18376 // Create empty object for this stack if it doesn't exist yet
18377 if (!stacks[key]) {
18378 stacks[key] = {};
18379 }
18380
18381 // Initialize StackItem for this x
18382 if (!stacks[key][x]) {
18383 if (oldStacks[key] && oldStacks[key][x]) {
18384 stacks[key][x] = oldStacks[key][x];
18385 stacks[key][x].total = null;
18386 } else {
18387 stacks[key][x] = new StackItem(
18388 yAxis,
18389 yAxis.options.stackLabels,
18390 isNegative,
18391 x,
18392 stackOption
18393 );
18394 }
18395 }
18396
18397 // If the StackItem doesn't exist, create it first
18398 stack = stacks[key][x];
18399 if (y !== null) {
18400 stack.points[pointKey] = stack.points[series.index] = [pick(stack.cum, stackThreshold)];
18401
18402 // Record the base of the stack
18403 if (!defined(stack.cum)) {
18404 stack.base = pointKey;
18405 }
18406 stack.touched = yAxis.stacksTouched;
18407
18408
18409 // In area charts, if there are multiple points on the same X value,
18410 // let the area fill the full span of those points
18411 if (stackIndicator.index > 0 && series.singleStacks === false) {
18412 stack.points[pointKey][0] =
18413 stack.points[series.index + ',' + x + ',0'][0];
18414 }
18415 }
18416
18417 // Add value to the stack total
18418 if (stacking === 'percent') {
18419
18420 // Percent stacked column, totals are the same for the positive and
18421 // negative stacks
18422 other = isNegative ? stackKey : negKey;
18423 if (negStacks && stacks[other] && stacks[other][x]) {
18424 other = stacks[other][x];
18425 stack.total = other.total =
18426 Math.max(other.total, stack.total) + Math.abs(y) || 0;
18427
18428 // Percent stacked areas
18429 } else {
18430 stack.total = correctFloat(stack.total + (Math.abs(y) || 0));
18431 }
18432 } else {
18433 stack.total = correctFloat(stack.total + (y || 0));
18434 }
18435
18436 stack.cum = pick(stack.cum, stackThreshold) + (y || 0);
18437
18438 if (y !== null) {
18439 stack.points[pointKey].push(stack.cum);
18440 stackedYData[i] = stack.cum;
18441 }
18442
18443 }
18444
18445 if (stacking === 'percent') {
18446 yAxis.usePercentage = true;
18447 }
18448
18449 this.stackedYData = stackedYData; // To be used in getExtremes
18450
18451 // Reset old stacks
18452 yAxis.oldStacks = {};
18453 };
18454
18455 /**
18456 * Iterate over all stacks and compute the absolute values to percent
18457 */
18458 Series.prototype.setPercentStacks = function() {
18459 var series = this,
18460 stackKey = series.stackKey,
18461 stacks = series.yAxis.stacks,
18462 processedXData = series.processedXData,
18463 stackIndicator;
18464
18465 each([stackKey, '-' + stackKey], function(key) {
18466 var i = processedXData.length,
18467 x,
18468 stack,
18469 pointExtremes,
18470 totalFactor;
18471
18472 while (i--) {
18473 x = processedXData[i];
18474 stackIndicator = series.getStackIndicator(
18475 stackIndicator,
18476 x,
18477 series.index,
18478 key
18479 );
18480 stack = stacks[key] && stacks[key][x];
18481 pointExtremes = stack && stack.points[stackIndicator.key];
18482 if (pointExtremes) {
18483 totalFactor = stack.total ? 100 / stack.total : 0;
18484 // Y bottom value
18485 pointExtremes[0] = correctFloat(pointExtremes[0] * totalFactor);
18486 // Y value
18487 pointExtremes[1] = correctFloat(pointExtremes[1] * totalFactor);
18488 series.stackedYData[i] = pointExtremes[1];
18489 }
18490 }
18491 });
18492 };
18493
18494 /**
18495 * Get stack indicator, according to it's x-value, to determine points with the
18496 * same x-value
18497 */
18498 Series.prototype.getStackIndicator = function(stackIndicator, x, index, key) {
18499 // Update stack indicator, when:
18500 // first point in a stack || x changed || stack type (negative vs positive)
18501 // changed:
18502 if (!defined(stackIndicator) || stackIndicator.x !== x ||
18503 (key && stackIndicator.key !== key)) {
18504 stackIndicator = {
18505 x: x,
18506 index: 0,
18507 key: key
18508 };
18509 } else {
18510 stackIndicator.index++;
18511 }
18512
18513 stackIndicator.key = [index, x, stackIndicator.index].join(',');
18514
18515 return stackIndicator;
18516 };
18517
18518 }(Highcharts));
18519 (function(H) {
18520 /**
18521 * (c) 2010-2016 Torstein Honsi
18522 *
18523 * License: www.highcharts.com/license
18524 */
18525 'use strict';
18526 var addEvent = H.addEvent,
18527 animate = H.animate,
18528 Axis = H.Axis,
18529 Chart = H.Chart,
18530 createElement = H.createElement,
18531 css = H.css,
18532 defined = H.defined,
18533 each = H.each,
18534 erase = H.erase,
18535 extend = H.extend,
18536 fireEvent = H.fireEvent,
18537 inArray = H.inArray,
18538 isNumber = H.isNumber,
18539 isObject = H.isObject,
18540 merge = H.merge,
18541 pick = H.pick,
18542 Point = H.Point,
18543 Series = H.Series,
18544 seriesTypes = H.seriesTypes,
18545 setAnimation = H.setAnimation,
18546 splat = H.splat;
18547
18548 // Extend the Chart prototype for dynamic methods
18549 extend(Chart.prototype, /** @lends Highcharts.Chart.prototype */ {
18550
18551 /**
18552 * Add a series dynamically after time
18553 *
18554 * @param {Object} options The config options
18555 * @param {Boolean} redraw Whether to redraw the chart after adding. Defaults to true.
18556 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
18557 * configuration
18558 *
18559 * @return {Object} series The newly created series object
18560 */
18561 addSeries: function(options, redraw, animation) {
18562 var series,
18563 chart = this;
18564
18565 if (options) {
18566 redraw = pick(redraw, true); // defaults to true
18567
18568 fireEvent(chart, 'addSeries', {
18569 options: options
18570 }, function() {
18571 series = chart.initSeries(options);
18572
18573 chart.isDirtyLegend = true; // the series array is out of sync with the display
18574 chart.linkSeries();
18575 if (redraw) {
18576 chart.redraw(animation);
18577 }
18578 });
18579 }
18580
18581 return series;
18582 },
18583
18584 /**
18585 * Add an axis to the chart
18586 * @param {Object} options The axis option
18587 * @param {Boolean} isX Whether it is an X axis or a value axis
18588 */
18589 addAxis: function(options, isX, redraw, animation) {
18590 var key = isX ? 'xAxis' : 'yAxis',
18591 chartOptions = this.options,
18592 userOptions = merge(options, {
18593 index: this[key].length,
18594 isX: isX
18595 });
18596
18597 new Axis(this, userOptions); // eslint-disable-line no-new
18598
18599 // Push the new axis options to the chart options
18600 chartOptions[key] = splat(chartOptions[key] || {});
18601 chartOptions[key].push(userOptions);
18602
18603 if (pick(redraw, true)) {
18604 this.redraw(animation);
18605 }
18606 },
18607
18608 /**
18609 * Dim the chart and show a loading text or symbol
18610 * @param {String} str An optional text to show in the loading label instead of the default one
18611 */
18612 showLoading: function(str) {
18613 var chart = this,
18614 options = chart.options,
18615 loadingDiv = chart.loadingDiv,
18616 loadingOptions = options.loading,
18617 setLoadingSize = function() {
18618 if (loadingDiv) {
18619 css(loadingDiv, {
18620 left: chart.plotLeft + 'px',
18621 top: chart.plotTop + 'px',
18622 width: chart.plotWidth + 'px',
18623 height: chart.plotHeight + 'px'
18624 });
18625 }
18626 };
18627
18628 // create the layer at the first call
18629 if (!loadingDiv) {
18630 chart.loadingDiv = loadingDiv = createElement('div', {
18631 className: 'highcharts-loading highcharts-loading-hidden'
18632 }, null, chart.container);
18633
18634 chart.loadingSpan = createElement(
18635 'span', {
18636 className: 'highcharts-loading-inner'
18637 },
18638 null,
18639 loadingDiv
18640 );
18641 addEvent(chart, 'redraw', setLoadingSize); // #1080
18642 }
18643
18644 loadingDiv.className = 'highcharts-loading';
18645
18646 // Update text
18647 chart.loadingSpan.innerHTML = str || options.lang.loading;
18648
18649
18650 // Update visuals
18651 css(loadingDiv, extend(loadingOptions.style, {
18652 zIndex: 10
18653 }));
18654 css(chart.loadingSpan, loadingOptions.labelStyle);
18655
18656 // Show it
18657 if (!chart.loadingShown) {
18658 css(loadingDiv, {
18659 opacity: 0,
18660 display: ''
18661 });
18662 animate(loadingDiv, {
18663 opacity: loadingOptions.style.opacity || 0.5
18664 }, {
18665 duration: loadingOptions.showDuration || 0
18666 });
18667 }
18668
18669
18670 chart.loadingShown = true;
18671 setLoadingSize();
18672 },
18673
18674 /**
18675 * Hide the loading layer
18676 */
18677 hideLoading: function() {
18678 var options = this.options,
18679 loadingDiv = this.loadingDiv;
18680
18681 if (loadingDiv) {
18682 loadingDiv.className = 'highcharts-loading highcharts-loading-hidden';
18683
18684 animate(loadingDiv, {
18685 opacity: 0
18686 }, {
18687 duration: options.loading.hideDuration || 100,
18688 complete: function() {
18689 css(loadingDiv, {
18690 display: 'none'
18691 });
18692 }
18693 });
18694
18695 }
18696 this.loadingShown = false;
18697 },
18698
18699 /**
18700 * These properties cause isDirtyBox to be set to true when updating. Can be extended from plugins.
18701 */
18702 propsRequireDirtyBox: ['backgroundColor', 'borderColor', 'borderWidth', 'margin', 'marginTop', 'marginRight',
18703 'marginBottom', 'marginLeft', 'spacing', 'spacingTop', 'spacingRight', 'spacingBottom', 'spacingLeft',
18704 'borderRadius', 'plotBackgroundColor', 'plotBackgroundImage', 'plotBorderColor', 'plotBorderWidth',
18705 'plotShadow', 'shadow'
18706 ],
18707
18708 /**
18709 * These properties cause all series to be updated when updating. Can be
18710 * extended from plugins.
18711 */
18712 propsRequireUpdateSeries: ['chart.inverted', 'chart.polar',
18713 'chart.ignoreHiddenSeries', 'chart.type', 'colors', 'plotOptions'
18714 ],
18715
18716 /**
18717 * Chart.update function that takes the whole options stucture.
18718 */
18719 update: function(options, redraw) {
18720 var key,
18721 adders = {
18722 credits: 'addCredits',
18723 title: 'setTitle',
18724 subtitle: 'setSubtitle'
18725 },
18726 optionsChart = options.chart,
18727 updateAllAxes,
18728 updateAllSeries,
18729 newWidth,
18730 newHeight;
18731
18732 // If the top-level chart option is present, some special updates are required
18733 if (optionsChart) {
18734 merge(true, this.options.chart, optionsChart);
18735
18736 // Setter function
18737 if ('className' in optionsChart) {
18738 this.setClassName(optionsChart.className);
18739 }
18740
18741 if ('inverted' in optionsChart || 'polar' in optionsChart) {
18742 this.propFromSeries(); // Parses options.chart.inverted and options.chart.polar together with the available series
18743 updateAllAxes = true;
18744 }
18745
18746 for (key in optionsChart) {
18747 if (optionsChart.hasOwnProperty(key)) {
18748 if (inArray('chart.' + key, this.propsRequireUpdateSeries) !== -1) {
18749 updateAllSeries = true;
18750 }
18751 // Only dirty box
18752 if (inArray(key, this.propsRequireDirtyBox) !== -1) {
18753 this.isDirtyBox = true;
18754 }
18755
18756 }
18757 }
18758
18759
18760 if ('style' in optionsChart) {
18761 this.renderer.setStyle(optionsChart.style);
18762 }
18763
18764 }
18765
18766 // Some option stuctures correspond one-to-one to chart objects that have
18767 // update methods, for example
18768 // options.credits => chart.credits
18769 // options.legend => chart.legend
18770 // options.title => chart.title
18771 // options.tooltip => chart.tooltip
18772 // options.subtitle => chart.subtitle
18773 // options.navigator => chart.navigator
18774 // options.scrollbar => chart.scrollbar
18775 for (key in options) {
18776 if (this[key] && typeof this[key].update === 'function') {
18777 this[key].update(options[key], false);
18778
18779 // If a one-to-one object does not exist, look for an adder function
18780 } else if (typeof this[adders[key]] === 'function') {
18781 this[adders[key]](options[key]);
18782 }
18783
18784 if (key !== 'chart' && inArray(key, this.propsRequireUpdateSeries) !== -1) {
18785 updateAllSeries = true;
18786 }
18787 }
18788
18789
18790 if (options.colors) {
18791 this.options.colors = options.colors;
18792 }
18793
18794
18795 if (options.plotOptions) {
18796 merge(true, this.options.plotOptions, options.plotOptions);
18797 }
18798
18799 // Setters for collections. For axes and series, each item is referred by an id. If the
18800 // id is not found, it defaults to the first item in the collection, so setting series
18801 // without an id, will update the first series in the chart.
18802 each(['xAxis', 'yAxis', 'series'], function(coll) {
18803 if (options[coll]) {
18804 each(splat(options[coll]), function(newOptions) {
18805 var item = (defined(newOptions.id) && this.get(newOptions.id)) || this[coll][0];
18806 if (item && item.coll === coll) {
18807 item.update(newOptions, false);
18808 }
18809 }, this);
18810 }
18811 }, this);
18812
18813 if (updateAllAxes) {
18814 each(this.axes, function(axis) {
18815 axis.update({}, false);
18816 });
18817 }
18818
18819 // Certain options require the whole series structure to be thrown away
18820 // and rebuilt
18821 if (updateAllSeries) {
18822 each(this.series, function(series) {
18823 series.update({}, false);
18824 });
18825 }
18826
18827 // For loading, just update the options, do not redraw
18828 if (options.loading) {
18829 merge(true, this.options.loading, options.loading);
18830 }
18831
18832 // Update size. Redraw is forced.
18833 newWidth = optionsChart && optionsChart.width;
18834 newHeight = optionsChart && optionsChart.height;
18835 if ((isNumber(newWidth) && newWidth !== this.chartWidth) ||
18836 (isNumber(newHeight) && newHeight !== this.chartHeight)) {
18837 this.setSize(newWidth, newHeight);
18838 } else if (pick(redraw, true)) {
18839 this.redraw();
18840 }
18841 },
18842
18843 /**
18844 * Setter function to allow use from chart.update
18845 */
18846 setSubtitle: function(options) {
18847 this.setTitle(undefined, options);
18848 }
18849
18850
18851 });
18852
18853 // extend the Point prototype for dynamic methods
18854 extend(Point.prototype, /** @lends Point.prototype */ {
18855 /**
18856 * Point.update with new options (typically x/y data) and optionally redraw the series.
18857 *
18858 * @param {Object} options Point options as defined in the series.data array
18859 * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call
18860 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
18861 * configuration
18862 */
18863 update: function(options, redraw, animation, runEvent) {
18864 var point = this,
18865 series = point.series,
18866 graphic = point.graphic,
18867 i,
18868 chart = series.chart,
18869 seriesOptions = series.options;
18870
18871 redraw = pick(redraw, true);
18872
18873 function update() {
18874
18875 point.applyOptions(options);
18876
18877 // Update visuals
18878 if (point.y === null && graphic) { // #4146
18879 point.graphic = graphic.destroy();
18880 }
18881 if (isObject(options, true)) {
18882 // Destroy so we can get new elements
18883 if (graphic && graphic.element) {
18884 if (options && options.marker && options.marker.symbol) {
18885 point.graphic = graphic.destroy();
18886 }
18887 }
18888 if (options && options.dataLabels && point.dataLabel) { // #2468
18889 point.dataLabel = point.dataLabel.destroy();
18890 }
18891 }
18892
18893 // record changes in the parallel arrays
18894 i = point.index;
18895 series.updateParallelArrays(point, i);
18896
18897 // Record the options to options.data. If there is an object from before,
18898 // use point options, otherwise use raw options. (#4701)
18899 seriesOptions.data[i] = isObject(seriesOptions.data[i], true) ? point.options : options;
18900
18901 // redraw
18902 series.isDirty = series.isDirtyData = true;
18903 if (!series.fixedBox && series.hasCartesianSeries) { // #1906, #2320
18904 chart.isDirtyBox = true;
18905 }
18906
18907 if (seriesOptions.legendType === 'point') { // #1831, #1885
18908 chart.isDirtyLegend = true;
18909 }
18910 if (redraw) {
18911 chart.redraw(animation);
18912 }
18913 }
18914
18915 // Fire the event with a default handler of doing the update
18916 if (runEvent === false) { // When called from setData
18917 update();
18918 } else {
18919 point.firePointEvent('update', {
18920 options: options
18921 }, update);
18922 }
18923 },
18924
18925 /**
18926 * Remove a point and optionally redraw the series and if necessary the axes
18927 * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call
18928 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
18929 * configuration
18930 */
18931 remove: function(redraw, animation) {
18932 this.series.removePoint(inArray(this, this.series.data), redraw, animation);
18933 }
18934 });
18935
18936 // Extend the series prototype for dynamic methods
18937 extend(Series.prototype, /** @lends Series.prototype */ {
18938 /**
18939 * Add a point dynamically after chart load time
18940 * @param {Object} options Point options as given in series.data
18941 * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call
18942 * @param {Boolean} shift If shift is true, a point is shifted off the start
18943 * of the series as one is appended to the end.
18944 * @param {Boolean|AnimationOptions} animation Whether to apply animation, and optionally animation
18945 * configuration
18946 */
18947 addPoint: function(options, redraw, shift, animation) {
18948 var series = this,
18949 seriesOptions = series.options,
18950 data = series.data,
18951 chart = series.chart,
18952 names = series.xAxis && series.xAxis.names,
18953 dataOptions = seriesOptions.data,
18954 point,
18955 isInTheMiddle,
18956 xData = series.xData,
18957 i,
18958 x;
18959
18960 // Optional redraw, defaults to true
18961 redraw = pick(redraw, true);
18962
18963 // Get options and push the point to xData, yData and series.options. In series.generatePoints
18964 // the Point instance will be created on demand and pushed to the series.data array.
18965 point = {
18966 series: series
18967 };
18968 series.pointClass.prototype.applyOptions.apply(point, [options]);
18969 x = point.x;
18970
18971 // Get the insertion point
18972 i = xData.length;
18973 if (series.requireSorting && x < xData[i - 1]) {
18974 isInTheMiddle = true;
18975 while (i && xData[i - 1] > x) {
18976 i--;
18977 }
18978 }
18979
18980 series.updateParallelArrays(point, 'splice', i, 0, 0); // insert undefined item
18981 series.updateParallelArrays(point, i); // update it
18982
18983 if (names && point.name) {
18984 names[x] = point.name;
18985 }
18986 dataOptions.splice(i, 0, options);
18987
18988 if (isInTheMiddle) {
18989 series.data.splice(i, 0, null);
18990 series.processData();
18991 }
18992
18993 // Generate points to be added to the legend (#1329)
18994 if (seriesOptions.legendType === 'point') {
18995 series.generatePoints();
18996 }
18997
18998 // Shift the first point off the parallel arrays
18999 if (shift) {
19000 if (data[0] && data[0].remove) {
19001 data[0].remove(false);
19002 } else {
19003 data.shift();
19004 series.updateParallelArrays(point, 'shift');
19005
19006 dataOptions.shift();
19007 }
19008 }
19009
19010 // redraw
19011 series.isDirty = true;
19012 series.isDirtyData = true;
19013
19014 if (redraw) {
19015
19016 chart.redraw(animation); // Animation is set anyway on redraw, #5665
19017 }
19018 },
19019
19020 /**
19021 * Remove a point (rendered or not), by index
19022 */
19023 removePoint: function(i, redraw, animation) {
19024
19025 var series = this,
19026 data = series.data,
19027 point = data[i],
19028 points = series.points,
19029 chart = series.chart,
19030 remove = function() {
19031
19032 if (points && points.length === data.length) { // #4935
19033 points.splice(i, 1);
19034 }
19035 data.splice(i, 1);
19036 series.options.data.splice(i, 1);
19037 series.updateParallelArrays(point || {
19038 series: series
19039 }, 'splice', i, 1);
19040
19041 if (point) {
19042 point.destroy();
19043 }
19044
19045 // redraw
19046 series.isDirty = true;
19047 series.isDirtyData = true;
19048 if (redraw) {
19049 chart.redraw();
19050 }
19051 };
19052
19053 setAnimation(animation, chart);
19054 redraw = pick(redraw, true);
19055
19056 // Fire the event with a default handler of removing the point
19057 if (point) {
19058 point.firePointEvent('remove', null, remove);
19059 } else {
19060 remove();
19061 }
19062 },
19063
19064 /**
19065 * Remove a series and optionally redraw the chart
19066 *
19067 * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call
19068 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation
19069 * configuration
19070 */
19071 remove: function(redraw, animation, withEvent) {
19072 var series = this,
19073 chart = series.chart;
19074
19075 function remove() {
19076
19077 // Destroy elements
19078 series.destroy();
19079
19080 // Redraw
19081 chart.isDirtyLegend = chart.isDirtyBox = true;
19082 chart.linkSeries();
19083
19084 if (pick(redraw, true)) {
19085 chart.redraw(animation);
19086 }
19087 }
19088
19089 // Fire the event with a default handler of removing the point
19090 if (withEvent !== false) {
19091 fireEvent(series, 'remove', null, remove);
19092 } else {
19093 remove();
19094 }
19095 },
19096
19097 /**
19098 * Series.update with a new set of options
19099 */
19100 update: function(newOptions, redraw) {
19101 var series = this,
19102 chart = this.chart,
19103 // must use user options when changing type because this.options is merged
19104 // in with type specific plotOptions
19105 oldOptions = this.userOptions,
19106 oldType = this.type,
19107 newType = newOptions.type || oldOptions.type || chart.options.chart.type,
19108 proto = seriesTypes[oldType].prototype,
19109 preserve = ['group', 'markerGroup', 'dataLabelsGroup'],
19110 n;
19111
19112 // If we're changing type or zIndex, create new groups (#3380, #3404)
19113 if ((newType && newType !== oldType) || newOptions.zIndex !== undefined) {
19114 preserve.length = 0;
19115 }
19116
19117 // Make sure groups are not destroyed (#3094)
19118 each(preserve, function(prop) {
19119 preserve[prop] = series[prop];
19120 delete series[prop];
19121 });
19122
19123 // Do the merge, with some forced options
19124 newOptions = merge(oldOptions, {
19125 animation: false,
19126 index: this.index,
19127 pointStart: this.xData[0] // when updating after addPoint
19128 }, {
19129 data: this.options.data
19130 }, newOptions);
19131
19132 // Destroy the series and delete all properties. Reinsert all methods
19133 // and properties from the new type prototype (#2270, #3719)
19134 this.remove(false, null, false);
19135 for (n in proto) {
19136 this[n] = undefined;
19137 }
19138 extend(this, seriesTypes[newType || oldType].prototype);
19139
19140 // Re-register groups (#3094)
19141 each(preserve, function(prop) {
19142 series[prop] = preserve[prop];
19143 });
19144
19145 this.init(chart, newOptions);
19146 chart.linkSeries(); // Links are lost in this.remove (#3028)
19147 if (pick(redraw, true)) {
19148 chart.redraw(false);
19149 }
19150 }
19151 });
19152
19153 // Extend the Axis.prototype for dynamic methods
19154 extend(Axis.prototype, /** @lends Axis.prototype */ {
19155
19156 /**
19157 * Axis.update with a new options structure
19158 */
19159 update: function(newOptions, redraw) {
19160 var chart = this.chart;
19161
19162 newOptions = chart.options[this.coll][this.options.index] = merge(this.userOptions, newOptions);
19163
19164 this.destroy(true);
19165
19166 this.init(chart, extend(newOptions, {
19167 events: undefined
19168 }));
19169
19170 chart.isDirtyBox = true;
19171 if (pick(redraw, true)) {
19172 chart.redraw();
19173 }
19174 },
19175
19176 /**
19177 * Remove the axis from the chart
19178 */
19179 remove: function(redraw) {
19180 var chart = this.chart,
19181 key = this.coll, // xAxis or yAxis
19182 axisSeries = this.series,
19183 i = axisSeries.length;
19184
19185 // Remove associated series (#2687)
19186 while (i--) {
19187 if (axisSeries[i]) {
19188 axisSeries[i].remove(false);
19189 }
19190 }
19191
19192 // Remove the axis
19193 erase(chart.axes, this);
19194 erase(chart[key], this);
19195 chart.options[key].splice(this.options.index, 1);
19196 each(chart[key], function(axis, i) { // Re-index, #1706
19197 axis.options.index = i;
19198 });
19199 this.destroy();
19200 chart.isDirtyBox = true;
19201
19202 if (pick(redraw, true)) {
19203 chart.redraw();
19204 }
19205 },
19206
19207 /**
19208 * Update the axis title by options
19209 */
19210 setTitle: function(newTitleOptions, redraw) {
19211 this.update({
19212 title: newTitleOptions
19213 }, redraw);
19214 },
19215
19216 /**
19217 * Set new axis categories and optionally redraw
19218 * @param {Array} categories
19219 * @param {Boolean} redraw
19220 */
19221 setCategories: function(categories, redraw) {
19222 this.update({
19223 categories: categories
19224 }, redraw);
19225 }
19226
19227 });
19228
19229 }(Highcharts));
19230 (function(H) {
19231 /**
19232 * (c) 2010-2016 Torstein Honsi
19233 *
19234 * License: www.highcharts.com/license
19235 */
19236 'use strict';
19237 var color = H.color,
19238 each = H.each,
19239 LegendSymbolMixin = H.LegendSymbolMixin,
19240 map = H.map,
19241 pick = H.pick,
19242 Series = H.Series,
19243 seriesType = H.seriesType;
19244
19245 /**
19246 * Area series type.
19247 * @constructor seriesTypes.area
19248 * @extends {Series}
19249 */
19250 seriesType('area', 'line', {
19251 softThreshold: false,
19252 threshold: 0
19253 // trackByArea: false,
19254 // lineColor: null, // overrides color, but lets fillColor be unaltered
19255 // fillOpacity: 0.75,
19256 // fillColor: null
19257 }, /** @lends seriesTypes.area.prototype */ {
19258 singleStacks: false,
19259 /**
19260 * Return an array of stacked points, where null and missing points are replaced by
19261 * dummy points in order for gaps to be drawn correctly in stacks.
19262 */
19263 getStackPoints: function() {
19264 var series = this,
19265 segment = [],
19266 keys = [],
19267 xAxis = this.xAxis,
19268 yAxis = this.yAxis,
19269 stack = yAxis.stacks[this.stackKey],
19270 pointMap = {},
19271 points = this.points,
19272 seriesIndex = series.index,
19273 yAxisSeries = yAxis.series,
19274 seriesLength = yAxisSeries.length,
19275 visibleSeries,
19276 upOrDown = pick(yAxis.options.reversedStacks, true) ? 1 : -1,
19277 i,
19278 x;
19279
19280 if (this.options.stacking) {
19281 // Create a map where we can quickly look up the points by their X value.
19282 for (i = 0; i < points.length; i++) {
19283 pointMap[points[i].x] = points[i];
19284 }
19285
19286 // Sort the keys (#1651)
19287 for (x in stack) {
19288 if (stack[x].total !== null) { // nulled after switching between grouping and not (#1651, #2336)
19289 keys.push(x);
19290 }
19291 }
19292 keys.sort(function(a, b) {
19293 return a - b;
19294 });
19295
19296 visibleSeries = map(yAxisSeries, function() {
19297 return this.visible;
19298 });
19299
19300 each(keys, function(x, idx) {
19301 var y = 0,
19302 stackPoint,
19303 stackedValues;
19304
19305 if (pointMap[x] && !pointMap[x].isNull) {
19306 segment.push(pointMap[x]);
19307
19308 // Find left and right cliff. -1 goes left, 1 goes right.
19309 each([-1, 1], function(direction) {
19310 var nullName = direction === 1 ? 'rightNull' : 'leftNull',
19311 cliffName = direction === 1 ? 'rightCliff' : 'leftCliff',
19312 cliff = 0,
19313 otherStack = stack[keys[idx + direction]];
19314
19315 // If there is a stack next to this one, to the left or to the right...
19316 if (otherStack) {
19317 i = seriesIndex;
19318 while (i >= 0 && i < seriesLength) { // Can go either up or down, depending on reversedStacks
19319 stackPoint = otherStack.points[i];
19320 if (!stackPoint) {
19321 // If the next point in this series is missing, mark the point
19322 // with point.leftNull or point.rightNull = true.
19323 if (i === seriesIndex) {
19324 pointMap[x][nullName] = true;
19325
19326 // If there are missing points in the next stack in any of the
19327 // series below this one, we need to substract the missing values
19328 // and add a hiatus to the left or right.
19329 } else if (visibleSeries[i]) {
19330 stackedValues = stack[x].points[i];
19331 if (stackedValues) {
19332 cliff -= stackedValues[1] - stackedValues[0];
19333 }
19334 }
19335 }
19336 // When reversedStacks is true, loop up, else loop down
19337 i += upOrDown;
19338 }
19339 }
19340 pointMap[x][cliffName] = cliff;
19341 });
19342
19343
19344 // There is no point for this X value in this series, so we
19345 // insert a dummy point in order for the areas to be drawn
19346 // correctly.
19347 } else {
19348
19349 // Loop down the stack to find the series below this one that has
19350 // a value (#1991)
19351 i = seriesIndex;
19352 while (i >= 0 && i < seriesLength) {
19353 stackPoint = stack[x].points[i];
19354 if (stackPoint) {
19355 y = stackPoint[1];
19356 break;
19357 }
19358 // When reversedStacks is true, loop up, else loop down
19359 i += upOrDown;
19360 }
19361
19362 y = yAxis.toPixels(y, true);
19363 segment.push({
19364 isNull: true,
19365 plotX: xAxis.toPixels(x, true),
19366 plotY: y,
19367 yBottom: y
19368 });
19369 }
19370 });
19371
19372 }
19373
19374 return segment;
19375 },
19376
19377 getGraphPath: function(points) {
19378 var getGraphPath = Series.prototype.getGraphPath,
19379 graphPath,
19380 options = this.options,
19381 stacking = options.stacking,
19382 yAxis = this.yAxis,
19383 topPath,
19384 //topPoints = [],
19385 bottomPath,
19386 bottomPoints = [],
19387 graphPoints = [],
19388 seriesIndex = this.index,
19389 i,
19390 areaPath,
19391 plotX,
19392 stacks = yAxis.stacks[this.stackKey],
19393 threshold = options.threshold,
19394 translatedThreshold = yAxis.getThreshold(options.threshold),
19395 isNull,
19396 yBottom,
19397 connectNulls = options.connectNulls || stacking === 'percent',
19398 /**
19399 * To display null points in underlying stacked series, this series graph must be
19400 * broken, and the area also fall down to fill the gap left by the null point. #2069
19401 */
19402 addDummyPoints = function(i, otherI, side) {
19403 var point = points[i],
19404 stackedValues = stacking && stacks[point.x].points[seriesIndex],
19405 nullVal = point[side + 'Null'] || 0,
19406 cliffVal = point[side + 'Cliff'] || 0,
19407 top,
19408 bottom,
19409 isNull = true;
19410
19411 if (cliffVal || nullVal) {
19412
19413 top = (nullVal ? stackedValues[0] : stackedValues[1]) + cliffVal;
19414 bottom = stackedValues[0] + cliffVal;
19415 isNull = !!nullVal;
19416
19417 } else if (!stacking && points[otherI] && points[otherI].isNull) {
19418 top = bottom = threshold;
19419 }
19420
19421 // Add to the top and bottom line of the area
19422 if (top !== undefined) {
19423 graphPoints.push({
19424 plotX: plotX,
19425 plotY: top === null ? translatedThreshold : yAxis.getThreshold(top),
19426 isNull: isNull
19427 });
19428 bottomPoints.push({
19429 plotX: plotX,
19430 plotY: bottom === null ? translatedThreshold : yAxis.getThreshold(bottom),
19431 doCurve: false // #1041, gaps in areaspline areas
19432 });
19433 }
19434 };
19435
19436 // Find what points to use
19437 points = points || this.points;
19438
19439 // Fill in missing points
19440 if (stacking) {
19441 points = this.getStackPoints();
19442 }
19443
19444 for (i = 0; i < points.length; i++) {
19445 isNull = points[i].isNull;
19446 plotX = pick(points[i].rectPlotX, points[i].plotX);
19447 yBottom = pick(points[i].yBottom, translatedThreshold);
19448
19449 if (!isNull || connectNulls) {
19450
19451 if (!connectNulls) {
19452 addDummyPoints(i, i - 1, 'left');
19453 }
19454
19455 if (!(isNull && !stacking && connectNulls)) { // Skip null point when stacking is false and connectNulls true
19456 graphPoints.push(points[i]);
19457 bottomPoints.push({
19458 x: i,
19459 plotX: plotX,
19460 plotY: yBottom
19461 });
19462 }
19463
19464 if (!connectNulls) {
19465 addDummyPoints(i, i + 1, 'right');
19466 }
19467 }
19468 }
19469
19470 topPath = getGraphPath.call(this, graphPoints, true, true);
19471
19472 bottomPoints.reversed = true;
19473 bottomPath = getGraphPath.call(this, bottomPoints, true, true);
19474 if (bottomPath.length) {
19475 bottomPath[0] = 'L';
19476 }
19477
19478 areaPath = topPath.concat(bottomPath);
19479 graphPath = getGraphPath.call(this, graphPoints, false, connectNulls); // TODO: don't set leftCliff and rightCliff when connectNulls?
19480
19481 areaPath.xMap = topPath.xMap;
19482 this.areaPath = areaPath;
19483 return graphPath;
19484 },
19485
19486 /**
19487 * Draw the graph and the underlying area. This method calls the Series base
19488 * function and adds the area. The areaPath is calculated in the getSegmentPath
19489 * method called from Series.prototype.drawGraph.
19490 */
19491 drawGraph: function() {
19492
19493 // Define or reset areaPath
19494 this.areaPath = [];
19495
19496 // Call the base method
19497 Series.prototype.drawGraph.apply(this);
19498
19499 // Define local variables
19500 var series = this,
19501 areaPath = this.areaPath,
19502 options = this.options,
19503 zones = this.zones,
19504 props = [
19505 [
19506 'area',
19507 'highcharts-area',
19508
19509 this.color,
19510 options.fillColor
19511
19512 ]
19513 ]; // area name, main color, fill color
19514
19515 each(zones, function(zone, i) {
19516 props.push([
19517 'zone-area-' + i,
19518 'highcharts-area highcharts-zone-area-' + i + ' ' + zone.className,
19519
19520 zone.color || series.color,
19521 zone.fillColor || options.fillColor
19522
19523 ]);
19524 });
19525
19526 each(props, function(prop) {
19527 var areaKey = prop[0],
19528 area = series[areaKey];
19529
19530 // Create or update the area
19531 if (area) { // update
19532 area.endX = areaPath.xMap;
19533 area.animate({
19534 d: areaPath
19535 });
19536
19537 } else { // create
19538 area = series[areaKey] = series.chart.renderer.path(areaPath)
19539 .addClass(prop[1])
19540 .attr({
19541
19542 fill: pick(
19543 prop[3],
19544 color(prop[2]).setOpacity(pick(options.fillOpacity, 0.75)).get()
19545 ),
19546
19547 zIndex: 0 // #1069
19548 }).add(series.group);
19549 area.isArea = true;
19550 }
19551 area.startX = areaPath.xMap;
19552 area.shiftUnit = options.step ? 2 : 1;
19553 });
19554 },
19555
19556 drawLegendSymbol: LegendSymbolMixin.drawRectangle
19557 });
19558
19559 }(Highcharts));
19560 (function(H) {
19561 /**
19562 * (c) 2010-2016 Torstein Honsi
19563 *
19564 * License: www.highcharts.com/license
19565 */
19566 'use strict';
19567 var pick = H.pick,
19568 seriesType = H.seriesType;
19569
19570 /**
19571 * Spline series type.
19572 * @constructor seriesTypes.spline
19573 * @extends {Series}
19574 */
19575 seriesType('spline', 'line', {}, /** @lends seriesTypes.spline.prototype */ {
19576 /**
19577 * Get the spline segment from a given point's previous neighbour to the given point
19578 */
19579 getPointSpline: function(points, point, i) {
19580 var smoothing = 1.5, // 1 means control points midway between points, 2 means 1/3 from the point, 3 is 1/4 etc
19581 denom = smoothing + 1,
19582 plotX = point.plotX,
19583 plotY = point.plotY,
19584 lastPoint = points[i - 1],
19585 nextPoint = points[i + 1],
19586 leftContX,
19587 leftContY,
19588 rightContX,
19589 rightContY,
19590 ret;
19591
19592 function doCurve(otherPoint) {
19593 return otherPoint && !otherPoint.isNull && otherPoint.doCurve !== false;
19594 }
19595
19596 // Find control points
19597 if (doCurve(lastPoint) && doCurve(nextPoint)) {
19598 var lastX = lastPoint.plotX,
19599 lastY = lastPoint.plotY,
19600 nextX = nextPoint.plotX,
19601 nextY = nextPoint.plotY,
19602 correction = 0;
19603
19604 leftContX = (smoothing * plotX + lastX) / denom;
19605 leftContY = (smoothing * plotY + lastY) / denom;
19606 rightContX = (smoothing * plotX + nextX) / denom;
19607 rightContY = (smoothing * plotY + nextY) / denom;
19608
19609 // Have the two control points make a straight line through main point
19610 if (rightContX !== leftContX) { // #5016, division by zero
19611 correction = ((rightContY - leftContY) * (rightContX - plotX)) /
19612 (rightContX - leftContX) + plotY - rightContY;
19613 }
19614
19615 leftContY += correction;
19616 rightContY += correction;
19617
19618 // to prevent false extremes, check that control points are between
19619 // neighbouring points' y values
19620 if (leftContY > lastY && leftContY > plotY) {
19621 leftContY = Math.max(lastY, plotY);
19622 rightContY = 2 * plotY - leftContY; // mirror of left control point
19623 } else if (leftContY < lastY && leftContY < plotY) {
19624 leftContY = Math.min(lastY, plotY);
19625 rightContY = 2 * plotY - leftContY;
19626 }
19627 if (rightContY > nextY && rightContY > plotY) {
19628 rightContY = Math.max(nextY, plotY);
19629 leftContY = 2 * plotY - rightContY;
19630 } else if (rightContY < nextY && rightContY < plotY) {
19631 rightContY = Math.min(nextY, plotY);
19632 leftContY = 2 * plotY - rightContY;
19633 }
19634
19635 // record for drawing in next point
19636 point.rightContX = rightContX;
19637 point.rightContY = rightContY;
19638
19639
19640 }
19641
19642 // Visualize control points for debugging
19643 /*
19644 if (leftContX) {
19645 this.chart.renderer.circle(leftContX + this.chart.plotLeft, leftContY + this.chart.plotTop, 2)
19646 .attr({
19647 stroke: 'red',
19648 'stroke-width': 2,
19649 fill: 'none',
19650 zIndex: 9
19651 })
19652 .add();
19653 this.chart.renderer.path(['M', leftContX + this.chart.plotLeft, leftContY + this.chart.plotTop,
19654 'L', plotX + this.chart.plotLeft, plotY + this.chart.plotTop])
19655 .attr({
19656 stroke: 'red',
19657 'stroke-width': 2,
19658 zIndex: 9
19659 })
19660 .add();
19661 }
19662 if (rightContX) {
19663 this.chart.renderer.circle(rightContX + this.chart.plotLeft, rightContY + this.chart.plotTop, 2)
19664 .attr({
19665 stroke: 'green',
19666 'stroke-width': 2,
19667 fill: 'none',
19668 zIndex: 9
19669 })
19670 .add();
19671 this.chart.renderer.path(['M', rightContX + this.chart.plotLeft, rightContY + this.chart.plotTop,
19672 'L', plotX + this.chart.plotLeft, plotY + this.chart.plotTop])
19673 .attr({
19674 stroke: 'green',
19675 'stroke-width': 2,
19676 zIndex: 9
19677 })
19678 .add();
19679 }
19680 // */
19681 ret = [
19682 'C',
19683 pick(lastPoint.rightContX, lastPoint.plotX),
19684 pick(lastPoint.rightContY, lastPoint.plotY),
19685 pick(leftContX, plotX),
19686 pick(leftContY, plotY),
19687 plotX,
19688 plotY
19689 ];
19690 lastPoint.rightContX = lastPoint.rightContY = null; // reset for updating series later
19691 return ret;
19692 }
19693 });
19694
19695 }(Highcharts));
19696 (function(H) {
19697 /**
19698 * (c) 2010-2016 Torstein Honsi
19699 *
19700 * License: www.highcharts.com/license
19701 */
19702 'use strict';
19703 var areaProto = H.seriesTypes.area.prototype,
19704 defaultPlotOptions = H.defaultPlotOptions,
19705 LegendSymbolMixin = H.LegendSymbolMixin,
19706 seriesType = H.seriesType;
19707 /**
19708 * AreaSplineSeries object
19709 */
19710 seriesType('areaspline', 'spline', defaultPlotOptions.area, {
19711 getStackPoints: areaProto.getStackPoints,
19712 getGraphPath: areaProto.getGraphPath,
19713 setStackCliffs: areaProto.setStackCliffs,
19714 drawGraph: areaProto.drawGraph,
19715 drawLegendSymbol: LegendSymbolMixin.drawRectangle
19716 });
19717
19718 }(Highcharts));
19719 (function(H) {
19720 /**
19721 * (c) 2010-2016 Torstein Honsi
19722 *
19723 * License: www.highcharts.com/license
19724 */
19725 'use strict';
19726 var animObject = H.animObject,
19727 color = H.color,
19728 each = H.each,
19729 extend = H.extend,
19730 isNumber = H.isNumber,
19731 LegendSymbolMixin = H.LegendSymbolMixin,
19732 merge = H.merge,
19733 noop = H.noop,
19734 pick = H.pick,
19735 Series = H.Series,
19736 seriesType = H.seriesType,
19737 svg = H.svg;
19738 /**
19739 * The column series type.
19740 *
19741 * @constructor seriesTypes.column
19742 * @augments Series
19743 */
19744 seriesType('column', 'line', {
19745 borderRadius: 0,
19746 //colorByPoint: undefined,
19747 groupPadding: 0.2,
19748 //grouping: true,
19749 marker: null, // point options are specified in the base options
19750 pointPadding: 0.1,
19751 //pointWidth: null,
19752 minPointLength: 0,
19753 cropThreshold: 50, // when there are more points, they will not animate out of the chart on xAxis.setExtremes
19754 pointRange: null, // null means auto, meaning 1 in a categorized axis and least distance between points if not categories
19755 states: {
19756 hover: {
19757 halo: false,
19758
19759 brightness: 0.1,
19760 shadow: false
19761
19762 },
19763
19764 select: {
19765 color: '#cccccc',
19766 borderColor: '#000000',
19767 shadow: false
19768 }
19769
19770 },
19771 dataLabels: {
19772 align: null, // auto
19773 verticalAlign: null, // auto
19774 y: null
19775 },
19776 softThreshold: false,
19777 startFromThreshold: true, // false doesn't work well: http://jsfiddle.net/highcharts/hz8fopan/14/
19778 stickyTracking: false,
19779 tooltip: {
19780 distance: 6
19781 },
19782 threshold: 0,
19783
19784 borderColor: '#ffffff'
19785 // borderWidth: 1
19786
19787
19788 }, /** @lends seriesTypes.column.prototype */ {
19789 cropShoulder: 0,
19790 directTouch: true, // When tooltip is not shared, this series (and derivatives) requires direct touch/hover. KD-tree does not apply.
19791 trackerGroups: ['group', 'dataLabelsGroup'],
19792 negStacks: true, // use separate negative stacks, unlike area stacks where a negative
19793 // point is substracted from previous (#1910)
19794
19795 /**
19796 * Initialize the series. Extends the basic Series.init method by
19797 * marking other series of the same type as dirty.
19798 *
19799 * @function #init
19800 * @memberOf seriesTypes.column
19801 * @returns {void}
19802 */
19803 init: function() {
19804 Series.prototype.init.apply(this, arguments);
19805
19806 var series = this,
19807 chart = series.chart;
19808
19809 // if the series is added dynamically, force redraw of other
19810 // series affected by a new column
19811 if (chart.hasRendered) {
19812 each(chart.series, function(otherSeries) {
19813 if (otherSeries.type === series.type) {
19814 otherSeries.isDirty = true;
19815 }
19816 });
19817 }
19818 },
19819
19820 /**
19821 * Return the width and x offset of the columns adjusted for grouping, groupPadding, pointPadding,
19822 * pointWidth etc.
19823 */
19824 getColumnMetrics: function() {
19825
19826 var series = this,
19827 options = series.options,
19828 xAxis = series.xAxis,
19829 yAxis = series.yAxis,
19830 reversedXAxis = xAxis.reversed,
19831 stackKey,
19832 stackGroups = {},
19833 columnCount = 0;
19834
19835 // Get the total number of column type series.
19836 // This is called on every series. Consider moving this logic to a
19837 // chart.orderStacks() function and call it on init, addSeries and removeSeries
19838 if (options.grouping === false) {
19839 columnCount = 1;
19840 } else {
19841 each(series.chart.series, function(otherSeries) {
19842 var otherOptions = otherSeries.options,
19843 otherYAxis = otherSeries.yAxis,
19844 columnIndex;
19845 if (otherSeries.type === series.type && otherSeries.visible &&
19846 yAxis.len === otherYAxis.len && yAxis.pos === otherYAxis.pos) { // #642, #2086
19847 if (otherOptions.stacking) {
19848 stackKey = otherSeries.stackKey;
19849 if (stackGroups[stackKey] === undefined) {
19850 stackGroups[stackKey] = columnCount++;
19851 }
19852 columnIndex = stackGroups[stackKey];
19853 } else if (otherOptions.grouping !== false) { // #1162
19854 columnIndex = columnCount++;
19855 }
19856 otherSeries.columnIndex = columnIndex;
19857 }
19858 });
19859 }
19860
19861 var categoryWidth = Math.min(
19862 Math.abs(xAxis.transA) * (xAxis.ordinalSlope || options.pointRange || xAxis.closestPointRange || xAxis.tickInterval || 1), // #2610
19863 xAxis.len // #1535
19864 ),
19865 groupPadding = categoryWidth * options.groupPadding,
19866 groupWidth = categoryWidth - 2 * groupPadding,
19867 pointOffsetWidth = groupWidth / columnCount,
19868 pointWidth = Math.min(
19869 options.maxPointWidth || xAxis.len,
19870 pick(options.pointWidth, pointOffsetWidth * (1 - 2 * options.pointPadding))
19871 ),
19872 pointPadding = (pointOffsetWidth - pointWidth) / 2,
19873 colIndex = (series.columnIndex || 0) + (reversedXAxis ? 1 : 0), // #1251, #3737
19874 pointXOffset = pointPadding + (groupPadding + colIndex *
19875 pointOffsetWidth - (categoryWidth / 2)) *
19876 (reversedXAxis ? -1 : 1);
19877
19878 // Save it for reading in linked series (Error bars particularly)
19879 series.columnMetrics = {
19880 width: pointWidth,
19881 offset: pointXOffset
19882 };
19883 return series.columnMetrics;
19884
19885 },
19886
19887 /**
19888 * Make the columns crisp. The edges are rounded to the nearest full pixel.
19889 */
19890 crispCol: function(x, y, w, h) {
19891 var chart = this.chart,
19892 borderWidth = this.borderWidth,
19893 xCrisp = -(borderWidth % 2 ? 0.5 : 0),
19894 yCrisp = borderWidth % 2 ? 0.5 : 1,
19895 right,
19896 bottom,
19897 fromTop;
19898
19899 if (chart.inverted && chart.renderer.isVML) {
19900 yCrisp += 1;
19901 }
19902
19903 // Horizontal. We need to first compute the exact right edge, then round it
19904 // and compute the width from there.
19905 right = Math.round(x + w) + xCrisp;
19906 x = Math.round(x) + xCrisp;
19907 w = right - x;
19908
19909 // Vertical
19910 bottom = Math.round(y + h) + yCrisp;
19911 fromTop = Math.abs(y) <= 0.5 && bottom > 0.5; // #4504, #4656
19912 y = Math.round(y) + yCrisp;
19913 h = bottom - y;
19914
19915 // Top edges are exceptions
19916 if (fromTop && h) { // #5146
19917 y -= 1;
19918 h += 1;
19919 }
19920
19921 return {
19922 x: x,
19923 y: y,
19924 width: w,
19925 height: h
19926 };
19927 },
19928
19929 /**
19930 * Translate each point to the plot area coordinate system and find shape positions
19931 */
19932 translate: function() {
19933 var series = this,
19934 chart = series.chart,
19935 options = series.options,
19936 dense = series.dense = series.closestPointRange * series.xAxis.transA < 2,
19937 borderWidth = series.borderWidth = pick(
19938 options.borderWidth,
19939 dense ? 0 : 1 // #3635
19940 ),
19941 yAxis = series.yAxis,
19942 threshold = options.threshold,
19943 translatedThreshold = series.translatedThreshold = yAxis.getThreshold(threshold),
19944 minPointLength = pick(options.minPointLength, 5),
19945 metrics = series.getColumnMetrics(),
19946 pointWidth = metrics.width,
19947 seriesBarW = series.barW = Math.max(pointWidth, 1 + 2 * borderWidth), // postprocessed for border width
19948 pointXOffset = series.pointXOffset = metrics.offset;
19949
19950 if (chart.inverted) {
19951 translatedThreshold -= 0.5; // #3355
19952 }
19953
19954 // When the pointPadding is 0, we want the columns to be packed tightly, so we allow individual
19955 // columns to have individual sizes. When pointPadding is greater, we strive for equal-width
19956 // columns (#2694).
19957 if (options.pointPadding) {
19958 seriesBarW = Math.ceil(seriesBarW);
19959 }
19960
19961 Series.prototype.translate.apply(series);
19962
19963 // Record the new values
19964 each(series.points, function(point) {
19965 var yBottom = pick(point.yBottom, translatedThreshold),
19966 safeDistance = 999 + Math.abs(yBottom),
19967 plotY = Math.min(Math.max(-safeDistance, point.plotY), yAxis.len + safeDistance), // Don't draw too far outside plot area (#1303, #2241, #4264)
19968 barX = point.plotX + pointXOffset,
19969 barW = seriesBarW,
19970 barY = Math.min(plotY, yBottom),
19971 up,
19972 barH = Math.max(plotY, yBottom) - barY;
19973
19974 // Handle options.minPointLength
19975 if (Math.abs(barH) < minPointLength) {
19976 if (minPointLength) {
19977 barH = minPointLength;
19978 up = (!yAxis.reversed && !point.negative) || (yAxis.reversed && point.negative);
19979 barY = Math.abs(barY - translatedThreshold) > minPointLength ? // stacked
19980 yBottom - minPointLength : // keep position
19981 translatedThreshold - (up ? minPointLength : 0); // #1485, #4051
19982 }
19983 }
19984
19985 // Cache for access in polar
19986 point.barX = barX;
19987 point.pointWidth = pointWidth;
19988
19989 // Fix the tooltip on center of grouped columns (#1216, #424, #3648)
19990 point.tooltipPos = chart.inverted ? [yAxis.len + yAxis.pos - chart.plotLeft - plotY, series.xAxis.len - barX - barW / 2, barH] : [barX + barW / 2, plotY + yAxis.pos - chart.plotTop, barH];
19991
19992 // Register shape type and arguments to be used in drawPoints
19993 point.shapeType = 'rect';
19994 point.shapeArgs = series.crispCol.apply(
19995 series,
19996 point.isNull ? [point.plotX, yAxis.len / 2, 0, 0] : // #3169, drilldown from null must have a position to work from
19997 [barX, barY, barW, barH]
19998 );
19999 });
20000
20001 },
20002
20003 getSymbol: noop,
20004
20005 /**
20006 * Use a solid rectangle like the area series types
20007 */
20008 drawLegendSymbol: LegendSymbolMixin.drawRectangle,
20009
20010
20011 /**
20012 * Columns have no graph
20013 */
20014 drawGraph: function() {
20015 this.group[this.dense ? 'addClass' : 'removeClass']('highcharts-dense-data');
20016 },
20017
20018
20019 /**
20020 * Get presentational attributes
20021 */
20022 pointAttribs: function(point, state) {
20023 var options = this.options,
20024 stateOptions,
20025 ret,
20026 p2o = this.pointAttrToOptions || {},
20027 strokeOption = p2o.stroke || 'borderColor',
20028 strokeWidthOption = p2o['stroke-width'] || 'borderWidth',
20029 fill = (point && point.color) || this.color,
20030 stroke = point[strokeOption] || options[strokeOption] ||
20031 this.color || fill, // set to fill when borderColor null
20032 dashstyle = options.dashStyle,
20033 zone,
20034 brightness;
20035
20036 // Handle zone colors
20037 if (point && this.zones.length) {
20038 zone = point.getZone();
20039 fill = (zone && zone.color) || point.options.color || this.color; // When zones are present, don't use point.color (#4267)
20040 }
20041
20042 // Select or hover states
20043 if (state) {
20044 stateOptions = options.states[state];
20045 brightness = stateOptions.brightness;
20046 fill = stateOptions.color ||
20047 (brightness !== undefined && color(fill).brighten(stateOptions.brightness).get()) ||
20048 fill;
20049 stroke = stateOptions[strokeOption] || stroke;
20050 dashstyle = stateOptions.dashStyle || dashstyle;
20051 }
20052
20053 ret = {
20054 'fill': fill,
20055 'stroke': stroke,
20056 'stroke-width': point[strokeWidthOption] || options[strokeWidthOption] || this[strokeWidthOption] || 0
20057 };
20058 if (options.borderRadius) {
20059 ret.r = options.borderRadius;
20060 }
20061
20062 if (dashstyle) {
20063 ret.dashstyle = dashstyle;
20064 }
20065
20066 return ret;
20067 },
20068
20069
20070 /**
20071 * Draw the columns. For bars, the series.group is rotated, so the same coordinates
20072 * apply for columns and bars. This method is inherited by scatter series.
20073 *
20074 */
20075 drawPoints: function() {
20076 var series = this,
20077 chart = this.chart,
20078 options = series.options,
20079 renderer = chart.renderer,
20080 animationLimit = options.animationLimit || 250,
20081 shapeArgs;
20082
20083 // draw the columns
20084 each(series.points, function(point) {
20085 var plotY = point.plotY,
20086 graphic = point.graphic;
20087
20088 if (isNumber(plotY) && point.y !== null) {
20089 shapeArgs = point.shapeArgs;
20090
20091 if (graphic) { // update
20092 graphic[chart.pointCount < animationLimit ? 'animate' : 'attr'](
20093 merge(shapeArgs)
20094 );
20095
20096 } else {
20097 point.graphic = graphic = renderer[point.shapeType](shapeArgs)
20098 .attr({
20099 'class': point.getClassName()
20100 })
20101 .add(point.group || series.group);
20102 }
20103
20104
20105 // Presentational
20106 graphic
20107 .attr(series.pointAttribs(point, point.selected && 'select'))
20108 .shadow(options.shadow, null, options.stacking && !options.borderRadius);
20109
20110
20111 } else if (graphic) {
20112 point.graphic = graphic.destroy(); // #1269
20113 }
20114 });
20115 },
20116
20117 /**
20118 * Animate the column heights one by one from zero
20119 * @param {Boolean} init Whether to initialize the animation or run it
20120 */
20121 animate: function(init) {
20122 var series = this,
20123 yAxis = this.yAxis,
20124 options = series.options,
20125 inverted = this.chart.inverted,
20126 attr = {},
20127 translatedThreshold;
20128
20129 if (svg) { // VML is too slow anyway
20130 if (init) {
20131 attr.scaleY = 0.001;
20132 translatedThreshold = Math.min(yAxis.pos + yAxis.len, Math.max(yAxis.pos, yAxis.toPixels(options.threshold)));
20133 if (inverted) {
20134 attr.translateX = translatedThreshold - yAxis.len;
20135 } else {
20136 attr.translateY = translatedThreshold;
20137 }
20138 series.group.attr(attr);
20139
20140 } else { // run the animation
20141
20142 attr[inverted ? 'translateX' : 'translateY'] = yAxis.pos;
20143 series.group.animate(attr, extend(animObject(series.options.animation), {
20144 // Do the scale synchronously to ensure smooth updating (#5030)
20145 step: function(val, fx) {
20146 series.group.attr({
20147 scaleY: Math.max(0.001, fx.pos) // #5250
20148 });
20149 }
20150 }));
20151
20152 // delete this function to allow it only once
20153 series.animate = null;
20154 }
20155 }
20156 },
20157
20158 /**
20159 * Remove this series from the chart
20160 */
20161 remove: function() {
20162 var series = this,
20163 chart = series.chart;
20164
20165 // column and bar series affects other series of the same type
20166 // as they are either stacked or grouped
20167 if (chart.hasRendered) {
20168 each(chart.series, function(otherSeries) {
20169 if (otherSeries.type === series.type) {
20170 otherSeries.isDirty = true;
20171 }
20172 });
20173 }
20174
20175 Series.prototype.remove.apply(series, arguments);
20176 }
20177 });
20178
20179 }(Highcharts));
20180 (function(H) {
20181 /**
20182 * (c) 2010-2016 Torstein Honsi
20183 *
20184 * License: www.highcharts.com/license
20185 */
20186 'use strict';
20187
20188 var seriesType = H.seriesType;
20189
20190 /**
20191 * The Bar series class
20192 */
20193 seriesType('bar', 'column', null, {
20194 inverted: true
20195 });
20196
20197 }(Highcharts));
20198 (function(H) {
20199 /**
20200 * (c) 2010-2016 Torstein Honsi
20201 *
20202 * License: www.highcharts.com/license
20203 */
20204 'use strict';
20205 var Series = H.Series,
20206 seriesType = H.seriesType;
20207 /**
20208 * The scatter series type
20209 */
20210 seriesType('scatter', 'line', {
20211 lineWidth: 0,
20212 marker: {
20213 enabled: true // Overrides auto-enabling in line series (#3647)
20214 },
20215 tooltip: {
20216 headerFormat: '<span style="color:{point.color}">\u25CF</span> <span style="font-size: 0.85em"> {series.name}</span><br/>',
20217 pointFormat: 'x: <b>{point.x}</b><br/>y: <b>{point.y}</b><br/>'
20218 }
20219
20220 // Prototype members
20221 }, {
20222 sorted: false,
20223 requireSorting: false,
20224 noSharedTooltip: true,
20225 trackerGroups: ['group', 'markerGroup', 'dataLabelsGroup'],
20226 takeOrdinalPosition: false, // #2342
20227 kdDimensions: 2,
20228 drawGraph: function() {
20229 if (this.options.lineWidth) {
20230 Series.prototype.drawGraph.call(this);
20231 }
20232 }
20233 });
20234
20235 }(Highcharts));
20236 (function(H) {
20237 /**
20238 * (c) 2010-2016 Torstein Honsi
20239 *
20240 * License: www.highcharts.com/license
20241 */
20242 'use strict';
20243 var pick = H.pick,
20244 relativeLength = H.relativeLength;
20245
20246 H.CenteredSeriesMixin = {
20247 /**
20248 * Get the center of the pie based on the size and center options relative to the
20249 * plot area. Borrowed by the polar and gauge series types.
20250 */
20251 getCenter: function() {
20252
20253 var options = this.options,
20254 chart = this.chart,
20255 slicingRoom = 2 * (options.slicedOffset || 0),
20256 handleSlicingRoom,
20257 plotWidth = chart.plotWidth - 2 * slicingRoom,
20258 plotHeight = chart.plotHeight - 2 * slicingRoom,
20259 centerOption = options.center,
20260 positions = [pick(centerOption[0], '50%'), pick(centerOption[1], '50%'), options.size || '100%', options.innerSize || 0],
20261 smallestSize = Math.min(plotWidth, plotHeight),
20262 i,
20263 value;
20264
20265 for (i = 0; i < 4; ++i) {
20266 value = positions[i];
20267 handleSlicingRoom = i < 2 || (i === 2 && /%$/.test(value));
20268
20269 // i == 0: centerX, relative to width
20270 // i == 1: centerY, relative to height
20271 // i == 2: size, relative to smallestSize
20272 // i == 3: innerSize, relative to size
20273 positions[i] = relativeLength(value, [plotWidth, plotHeight, smallestSize, positions[2]][i]) +
20274 (handleSlicingRoom ? slicingRoom : 0);
20275
20276 }
20277 // innerSize cannot be larger than size (#3632)
20278 if (positions[3] > positions[2]) {
20279 positions[3] = positions[2];
20280 }
20281 return positions;
20282 }
20283 };
20284
20285 }(Highcharts));
20286 (function(H) {
20287 /**
20288 * (c) 2010-2016 Torstein Honsi
20289 *
20290 * License: www.highcharts.com/license
20291 */
20292 'use strict';
20293 var addEvent = H.addEvent,
20294 CenteredSeriesMixin = H.CenteredSeriesMixin,
20295 defined = H.defined,
20296 each = H.each,
20297 extend = H.extend,
20298 inArray = H.inArray,
20299 LegendSymbolMixin = H.LegendSymbolMixin,
20300 noop = H.noop,
20301 pick = H.pick,
20302 Point = H.Point,
20303 Series = H.Series,
20304 seriesType = H.seriesType,
20305 seriesTypes = H.seriesTypes,
20306 setAnimation = H.setAnimation;
20307
20308 /**
20309 * The pie series type.
20310 *
20311 * @constructor seriesTypes.pie
20312 * @augments Series
20313 */
20314 seriesType('pie', 'line', {
20315 center: [null, null],
20316 clip: false,
20317 colorByPoint: true, // always true for pies
20318 dataLabels: {
20319 // align: null,
20320 // connectorWidth: 1,
20321 // connectorColor: point.color,
20322 // connectorPadding: 5,
20323 distance: 30,
20324 enabled: true,
20325 formatter: function() { // #2945
20326 return this.y === null ? undefined : this.point.name;
20327 },
20328 // softConnector: true,
20329 x: 0
20330 // y: 0
20331 },
20332 ignoreHiddenPoint: true,
20333 //innerSize: 0,
20334 legendType: 'point',
20335 marker: null, // point options are specified in the base options
20336 size: null,
20337 showInLegend: false,
20338 slicedOffset: 10,
20339 stickyTracking: false,
20340 tooltip: {
20341 followPointer: true
20342 },
20343
20344 borderColor: '#ffffff',
20345 borderWidth: 1,
20346 states: {
20347 hover: {
20348 brightness: 0.1,
20349 shadow: false
20350 }
20351 }
20352
20353
20354 }, /** @lends seriesTypes.pie.prototype */ {
20355 isCartesian: false,
20356 requireSorting: false,
20357 directTouch: true,
20358 noSharedTooltip: true,
20359 trackerGroups: ['group', 'dataLabelsGroup'],
20360 axisTypes: [],
20361 pointAttribs: seriesTypes.column.prototype.pointAttribs,
20362 /**
20363 * Animate the pies in
20364 */
20365 animate: function(init) {
20366 var series = this,
20367 points = series.points,
20368 startAngleRad = series.startAngleRad;
20369
20370 if (!init) {
20371 each(points, function(point) {
20372 var graphic = point.graphic,
20373 args = point.shapeArgs;
20374
20375 if (graphic) {
20376 // start values
20377 graphic.attr({
20378 r: point.startR || (series.center[3] / 2), // animate from inner radius (#779)
20379 start: startAngleRad,
20380 end: startAngleRad
20381 });
20382
20383 // animate
20384 graphic.animate({
20385 r: args.r,
20386 start: args.start,
20387 end: args.end
20388 }, series.options.animation);
20389 }
20390 });
20391
20392 // delete this function to allow it only once
20393 series.animate = null;
20394 }
20395 },
20396
20397 /**
20398 * Recompute total chart sum and update percentages of points.
20399 */
20400 updateTotals: function() {
20401 var i,
20402 total = 0,
20403 points = this.points,
20404 len = points.length,
20405 point,
20406 ignoreHiddenPoint = this.options.ignoreHiddenPoint;
20407
20408 // Get the total sum
20409 for (i = 0; i < len; i++) {
20410 point = points[i];
20411 // Disallow negative values (#1530, #3623, #5322)
20412 if (point.y < 0) {
20413 point.y = null;
20414 }
20415 total += (ignoreHiddenPoint && !point.visible) ? 0 : point.y;
20416 }
20417 this.total = total;
20418
20419 // Set each point's properties
20420 for (i = 0; i < len; i++) {
20421 point = points[i];
20422 point.percentage = (total > 0 && (point.visible || !ignoreHiddenPoint)) ? point.y / total * 100 : 0;
20423 point.total = total;
20424 }
20425 },
20426
20427 /**
20428 * Extend the generatePoints method by adding total and percentage properties to each point
20429 */
20430 generatePoints: function() {
20431 Series.prototype.generatePoints.call(this);
20432 this.updateTotals();
20433 },
20434
20435 /**
20436 * Do translation for pie slices
20437 */
20438 translate: function(positions) {
20439 this.generatePoints();
20440
20441 var series = this,
20442 cumulative = 0,
20443 precision = 1000, // issue #172
20444 options = series.options,
20445 slicedOffset = options.slicedOffset,
20446 connectorOffset = slicedOffset + (options.borderWidth || 0),
20447 start,
20448 end,
20449 angle,
20450 startAngle = options.startAngle || 0,
20451 startAngleRad = series.startAngleRad = Math.PI / 180 * (startAngle - 90),
20452 endAngleRad = series.endAngleRad = Math.PI / 180 * ((pick(options.endAngle, startAngle + 360)) - 90),
20453 circ = endAngleRad - startAngleRad, //2 * Math.PI,
20454 points = series.points,
20455 radiusX, // the x component of the radius vector for a given point
20456 radiusY,
20457 labelDistance = options.dataLabels.distance,
20458 ignoreHiddenPoint = options.ignoreHiddenPoint,
20459 i,
20460 len = points.length,
20461 point;
20462
20463 // Get positions - either an integer or a percentage string must be given.
20464 // If positions are passed as a parameter, we're in a recursive loop for adjusting
20465 // space for data labels.
20466 if (!positions) {
20467 series.center = positions = series.getCenter();
20468 }
20469
20470 // utility for getting the x value from a given y, used for anticollision logic in data labels
20471 series.getX = function(y, left) {
20472
20473 angle = Math.asin(Math.min((y - positions[1]) / (positions[2] / 2 + labelDistance), 1));
20474
20475 return positions[0] +
20476 (left ? -1 : 1) *
20477 (Math.cos(angle) * (positions[2] / 2 + labelDistance));
20478 };
20479
20480 // Calculate the geometry for each point
20481 for (i = 0; i < len; i++) {
20482
20483 point = points[i];
20484
20485 // set start and end angle
20486 start = startAngleRad + (cumulative * circ);
20487 if (!ignoreHiddenPoint || point.visible) {
20488 cumulative += point.percentage / 100;
20489 }
20490 end = startAngleRad + (cumulative * circ);
20491
20492 // set the shape
20493 point.shapeType = 'arc';
20494 point.shapeArgs = {
20495 x: positions[0],
20496 y: positions[1],
20497 r: positions[2] / 2,
20498 innerR: positions[3] / 2,
20499 start: Math.round(start * precision) / precision,
20500 end: Math.round(end * precision) / precision
20501 };
20502
20503 // The angle must stay within -90 and 270 (#2645)
20504 angle = (end + start) / 2;
20505 if (angle > 1.5 * Math.PI) {
20506 angle -= 2 * Math.PI;
20507 } else if (angle < -Math.PI / 2) {
20508 angle += 2 * Math.PI;
20509 }
20510
20511 // Center for the sliced out slice
20512 point.slicedTranslation = {
20513 translateX: Math.round(Math.cos(angle) * slicedOffset),
20514 translateY: Math.round(Math.sin(angle) * slicedOffset)
20515 };
20516
20517 // set the anchor point for tooltips
20518 radiusX = Math.cos(angle) * positions[2] / 2;
20519 radiusY = Math.sin(angle) * positions[2] / 2;
20520 point.tooltipPos = [
20521 positions[0] + radiusX * 0.7,
20522 positions[1] + radiusY * 0.7
20523 ];
20524
20525 point.half = angle < -Math.PI / 2 || angle > Math.PI / 2 ? 1 : 0;
20526 point.angle = angle;
20527
20528 // set the anchor point for data labels
20529 connectorOffset = Math.min(connectorOffset, labelDistance / 5); // #1678
20530 point.labelPos = [
20531 positions[0] + radiusX + Math.cos(angle) * labelDistance, // first break of connector
20532 positions[1] + radiusY + Math.sin(angle) * labelDistance, // a/a
20533 positions[0] + radiusX + Math.cos(angle) * connectorOffset, // second break, right outside pie
20534 positions[1] + radiusY + Math.sin(angle) * connectorOffset, // a/a
20535 positions[0] + radiusX, // landing point for connector
20536 positions[1] + radiusY, // a/a
20537 labelDistance < 0 ? // alignment
20538 'center' :
20539 point.half ? 'right' : 'left', // alignment
20540 angle // center angle
20541 ];
20542
20543 }
20544 },
20545
20546 drawGraph: null,
20547
20548 /**
20549 * Draw the data points
20550 */
20551 drawPoints: function() {
20552 var series = this,
20553 chart = series.chart,
20554 renderer = chart.renderer,
20555 groupTranslation,
20556 //center,
20557 graphic,
20558 //group,
20559 pointAttr,
20560 shapeArgs;
20561
20562
20563 var shadow = series.options.shadow;
20564 if (shadow && !series.shadowGroup) {
20565 series.shadowGroup = renderer.g('shadow')
20566 .add(series.group);
20567 }
20568
20569
20570 // draw the slices
20571 each(series.points, function(point) {
20572 if (point.y !== null) {
20573 graphic = point.graphic;
20574 shapeArgs = point.shapeArgs;
20575
20576
20577 // if the point is sliced, use special translation, else use plot area traslation
20578 groupTranslation = point.sliced ? point.slicedTranslation : {};
20579
20580
20581 // Put the shadow behind all points
20582 var shadowGroup = point.shadowGroup;
20583 if (shadow && !shadowGroup) {
20584 shadowGroup = point.shadowGroup = renderer.g('shadow')
20585 .add(series.shadowGroup);
20586 }
20587
20588 if (shadowGroup) {
20589 shadowGroup.attr(groupTranslation);
20590 }
20591 pointAttr = series.pointAttribs(point, point.selected && 'select');
20592
20593
20594 // Draw the slice
20595 if (graphic) {
20596 graphic
20597 .setRadialReference(series.center)
20598
20599 .attr(pointAttr)
20600
20601 .animate(extend(shapeArgs, groupTranslation));
20602 } else {
20603
20604 point.graphic = graphic = renderer[point.shapeType](shapeArgs)
20605 .addClass(point.getClassName())
20606 .setRadialReference(series.center)
20607 .attr(groupTranslation)
20608 .add(series.group);
20609
20610 if (!point.visible) {
20611 graphic.attr({
20612 visibility: 'hidden'
20613 });
20614 }
20615
20616
20617 graphic
20618 .attr(pointAttr)
20619 .attr({
20620 'stroke-linejoin': 'round'
20621 })
20622 .shadow(shadow, shadowGroup);
20623
20624 }
20625 }
20626 });
20627
20628 },
20629
20630
20631 searchPoint: noop,
20632
20633 /**
20634 * Utility for sorting data labels
20635 */
20636 sortByAngle: function(points, sign) {
20637 points.sort(function(a, b) {
20638 return a.angle !== undefined && (b.angle - a.angle) * sign;
20639 });
20640 },
20641
20642 /**
20643 * Use a simple symbol from LegendSymbolMixin
20644 */
20645 drawLegendSymbol: LegendSymbolMixin.drawRectangle,
20646
20647 /**
20648 * Use the getCenter method from drawLegendSymbol
20649 */
20650 getCenter: CenteredSeriesMixin.getCenter,
20651
20652 /**
20653 * Pies don't have point marker symbols
20654 */
20655 getSymbol: noop
20656
20657
20658 /**
20659 * @constructor seriesTypes.pie.prototype.pointClass
20660 * @extends {Point}
20661 */
20662 }, /** @lends seriesTypes.pie.prototype.pointClass.prototype */ {
20663 /**
20664 * Initiate the pie slice
20665 */
20666 init: function() {
20667
20668 Point.prototype.init.apply(this, arguments);
20669
20670 var point = this,
20671 toggleSlice;
20672
20673 point.name = pick(point.name, 'Slice');
20674
20675 // add event listener for select
20676 toggleSlice = function(e) {
20677 point.slice(e.type === 'select');
20678 };
20679 addEvent(point, 'select', toggleSlice);
20680 addEvent(point, 'unselect', toggleSlice);
20681
20682 return point;
20683 },
20684
20685 /**
20686 * Toggle the visibility of the pie slice
20687 * @param {Boolean} vis Whether to show the slice or not. If undefined, the
20688 * visibility is toggled
20689 */
20690 setVisible: function(vis, redraw) {
20691 var point = this,
20692 series = point.series,
20693 chart = series.chart,
20694 ignoreHiddenPoint = series.options.ignoreHiddenPoint;
20695
20696 redraw = pick(redraw, ignoreHiddenPoint);
20697
20698 if (vis !== point.visible) {
20699
20700 // If called without an argument, toggle visibility
20701 point.visible = point.options.visible = vis = vis === undefined ? !point.visible : vis;
20702 series.options.data[inArray(point, series.data)] = point.options; // update userOptions.data
20703
20704 // Show and hide associated elements. This is performed regardless of redraw or not,
20705 // because chart.redraw only handles full series.
20706 each(['graphic', 'dataLabel', 'connector', 'shadowGroup'], function(key) {
20707 if (point[key]) {
20708 point[key][vis ? 'show' : 'hide'](true);
20709 }
20710 });
20711
20712 if (point.legendItem) {
20713 chart.legend.colorizeItem(point, vis);
20714 }
20715
20716 // #4170, hide halo after hiding point
20717 if (!vis && point.state === 'hover') {
20718 point.setState('');
20719 }
20720
20721 // Handle ignore hidden slices
20722 if (ignoreHiddenPoint) {
20723 series.isDirty = true;
20724 }
20725
20726 if (redraw) {
20727 chart.redraw();
20728 }
20729 }
20730 },
20731
20732 /**
20733 * Set or toggle whether the slice is cut out from the pie
20734 * @param {Boolean} sliced When undefined, the slice state is toggled
20735 * @param {Boolean} redraw Whether to redraw the chart. True by default.
20736 */
20737 slice: function(sliced, redraw, animation) {
20738 var point = this,
20739 series = point.series,
20740 chart = series.chart,
20741 translation;
20742
20743 setAnimation(animation, chart);
20744
20745 // redraw is true by default
20746 redraw = pick(redraw, true);
20747
20748 // if called without an argument, toggle
20749 point.sliced = point.options.sliced = sliced = defined(sliced) ? sliced : !point.sliced;
20750 series.options.data[inArray(point, series.data)] = point.options; // update userOptions.data
20751
20752 translation = sliced ? point.slicedTranslation : {
20753 translateX: 0,
20754 translateY: 0
20755 };
20756
20757 point.graphic.animate(translation);
20758
20759
20760 if (point.shadowGroup) {
20761 point.shadowGroup.animate(translation);
20762 }
20763
20764 },
20765
20766 haloPath: function(size) {
20767 var shapeArgs = this.shapeArgs;
20768
20769 return this.sliced || !this.visible ? [] :
20770 this.series.chart.renderer.symbols.arc(
20771 shapeArgs.x,
20772 shapeArgs.y,
20773 shapeArgs.r + size,
20774 shapeArgs.r + size, {
20775 innerR: this.shapeArgs.r,
20776 start: shapeArgs.start,
20777 end: shapeArgs.end
20778 }
20779 );
20780 }
20781 });
20782
20783 }(Highcharts));
20784 (function(H) {
20785 /**
20786 * (c) 2010-2016 Torstein Honsi
20787 *
20788 * License: www.highcharts.com/license
20789 */
20790 'use strict';
20791 var addEvent = H.addEvent,
20792 arrayMax = H.arrayMax,
20793 defined = H.defined,
20794 each = H.each,
20795 extend = H.extend,
20796 format = H.format,
20797 map = H.map,
20798 merge = H.merge,
20799 noop = H.noop,
20800 pick = H.pick,
20801 relativeLength = H.relativeLength,
20802 Series = H.Series,
20803 seriesTypes = H.seriesTypes,
20804 stableSort = H.stableSort;
20805
20806
20807 /**
20808 * Generatl distribution algorithm for distributing labels of differing size along a
20809 * confined length in two dimensions. The algorithm takes an array of objects containing
20810 * a size, a target and a rank. It will place the labels as close as possible to their
20811 * targets, skipping the lowest ranked labels if necessary.
20812 */
20813 H.distribute = function(boxes, len) {
20814
20815 var i,
20816 overlapping = true,
20817 origBoxes = boxes, // Original array will be altered with added .pos
20818 restBoxes = [], // The outranked overshoot
20819 box,
20820 target,
20821 total = 0;
20822
20823 function sortByTarget(a, b) {
20824 return a.target - b.target;
20825 }
20826
20827 // If the total size exceeds the len, remove those boxes with the lowest rank
20828 i = boxes.length;
20829 while (i--) {
20830 total += boxes[i].size;
20831 }
20832
20833 // Sort by rank, then slice away overshoot
20834 if (total > len) {
20835 stableSort(boxes, function(a, b) {
20836 return (b.rank || 0) - (a.rank || 0);
20837 });
20838 i = 0;
20839 total = 0;
20840 while (total <= len) {
20841 total += boxes[i].size;
20842 i++;
20843 }
20844 restBoxes = boxes.splice(i - 1, boxes.length);
20845 }
20846
20847 // Order by target
20848 stableSort(boxes, sortByTarget);
20849
20850
20851 // So far we have been mutating the original array. Now
20852 // create a copy with target arrays
20853 boxes = map(boxes, function(box) {
20854 return {
20855 size: box.size,
20856 targets: [box.target]
20857 };
20858 });
20859
20860 while (overlapping) {
20861 // Initial positions: target centered in box
20862 i = boxes.length;
20863 while (i--) {
20864 box = boxes[i];
20865 // Composite box, average of targets
20866 target = (Math.min.apply(0, box.targets) + Math.max.apply(0, box.targets)) / 2;
20867 box.pos = Math.min(Math.max(0, target - box.size / 2), len - box.size);
20868 }
20869
20870 // Detect overlap and join boxes
20871 i = boxes.length;
20872 overlapping = false;
20873 while (i--) {
20874 if (i > 0 && boxes[i - 1].pos + boxes[i - 1].size > boxes[i].pos) { // Overlap
20875 boxes[i - 1].size += boxes[i].size; // Add this size to the previous box
20876 boxes[i - 1].targets = boxes[i - 1].targets.concat(boxes[i].targets);
20877
20878 // Overlapping right, push left
20879 if (boxes[i - 1].pos + boxes[i - 1].size > len) {
20880 boxes[i - 1].pos = len - boxes[i - 1].size;
20881 }
20882 boxes.splice(i, 1); // Remove this item
20883 overlapping = true;
20884 }
20885 }
20886 }
20887
20888 // Now the composite boxes are placed, we need to put the original boxes within them
20889 i = 0;
20890 each(boxes, function(box) {
20891 var posInCompositeBox = 0;
20892 each(box.targets, function() {
20893 origBoxes[i].pos = box.pos + posInCompositeBox;
20894 posInCompositeBox += origBoxes[i].size;
20895 i++;
20896 });
20897 });
20898
20899 // Add the rest (hidden) boxes and sort by target
20900 origBoxes.push.apply(origBoxes, restBoxes);
20901 stableSort(origBoxes, sortByTarget);
20902 };
20903
20904
20905 /**
20906 * Draw the data labels
20907 */
20908 Series.prototype.drawDataLabels = function() {
20909
20910 var series = this,
20911 seriesOptions = series.options,
20912 options = seriesOptions.dataLabels,
20913 points = series.points,
20914 pointOptions,
20915 generalOptions,
20916 hasRendered = series.hasRendered || 0,
20917 str,
20918 dataLabelsGroup,
20919 defer = pick(options.defer, true),
20920 renderer = series.chart.renderer;
20921
20922 if (options.enabled || series._hasPointLabels) {
20923
20924 // Process default alignment of data labels for columns
20925 if (series.dlProcessOptions) {
20926 series.dlProcessOptions(options);
20927 }
20928
20929 // Create a separate group for the data labels to avoid rotation
20930 dataLabelsGroup = series.plotGroup(
20931 'dataLabelsGroup',
20932 'data-labels',
20933 defer && !hasRendered ? 'hidden' : 'visible', // #5133
20934 options.zIndex || 6
20935 );
20936
20937 if (defer) {
20938 dataLabelsGroup.attr({
20939 opacity: +hasRendered
20940 }); // #3300
20941 if (!hasRendered) {
20942 addEvent(series, 'afterAnimate', function() {
20943 if (series.visible) { // #2597, #3023, #3024
20944 dataLabelsGroup.show(true);
20945 }
20946 dataLabelsGroup[seriesOptions.animation ? 'animate' : 'attr']({
20947 opacity: 1
20948 }, {
20949 duration: 200
20950 });
20951 });
20952 }
20953 }
20954
20955 // Make the labels for each point
20956 generalOptions = options;
20957 each(points, function(point) {
20958
20959 var enabled,
20960 dataLabel = point.dataLabel,
20961 labelConfig,
20962 attr,
20963 name,
20964 rotation,
20965 connector = point.connector,
20966 isNew = true,
20967 style,
20968 moreStyle = {};
20969
20970 // Determine if each data label is enabled
20971 pointOptions = point.dlOptions || (point.options && point.options.dataLabels); // dlOptions is used in treemaps
20972 enabled = pick(pointOptions && pointOptions.enabled, generalOptions.enabled) && point.y !== null; // #2282, #4641
20973
20974
20975 // If the point is outside the plot area, destroy it. #678, #820
20976 if (dataLabel && !enabled) {
20977 point.dataLabel = dataLabel.destroy();
20978
20979 // Individual labels are disabled if the are explicitly disabled
20980 // in the point options, or if they fall outside the plot area.
20981 } else if (enabled) {
20982
20983 // Create individual options structure that can be extended without
20984 // affecting others
20985 options = merge(generalOptions, pointOptions);
20986 style = options.style;
20987
20988 rotation = options.rotation;
20989
20990 // Get the string
20991 labelConfig = point.getLabelConfig();
20992 str = options.format ?
20993 format(options.format, labelConfig) :
20994 options.formatter.call(labelConfig, options);
20995
20996
20997 // Determine the color
20998 style.color = pick(options.color, style.color, series.color, '#000000');
20999
21000
21001 // update existing label
21002 if (dataLabel) {
21003
21004 if (defined(str)) {
21005 dataLabel
21006 .attr({
21007 text: str
21008 });
21009 isNew = false;
21010
21011 } else { // #1437 - the label is shown conditionally
21012 point.dataLabel = dataLabel = dataLabel.destroy();
21013 if (connector) {
21014 point.connector = connector.destroy();
21015 }
21016 }
21017
21018 // create new label
21019 } else if (defined(str)) {
21020 attr = {
21021 //align: align,
21022
21023 fill: options.backgroundColor,
21024 stroke: options.borderColor,
21025 'stroke-width': options.borderWidth,
21026
21027 r: options.borderRadius || 0,
21028 rotation: rotation,
21029 padding: options.padding,
21030 zIndex: 1
21031 };
21032
21033
21034 // Get automated contrast color
21035 if (style.color === 'contrast') {
21036 moreStyle.color = options.inside || options.distance < 0 || !!seriesOptions.stacking ?
21037 renderer.getContrast(point.color || series.color) :
21038 '#000000';
21039 }
21040
21041 if (seriesOptions.cursor) {
21042 moreStyle.cursor = seriesOptions.cursor;
21043 }
21044
21045
21046
21047 // Remove unused attributes (#947)
21048 for (name in attr) {
21049 if (attr[name] === undefined) {
21050 delete attr[name];
21051 }
21052 }
21053
21054 dataLabel = point.dataLabel = renderer[rotation ? 'text' : 'label']( // labels don't support rotation
21055 str,
21056 0, -9999,
21057 options.shape,
21058 null,
21059 null,
21060 options.useHTML,
21061 null,
21062 'data-label'
21063 )
21064 .attr(attr);
21065
21066 dataLabel.addClass(
21067 'highcharts-data-label-color-' + point.colorIndex +
21068 ' ' + (options.className || '') +
21069 (options.useHTML ? 'highcharts-tracker' : '') // #3398
21070 );
21071
21072
21073 // Styles must be applied before add in order to read text bounding box
21074 dataLabel.css(extend(style, moreStyle));
21075
21076
21077 dataLabel.add(dataLabelsGroup);
21078
21079
21080 dataLabel.shadow(options.shadow);
21081
21082
21083
21084 }
21085
21086 if (dataLabel) {
21087 // Now the data label is created and placed at 0,0, so we need to align it
21088 series.alignDataLabel(point, dataLabel, options, null, isNew);
21089 }
21090 }
21091 });
21092 }
21093 };
21094
21095 /**
21096 * Align each individual data label
21097 */
21098 Series.prototype.alignDataLabel = function(point, dataLabel, options, alignTo, isNew) {
21099 var chart = this.chart,
21100 inverted = chart.inverted,
21101 plotX = pick(point.plotX, -9999),
21102 plotY = pick(point.plotY, -9999),
21103 bBox = dataLabel.getBBox(),
21104 fontSize,
21105 baseline,
21106 rotation = options.rotation,
21107 normRotation,
21108 negRotation,
21109 align = options.align,
21110 rotCorr, // rotation correction
21111 // Math.round for rounding errors (#2683), alignTo to allow column labels (#2700)
21112 visible = this.visible && (point.series.forceDL || chart.isInsidePlot(plotX, Math.round(plotY), inverted) ||
21113 (alignTo && chart.isInsidePlot(plotX, inverted ? alignTo.x + 1 : alignTo.y + alignTo.height - 1, inverted))),
21114 alignAttr, // the final position;
21115 justify = pick(options.overflow, 'justify') === 'justify';
21116
21117 if (visible) {
21118
21119
21120 fontSize = options.style.fontSize;
21121
21122
21123 baseline = chart.renderer.fontMetrics(fontSize, dataLabel).b;
21124
21125 // The alignment box is a singular point
21126 alignTo = extend({
21127 x: inverted ? chart.plotWidth - plotY : plotX,
21128 y: Math.round(inverted ? chart.plotHeight - plotX : plotY),
21129 width: 0,
21130 height: 0
21131 }, alignTo);
21132
21133 // Add the text size for alignment calculation
21134 extend(options, {
21135 width: bBox.width,
21136 height: bBox.height
21137 });
21138
21139 // Allow a hook for changing alignment in the last moment, then do the alignment
21140 if (rotation) {
21141 justify = false; // Not supported for rotated text
21142 rotCorr = chart.renderer.rotCorr(baseline, rotation); // #3723
21143 alignAttr = {
21144 x: alignTo.x + options.x + alignTo.width / 2 + rotCorr.x,
21145 y: alignTo.y + options.y + {
21146 top: 0,
21147 middle: 0.5,
21148 bottom: 1
21149 }[options.verticalAlign] * alignTo.height
21150 };
21151 dataLabel[isNew ? 'attr' : 'animate'](alignAttr)
21152 .attr({ // #3003
21153 align: align
21154 });
21155
21156 // Compensate for the rotated label sticking out on the sides
21157 normRotation = (rotation + 720) % 360;
21158 negRotation = normRotation > 180 && normRotation < 360;
21159
21160 if (align === 'left') {
21161 alignAttr.y -= negRotation ? bBox.height : 0;
21162 } else if (align === 'center') {
21163 alignAttr.x -= bBox.width / 2;
21164 alignAttr.y -= bBox.height / 2;
21165 } else if (align === 'right') {
21166 alignAttr.x -= bBox.width;
21167 alignAttr.y -= negRotation ? 0 : bBox.height;
21168 }
21169
21170
21171 } else {
21172 dataLabel.align(options, null, alignTo);
21173 alignAttr = dataLabel.alignAttr;
21174 }
21175
21176 // Handle justify or crop
21177 if (justify) {
21178 this.justifyDataLabel(dataLabel, options, alignAttr, bBox, alignTo, isNew);
21179
21180 // Now check that the data label is within the plot area
21181 } else if (pick(options.crop, true)) {
21182 visible = chart.isInsidePlot(alignAttr.x, alignAttr.y) && chart.isInsidePlot(alignAttr.x + bBox.width, alignAttr.y + bBox.height);
21183 }
21184
21185 // When we're using a shape, make it possible with a connector or an arrow pointing to thie point
21186 if (options.shape && !rotation) {
21187 dataLabel.attr({
21188 anchorX: point.plotX,
21189 anchorY: point.plotY
21190 });
21191 }
21192 }
21193
21194 // Show or hide based on the final aligned position
21195 if (!visible) {
21196 dataLabel.attr({
21197 y: -9999
21198 });
21199 dataLabel.placed = false; // don't animate back in
21200 }
21201
21202 };
21203
21204 /**
21205 * If data labels fall partly outside the plot area, align them back in, in a way that
21206 * doesn't hide the point.
21207 */
21208 Series.prototype.justifyDataLabel = function(dataLabel, options, alignAttr, bBox, alignTo, isNew) {
21209 var chart = this.chart,
21210 align = options.align,
21211 verticalAlign = options.verticalAlign,
21212 off,
21213 justified,
21214 padding = dataLabel.box ? 0 : (dataLabel.padding || 0);
21215
21216 // Off left
21217 off = alignAttr.x + padding;
21218 if (off < 0) {
21219 if (align === 'right') {
21220 options.align = 'left';
21221 } else {
21222 options.x = -off;
21223 }
21224 justified = true;
21225 }
21226
21227 // Off right
21228 off = alignAttr.x + bBox.width - padding;
21229 if (off > chart.plotWidth) {
21230 if (align === 'left') {
21231 options.align = 'right';
21232 } else {
21233 options.x = chart.plotWidth - off;
21234 }
21235 justified = true;
21236 }
21237
21238 // Off top
21239 off = alignAttr.y + padding;
21240 if (off < 0) {
21241 if (verticalAlign === 'bottom') {
21242 options.verticalAlign = 'top';
21243 } else {
21244 options.y = -off;
21245 }
21246 justified = true;
21247 }
21248
21249 // Off bottom
21250 off = alignAttr.y + bBox.height - padding;
21251 if (off > chart.plotHeight) {
21252 if (verticalAlign === 'top') {
21253 options.verticalAlign = 'bottom';
21254 } else {
21255 options.y = chart.plotHeight - off;
21256 }
21257 justified = true;
21258 }
21259
21260 if (justified) {
21261 dataLabel.placed = !isNew;
21262 dataLabel.align(options, null, alignTo);
21263 }
21264 };
21265
21266 /**
21267 * Override the base drawDataLabels method by pie specific functionality
21268 */
21269 if (seriesTypes.pie) {
21270 seriesTypes.pie.prototype.drawDataLabels = function() {
21271 var series = this,
21272 data = series.data,
21273 point,
21274 chart = series.chart,
21275 options = series.options.dataLabels,
21276 connectorPadding = pick(options.connectorPadding, 10),
21277 connectorWidth = pick(options.connectorWidth, 1),
21278 plotWidth = chart.plotWidth,
21279 plotHeight = chart.plotHeight,
21280 connector,
21281 distanceOption = options.distance,
21282 seriesCenter = series.center,
21283 radius = seriesCenter[2] / 2,
21284 centerY = seriesCenter[1],
21285 outside = distanceOption > 0,
21286 dataLabel,
21287 dataLabelWidth,
21288 labelPos,
21289 labelHeight,
21290 halves = [ // divide the points into right and left halves for anti collision
21291 [], // right
21292 [] // left
21293 ],
21294 x,
21295 y,
21296 visibility,
21297 j,
21298 overflow = [0, 0, 0, 0]; // top, right, bottom, left
21299
21300 // get out if not enabled
21301 if (!series.visible || (!options.enabled && !series._hasPointLabels)) {
21302 return;
21303 }
21304
21305 // run parent method
21306 Series.prototype.drawDataLabels.apply(series);
21307
21308 each(data, function(point) {
21309 if (point.dataLabel && point.visible) { // #407, #2510
21310
21311 // Arrange points for detection collision
21312 halves[point.half].push(point);
21313
21314 // Reset positions (#4905)
21315 point.dataLabel._pos = null;
21316 }
21317 });
21318
21319 /* Loop over the points in each half, starting from the top and bottom
21320 * of the pie to detect overlapping labels.
21321 */
21322 each(halves, function(points, i) {
21323
21324 var top,
21325 bottom,
21326 length = points.length,
21327 positions,
21328 naturalY,
21329 size;
21330
21331 if (!length) {
21332 return;
21333 }
21334
21335 // Sort by angle
21336 series.sortByAngle(points, i - 0.5);
21337
21338 // Only do anti-collision when we are outside the pie and have connectors (#856)
21339 if (distanceOption > 0) {
21340 top = Math.max(0, centerY - radius - distanceOption);
21341 bottom = Math.min(centerY + radius + distanceOption, chart.plotHeight);
21342 positions = map(points, function(point) {
21343 if (point.dataLabel) {
21344 size = point.dataLabel.getBBox().height || 21;
21345 return {
21346 target: point.labelPos[1] - top + size / 2,
21347 size: size,
21348 rank: point.y
21349 };
21350 }
21351 });
21352 H.distribute(positions, bottom + size - top);
21353 }
21354
21355 // now the used slots are sorted, fill them up sequentially
21356 for (j = 0; j < length; j++) {
21357
21358 point = points[j];
21359 labelPos = point.labelPos;
21360 dataLabel = point.dataLabel;
21361 visibility = point.visible === false ? 'hidden' : 'inherit';
21362 naturalY = labelPos[1];
21363
21364 if (positions) {
21365 if (positions[j].pos === undefined) {
21366 visibility = 'hidden';
21367 } else {
21368 labelHeight = positions[j].size;
21369 y = top + positions[j].pos;
21370 }
21371
21372 } else {
21373 y = naturalY;
21374 }
21375
21376 // get the x - use the natural x position for labels near the top and bottom, to prevent the top
21377 // and botton slice connectors from touching each other on either side
21378 if (options.justify) {
21379 x = seriesCenter[0] + (i ? -1 : 1) * (radius + distanceOption);
21380 } else {
21381 x = series.getX(y < top + 2 || y > bottom - 2 ? naturalY : y, i);
21382 }
21383
21384
21385 // Record the placement and visibility
21386 dataLabel._attr = {
21387 visibility: visibility,
21388 align: labelPos[6]
21389 };
21390 dataLabel._pos = {
21391 x: x + options.x +
21392 ({
21393 left: connectorPadding,
21394 right: -connectorPadding
21395 }[labelPos[6]] || 0),
21396 y: y + options.y - 10 // 10 is for the baseline (label vs text)
21397 };
21398 labelPos.x = x;
21399 labelPos.y = y;
21400
21401
21402 // Detect overflowing data labels
21403 if (series.options.size === null) {
21404 dataLabelWidth = dataLabel.width;
21405 // Overflow left
21406 if (x - dataLabelWidth < connectorPadding) {
21407 overflow[3] = Math.max(Math.round(dataLabelWidth - x + connectorPadding), overflow[3]);
21408
21409 // Overflow right
21410 } else if (x + dataLabelWidth > plotWidth - connectorPadding) {
21411 overflow[1] = Math.max(Math.round(x + dataLabelWidth - plotWidth + connectorPadding), overflow[1]);
21412 }
21413
21414 // Overflow top
21415 if (y - labelHeight / 2 < 0) {
21416 overflow[0] = Math.max(Math.round(-y + labelHeight / 2), overflow[0]);
21417
21418 // Overflow left
21419 } else if (y + labelHeight / 2 > plotHeight) {
21420 overflow[2] = Math.max(Math.round(y + labelHeight / 2 - plotHeight), overflow[2]);
21421 }
21422 }
21423 } // for each point
21424 }); // for each half
21425
21426 // Do not apply the final placement and draw the connectors until we have verified
21427 // that labels are not spilling over.
21428 if (arrayMax(overflow) === 0 || this.verifyDataLabelOverflow(overflow)) {
21429
21430 // Place the labels in the final position
21431 this.placeDataLabels();
21432
21433 // Draw the connectors
21434 if (outside && connectorWidth) {
21435 each(this.points, function(point) {
21436 var isNew;
21437
21438 connector = point.connector;
21439 dataLabel = point.dataLabel;
21440
21441 if (dataLabel && dataLabel._pos && point.visible) {
21442 visibility = dataLabel._attr.visibility;
21443
21444 isNew = !connector;
21445
21446 if (isNew) {
21447 point.connector = connector = chart.renderer.path()
21448 .addClass('highcharts-data-label-connector highcharts-color-' + point.colorIndex)
21449 .add(series.dataLabelsGroup);
21450
21451
21452 connector.attr({
21453 'stroke-width': connectorWidth,
21454 'stroke': options.connectorColor || point.color || '#666666'
21455 });
21456
21457 }
21458 connector[isNew ? 'attr' : 'animate']({
21459 d: series.connectorPath(point.labelPos)
21460 });
21461 connector.attr('visibility', visibility);
21462
21463 } else if (connector) {
21464 point.connector = connector.destroy();
21465 }
21466 });
21467 }
21468 }
21469 };
21470
21471 /**
21472 * Extendable method for getting the path of the connector between the data label
21473 * and the pie slice.
21474 */
21475 seriesTypes.pie.prototype.connectorPath = function(labelPos) {
21476 var x = labelPos.x,
21477 y = labelPos.y;
21478 return pick(this.options.dataLabels.softConnector, true) ? [
21479 'M',
21480 x + (labelPos[6] === 'left' ? 5 : -5), y, // end of the string at the label
21481 'C',
21482 x, y, // first break, next to the label
21483 2 * labelPos[2] - labelPos[4], 2 * labelPos[3] - labelPos[5],
21484 labelPos[2], labelPos[3], // second break
21485 'L',
21486 labelPos[4], labelPos[5] // base
21487 ] : [
21488 'M',
21489 x + (labelPos[6] === 'left' ? 5 : -5), y, // end of the string at the label
21490 'L',
21491 labelPos[2], labelPos[3], // second break
21492 'L',
21493 labelPos[4], labelPos[5] // base
21494 ];
21495 };
21496
21497 /**
21498 * Perform the final placement of the data labels after we have verified that they
21499 * fall within the plot area.
21500 */
21501 seriesTypes.pie.prototype.placeDataLabels = function() {
21502 each(this.points, function(point) {
21503 var dataLabel = point.dataLabel,
21504 _pos;
21505
21506 if (dataLabel && point.visible) {
21507 _pos = dataLabel._pos;
21508 if (_pos) {
21509 dataLabel.attr(dataLabel._attr);
21510 dataLabel[dataLabel.moved ? 'animate' : 'attr'](_pos);
21511 dataLabel.moved = true;
21512 } else if (dataLabel) {
21513 dataLabel.attr({
21514 y: -9999
21515 });
21516 }
21517 }
21518 });
21519 };
21520
21521 seriesTypes.pie.prototype.alignDataLabel = noop;
21522
21523 /**
21524 * Verify whether the data labels are allowed to draw, or we should run more translation and data
21525 * label positioning to keep them inside the plot area. Returns true when data labels are ready
21526 * to draw.
21527 */
21528 seriesTypes.pie.prototype.verifyDataLabelOverflow = function(overflow) {
21529
21530 var center = this.center,
21531 options = this.options,
21532 centerOption = options.center,
21533 minSize = options.minSize || 80,
21534 newSize = minSize,
21535 ret;
21536
21537 // Handle horizontal size and center
21538 if (centerOption[0] !== null) { // Fixed center
21539 newSize = Math.max(center[2] - Math.max(overflow[1], overflow[3]), minSize);
21540
21541 } else { // Auto center
21542 newSize = Math.max(
21543 center[2] - overflow[1] - overflow[3], // horizontal overflow
21544 minSize
21545 );
21546 center[0] += (overflow[3] - overflow[1]) / 2; // horizontal center
21547 }
21548
21549 // Handle vertical size and center
21550 if (centerOption[1] !== null) { // Fixed center
21551 newSize = Math.max(Math.min(newSize, center[2] - Math.max(overflow[0], overflow[2])), minSize);
21552
21553 } else { // Auto center
21554 newSize = Math.max(
21555 Math.min(
21556 newSize,
21557 center[2] - overflow[0] - overflow[2] // vertical overflow
21558 ),
21559 minSize
21560 );
21561 center[1] += (overflow[0] - overflow[2]) / 2; // vertical center
21562 }
21563
21564 // If the size must be decreased, we need to run translate and drawDataLabels again
21565 if (newSize < center[2]) {
21566 center[2] = newSize;
21567 center[3] = Math.min(relativeLength(options.innerSize || 0, newSize), newSize); // #3632
21568 this.translate(center);
21569
21570 if (this.drawDataLabels) {
21571 this.drawDataLabels();
21572 }
21573 // Else, return true to indicate that the pie and its labels is within the plot area
21574 } else {
21575 ret = true;
21576 }
21577 return ret;
21578 };
21579 }
21580
21581 if (seriesTypes.column) {
21582
21583 /**
21584 * Override the basic data label alignment by adjusting for the position of the column
21585 */
21586 seriesTypes.column.prototype.alignDataLabel = function(point, dataLabel, options, alignTo, isNew) {
21587 var inverted = this.chart.inverted,
21588 series = point.series,
21589 dlBox = point.dlBox || point.shapeArgs, // data label box for alignment
21590 below = pick(point.below, point.plotY > pick(this.translatedThreshold, series.yAxis.len)), // point.below is used in range series
21591 inside = pick(options.inside, !!this.options.stacking), // draw it inside the box?
21592 overshoot;
21593
21594 // Align to the column itself, or the top of it
21595 if (dlBox) { // Area range uses this method but not alignTo
21596 alignTo = merge(dlBox);
21597
21598 if (alignTo.y < 0) {
21599 alignTo.height += alignTo.y;
21600 alignTo.y = 0;
21601 }
21602 overshoot = alignTo.y + alignTo.height - series.yAxis.len;
21603 if (overshoot > 0) {
21604 alignTo.height -= overshoot;
21605 }
21606
21607 if (inverted) {
21608 alignTo = {
21609 x: series.yAxis.len - alignTo.y - alignTo.height,
21610 y: series.xAxis.len - alignTo.x - alignTo.width,
21611 width: alignTo.height,
21612 height: alignTo.width
21613 };
21614 }
21615
21616 // Compute the alignment box
21617 if (!inside) {
21618 if (inverted) {
21619 alignTo.x += below ? 0 : alignTo.width;
21620 alignTo.width = 0;
21621 } else {
21622 alignTo.y += below ? alignTo.height : 0;
21623 alignTo.height = 0;
21624 }
21625 }
21626 }
21627
21628
21629 // When alignment is undefined (typically columns and bars), display the individual
21630 // point below or above the point depending on the threshold
21631 options.align = pick(
21632 options.align, !inverted || inside ? 'center' : below ? 'right' : 'left'
21633 );
21634 options.verticalAlign = pick(
21635 options.verticalAlign,
21636 inverted || inside ? 'middle' : below ? 'top' : 'bottom'
21637 );
21638
21639 // Call the parent method
21640 Series.prototype.alignDataLabel.call(this, point, dataLabel, options, alignTo, isNew);
21641 };
21642 }
21643
21644 }(Highcharts));
21645 (function(H) {
21646 /**
21647 * (c) 2009-2016 Torstein Honsi
21648 *
21649 * License: www.highcharts.com/license
21650 */
21651 'use strict';
21652 /**
21653 * Highcharts module to hide overlapping data labels. This module is included in Highcharts.
21654 */
21655 var Chart = H.Chart,
21656 each = H.each,
21657 pick = H.pick,
21658 addEvent = H.addEvent;
21659
21660 // Collect potensial overlapping data labels. Stack labels probably don't need to be
21661 // considered because they are usually accompanied by data labels that lie inside the columns.
21662 Chart.prototype.callbacks.push(function(chart) {
21663 function collectAndHide() {
21664 var labels = [];
21665
21666 each(chart.series, function(series) {
21667 var dlOptions = series.options.dataLabels,
21668 collections = series.dataLabelCollections || ['dataLabel']; // Range series have two collections
21669 if ((dlOptions.enabled || series._hasPointLabels) && !dlOptions.allowOverlap && series.visible) { // #3866
21670 each(collections, function(coll) {
21671 each(series.points, function(point) {
21672 if (point[coll]) {
21673 point[coll].labelrank = pick(point.labelrank, point.shapeArgs && point.shapeArgs.height); // #4118
21674 labels.push(point[coll]);
21675 }
21676 });
21677 });
21678 }
21679 });
21680 chart.hideOverlappingLabels(labels);
21681 }
21682
21683 // Do it now ...
21684 collectAndHide();
21685
21686 // ... and after each chart redraw
21687 addEvent(chart, 'redraw', collectAndHide);
21688
21689 });
21690
21691 /**
21692 * Hide overlapping labels. Labels are moved and faded in and out on zoom to provide a smooth
21693 * visual imression.
21694 */
21695 Chart.prototype.hideOverlappingLabels = function(labels) {
21696
21697 var len = labels.length,
21698 label,
21699 i,
21700 j,
21701 label1,
21702 label2,
21703 isIntersecting,
21704 pos1,
21705 pos2,
21706 parent1,
21707 parent2,
21708 padding,
21709 intersectRect = function(x1, y1, w1, h1, x2, y2, w2, h2) {
21710 return !(
21711 x2 > x1 + w1 ||
21712 x2 + w2 < x1 ||
21713 y2 > y1 + h1 ||
21714 y2 + h2 < y1
21715 );
21716 };
21717
21718 // Mark with initial opacity
21719 for (i = 0; i < len; i++) {
21720 label = labels[i];
21721 if (label) {
21722 label.oldOpacity = label.opacity;
21723 label.newOpacity = 1;
21724 }
21725 }
21726
21727 // Prevent a situation in a gradually rising slope, that each label
21728 // will hide the previous one because the previous one always has
21729 // lower rank.
21730 labels.sort(function(a, b) {
21731 return (b.labelrank || 0) - (a.labelrank || 0);
21732 });
21733
21734 // Detect overlapping labels
21735 for (i = 0; i < len; i++) {
21736 label1 = labels[i];
21737
21738 for (j = i + 1; j < len; ++j) {
21739 label2 = labels[j];
21740 if (label1 && label2 && label1.placed && label2.placed && label1.newOpacity !== 0 && label2.newOpacity !== 0) {
21741 pos1 = label1.alignAttr;
21742 pos2 = label2.alignAttr;
21743 parent1 = label1.parentGroup; // Different panes have different positions
21744 parent2 = label2.parentGroup;
21745 padding = 2 * (label1.box ? 0 : label1.padding); // Substract the padding if no background or border (#4333)
21746 isIntersecting = intersectRect(
21747 pos1.x + parent1.translateX,
21748 pos1.y + parent1.translateY,
21749 label1.width - padding,
21750 label1.height - padding,
21751 pos2.x + parent2.translateX,
21752 pos2.y + parent2.translateY,
21753 label2.width - padding,
21754 label2.height - padding
21755 );
21756
21757 if (isIntersecting) {
21758 (label1.labelrank < label2.labelrank ? label1 : label2).newOpacity = 0;
21759 }
21760 }
21761 }
21762 }
21763
21764 // Hide or show
21765 each(labels, function(label) {
21766 var complete,
21767 newOpacity;
21768
21769 if (label) {
21770 newOpacity = label.newOpacity;
21771
21772 if (label.oldOpacity !== newOpacity && label.placed) {
21773
21774 // Make sure the label is completely hidden to avoid catching clicks (#4362)
21775 if (newOpacity) {
21776 label.show(true);
21777 } else {
21778 complete = function() {
21779 label.hide();
21780 };
21781 }
21782
21783 // Animate or set the opacity
21784 label.alignAttr.opacity = newOpacity;
21785 label[label.isOld ? 'animate' : 'attr'](label.alignAttr, null, complete);
21786
21787 }
21788 label.isOld = true;
21789 }
21790 });
21791 };
21792
21793 }(Highcharts));
21794 (function(H) {
21795 /**
21796 * (c) 2010-2016 Torstein Honsi
21797 *
21798 * License: www.highcharts.com/license
21799 */
21800 'use strict';
21801 var addEvent = H.addEvent,
21802 Chart = H.Chart,
21803 createElement = H.createElement,
21804 css = H.css,
21805 defaultOptions = H.defaultOptions,
21806 defaultPlotOptions = H.defaultPlotOptions,
21807 each = H.each,
21808 extend = H.extend,
21809 fireEvent = H.fireEvent,
21810 hasTouch = H.hasTouch,
21811 inArray = H.inArray,
21812 isObject = H.isObject,
21813 Legend = H.Legend,
21814 merge = H.merge,
21815 pick = H.pick,
21816 Point = H.Point,
21817 Series = H.Series,
21818 seriesTypes = H.seriesTypes,
21819 svg = H.svg,
21820 TrackerMixin;
21821
21822 /**
21823 * TrackerMixin for points and graphs.
21824 *
21825 * @mixin
21826 */
21827 TrackerMixin = H.TrackerMixin = {
21828
21829 /**
21830 * Draw the tracker for a point.
21831 */
21832 drawTrackerPoint: function() {
21833 var series = this,
21834 chart = series.chart,
21835 pointer = chart.pointer,
21836 onMouseOver = function(e) {
21837 var target = e.target,
21838 point;
21839
21840 while (target && !point) {
21841 point = target.point;
21842 target = target.parentNode;
21843 }
21844
21845 if (point !== undefined && point !== chart.hoverPoint) { // undefined on graph in scatterchart
21846 point.onMouseOver(e);
21847 }
21848 };
21849
21850 // Add reference to the point
21851 each(series.points, function(point) {
21852 if (point.graphic) {
21853 point.graphic.element.point = point;
21854 }
21855 if (point.dataLabel) {
21856 if (point.dataLabel.div) {
21857 point.dataLabel.div.point = point;
21858 } else {
21859 point.dataLabel.element.point = point;
21860 }
21861 }
21862 });
21863
21864 // Add the event listeners, we need to do this only once
21865 if (!series._hasTracking) {
21866 each(series.trackerGroups, function(key) {
21867 if (series[key]) { // we don't always have dataLabelsGroup
21868 series[key]
21869 .addClass('highcharts-tracker')
21870 .on('mouseover', onMouseOver)
21871 .on('mouseout', function(e) {
21872 pointer.onTrackerMouseOut(e);
21873 });
21874 if (hasTouch) {
21875 series[key].on('touchstart', onMouseOver);
21876 }
21877
21878
21879 if (series.options.cursor) {
21880 series[key]
21881 .css(css)
21882 .css({
21883 cursor: series.options.cursor
21884 });
21885 }
21886
21887 }
21888 });
21889 series._hasTracking = true;
21890 }
21891 },
21892
21893 /**
21894 * Draw the tracker object that sits above all data labels and markers to
21895 * track mouse events on the graph or points. For the line type charts
21896 * the tracker uses the same graphPath, but with a greater stroke width
21897 * for better control.
21898 */
21899 drawTrackerGraph: function() {
21900 var series = this,
21901 options = series.options,
21902 trackByArea = options.trackByArea,
21903 trackerPath = [].concat(trackByArea ? series.areaPath : series.graphPath),
21904 trackerPathLength = trackerPath.length,
21905 chart = series.chart,
21906 pointer = chart.pointer,
21907 renderer = chart.renderer,
21908 snap = chart.options.tooltip.snap,
21909 tracker = series.tracker,
21910 i,
21911 onMouseOver = function() {
21912 if (chart.hoverSeries !== series) {
21913 series.onMouseOver();
21914 }
21915 },
21916 /*
21917 * Empirical lowest possible opacities for TRACKER_FILL for an element to stay invisible but clickable
21918 * IE6: 0.002
21919 * IE7: 0.002
21920 * IE8: 0.002
21921 * IE9: 0.00000000001 (unlimited)
21922 * IE10: 0.0001 (exporting only)
21923 * FF: 0.00000000001 (unlimited)
21924 * Chrome: 0.000001
21925 * Safari: 0.000001
21926 * Opera: 0.00000000001 (unlimited)
21927 */
21928 TRACKER_FILL = 'rgba(192,192,192,' + (svg ? 0.0001 : 0.002) + ')';
21929
21930 // Extend end points. A better way would be to use round linecaps,
21931 // but those are not clickable in VML.
21932 if (trackerPathLength && !trackByArea) {
21933 i = trackerPathLength + 1;
21934 while (i--) {
21935 if (trackerPath[i] === 'M') { // extend left side
21936 trackerPath.splice(i + 1, 0, trackerPath[i + 1] - snap, trackerPath[i + 2], 'L');
21937 }
21938 if ((i && trackerPath[i] === 'M') || i === trackerPathLength) { // extend right side
21939 trackerPath.splice(i, 0, 'L', trackerPath[i - 2] + snap, trackerPath[i - 1]);
21940 }
21941 }
21942 }
21943
21944 // handle single points
21945 /*for (i = 0; i < singlePoints.length; i++) {
21946 singlePoint = singlePoints[i];
21947 trackerPath.push(M, singlePoint.plotX - snap, singlePoint.plotY,
21948 L, singlePoint.plotX + snap, singlePoint.plotY);
21949 }*/
21950
21951 // draw the tracker
21952 if (tracker) {
21953 tracker.attr({
21954 d: trackerPath
21955 });
21956 } else if (series.graph) { // create
21957
21958 series.tracker = renderer.path(trackerPath)
21959 .attr({
21960 'stroke-linejoin': 'round', // #1225
21961 visibility: series.visible ? 'visible' : 'hidden',
21962 stroke: TRACKER_FILL,
21963 fill: trackByArea ? TRACKER_FILL : 'none',
21964 'stroke-width': series.graph.strokeWidth() + (trackByArea ? 0 : 2 * snap),
21965 zIndex: 2
21966 })
21967 .add(series.group);
21968
21969 // The tracker is added to the series group, which is clipped, but is covered
21970 // by the marker group. So the marker group also needs to capture events.
21971 each([series.tracker, series.markerGroup], function(tracker) {
21972 tracker.addClass('highcharts-tracker')
21973 .on('mouseover', onMouseOver)
21974 .on('mouseout', function(e) {
21975 pointer.onTrackerMouseOut(e);
21976 });
21977
21978
21979 if (options.cursor) {
21980 tracker.css({
21981 cursor: options.cursor
21982 });
21983 }
21984
21985
21986 if (hasTouch) {
21987 tracker.on('touchstart', onMouseOver);
21988 }
21989 });
21990 }
21991 }
21992 };
21993 /* End TrackerMixin */
21994
21995
21996 /**
21997 * Add tracking event listener to the series group, so the point graphics
21998 * themselves act as trackers
21999 */
22000
22001 if (seriesTypes.column) {
22002 seriesTypes.column.prototype.drawTracker = TrackerMixin.drawTrackerPoint;
22003 }
22004
22005 if (seriesTypes.pie) {
22006 seriesTypes.pie.prototype.drawTracker = TrackerMixin.drawTrackerPoint;
22007 }
22008
22009 if (seriesTypes.scatter) {
22010 seriesTypes.scatter.prototype.drawTracker = TrackerMixin.drawTrackerPoint;
22011 }
22012
22013 /*
22014 * Extend Legend for item events
22015 */
22016 extend(Legend.prototype, {
22017
22018 setItemEvents: function(item, legendItem, useHTML) {
22019 var legend = this,
22020 chart = legend.chart,
22021 activeClass = 'highcharts-legend-' + (item.series ? 'point' : 'series') + '-active';
22022
22023 // Set the events on the item group, or in case of useHTML, the item itself (#1249)
22024 (useHTML ? legendItem : item.legendGroup).on('mouseover', function() {
22025 item.setState('hover');
22026
22027 // A CSS class to dim or hide other than the hovered series
22028 chart.seriesGroup.addClass(activeClass);
22029
22030
22031 legendItem.css(legend.options.itemHoverStyle);
22032
22033 })
22034 .on('mouseout', function() {
22035
22036 legendItem.css(item.visible ? legend.itemStyle : legend.itemHiddenStyle);
22037
22038
22039 // A CSS class to dim or hide other than the hovered series
22040 chart.seriesGroup.removeClass(activeClass);
22041
22042 item.setState();
22043 })
22044 .on('click', function(event) {
22045 var strLegendItemClick = 'legendItemClick',
22046 fnLegendItemClick = function() {
22047 if (item.setVisible) {
22048 item.setVisible();
22049 }
22050 };
22051
22052 // Pass over the click/touch event. #4.
22053 event = {
22054 browserEvent: event
22055 };
22056
22057 // click the name or symbol
22058 if (item.firePointEvent) { // point
22059 item.firePointEvent(strLegendItemClick, event, fnLegendItemClick);
22060 } else {
22061 fireEvent(item, strLegendItemClick, event, fnLegendItemClick);
22062 }
22063 });
22064 },
22065
22066 createCheckboxForItem: function(item) {
22067 var legend = this;
22068
22069 item.checkbox = createElement('input', {
22070 type: 'checkbox',
22071 checked: item.selected,
22072 defaultChecked: item.selected // required by IE7
22073 }, legend.options.itemCheckboxStyle, legend.chart.container);
22074
22075 addEvent(item.checkbox, 'click', function(event) {
22076 var target = event.target;
22077 fireEvent(
22078 item.series || item,
22079 'checkboxClick', { // #3712
22080 checked: target.checked,
22081 item: item
22082 },
22083 function() {
22084 item.select();
22085 }
22086 );
22087 });
22088 }
22089 });
22090
22091
22092
22093 // Add pointer cursor to legend itemstyle in defaultOptions
22094 defaultOptions.legend.itemStyle.cursor = 'pointer';
22095
22096
22097
22098 /*
22099 * Extend the Chart object with interaction
22100 */
22101
22102 extend(Chart.prototype, /** @lends Chart.prototype */ {
22103 /**
22104 * Display the zoom button
22105 */
22106 showResetZoom: function() {
22107 var chart = this,
22108 lang = defaultOptions.lang,
22109 btnOptions = chart.options.chart.resetZoomButton,
22110 theme = btnOptions.theme,
22111 states = theme.states,
22112 alignTo = btnOptions.relativeTo === 'chart' ? null : 'plotBox';
22113
22114 function zoomOut() {
22115 chart.zoomOut();
22116 }
22117
22118 this.resetZoomButton = chart.renderer.button(lang.resetZoom, null, null, zoomOut, theme, states && states.hover)
22119 .attr({
22120 align: btnOptions.position.align,
22121 title: lang.resetZoomTitle
22122 })
22123 .addClass('highcharts-reset-zoom')
22124 .add()
22125 .align(btnOptions.position, false, alignTo);
22126
22127 },
22128
22129 /**
22130 * Zoom out to 1:1
22131 */
22132 zoomOut: function() {
22133 var chart = this;
22134 fireEvent(chart, 'selection', {
22135 resetSelection: true
22136 }, function() {
22137 chart.zoom();
22138 });
22139 },
22140
22141 /**
22142 * Zoom into a given portion of the chart given by axis coordinates
22143 * @param {Object} event
22144 */
22145 zoom: function(event) {
22146 var chart = this,
22147 hasZoomed,
22148 pointer = chart.pointer,
22149 displayButton = false,
22150 resetZoomButton;
22151
22152 // If zoom is called with no arguments, reset the axes
22153 if (!event || event.resetSelection) {
22154 each(chart.axes, function(axis) {
22155 hasZoomed = axis.zoom();
22156 });
22157 } else { // else, zoom in on all axes
22158 each(event.xAxis.concat(event.yAxis), function(axisData) {
22159 var axis = axisData.axis,
22160 isXAxis = axis.isXAxis;
22161
22162 // don't zoom more than minRange
22163 if (pointer[isXAxis ? 'zoomX' : 'zoomY']) {
22164 hasZoomed = axis.zoom(axisData.min, axisData.max);
22165 if (axis.displayBtn) {
22166 displayButton = true;
22167 }
22168 }
22169 });
22170 }
22171
22172 // Show or hide the Reset zoom button
22173 resetZoomButton = chart.resetZoomButton;
22174 if (displayButton && !resetZoomButton) {
22175 chart.showResetZoom();
22176 } else if (!displayButton && isObject(resetZoomButton)) {
22177 chart.resetZoomButton = resetZoomButton.destroy();
22178 }
22179
22180
22181 // Redraw
22182 if (hasZoomed) {
22183 chart.redraw(
22184 pick(chart.options.chart.animation, event && event.animation, chart.pointCount < 100) // animation
22185 );
22186 }
22187 },
22188
22189 /**
22190 * Pan the chart by dragging the mouse across the pane. This function is called
22191 * on mouse move, and the distance to pan is computed from chartX compared to
22192 * the first chartX position in the dragging operation.
22193 */
22194 pan: function(e, panning) {
22195
22196 var chart = this,
22197 hoverPoints = chart.hoverPoints,
22198 doRedraw;
22199
22200 // remove active points for shared tooltip
22201 if (hoverPoints) {
22202 each(hoverPoints, function(point) {
22203 point.setState();
22204 });
22205 }
22206
22207 each(panning === 'xy' ? [1, 0] : [1], function(isX) { // xy is used in maps
22208 var axis = chart[isX ? 'xAxis' : 'yAxis'][0],
22209 horiz = axis.horiz,
22210 reversed = axis.reversed,
22211 mousePos = e[horiz ? 'chartX' : 'chartY'],
22212 mouseDown = horiz ? 'mouseDownX' : 'mouseDownY',
22213 startPos = chart[mouseDown],
22214 halfPointRange = (axis.pointRange || 0) / (reversed ? -2 : 2),
22215 extremes = axis.getExtremes(),
22216 newMin = axis.toValue(startPos - mousePos, true) + halfPointRange,
22217 newMax = axis.toValue(startPos + axis.len - mousePos, true) - halfPointRange,
22218 goingLeft = startPos > mousePos, // #3613
22219 tmp;
22220
22221 // Swap min/max for reversed axes (#5997)
22222 if (reversed) {
22223 goingLeft = !goingLeft;
22224 tmp = newMin;
22225 newMin = newMax;
22226 newMax = tmp;
22227 }
22228
22229 if (axis.series.length &&
22230 (goingLeft || newMin > Math.min(extremes.dataMin, extremes.min)) &&
22231 (!goingLeft || newMax < Math.max(extremes.dataMax, extremes.max))) {
22232 axis.setExtremes(newMin, newMax, false, false, {
22233 trigger: 'pan'
22234 });
22235 doRedraw = true;
22236 }
22237
22238 chart[mouseDown] = mousePos; // set new reference for next run
22239 });
22240
22241 if (doRedraw) {
22242 chart.redraw(false);
22243 }
22244 css(chart.container, {
22245 cursor: 'move'
22246 });
22247 }
22248 });
22249
22250 /*
22251 * Extend the Point object with interaction
22252 */
22253 extend(Point.prototype, /** @lends Point.prototype */ {
22254 /**
22255 * Toggle the selection status of a point
22256 * @param {Boolean} selected Whether to select or unselect the point.
22257 * @param {Boolean} accumulate Whether to add to the previous selection. By default,
22258 * this happens if the control key (Cmd on Mac) was pressed during clicking.
22259 */
22260 select: function(selected, accumulate) {
22261 var point = this,
22262 series = point.series,
22263 chart = series.chart;
22264
22265 selected = pick(selected, !point.selected);
22266
22267 // fire the event with the default handler
22268 point.firePointEvent(selected ? 'select' : 'unselect', {
22269 accumulate: accumulate
22270 }, function() {
22271 point.selected = point.options.selected = selected;
22272 series.options.data[inArray(point, series.data)] = point.options;
22273
22274 point.setState(selected && 'select');
22275
22276 // unselect all other points unless Ctrl or Cmd + click
22277 if (!accumulate) {
22278 each(chart.getSelectedPoints(), function(loopPoint) {
22279 if (loopPoint.selected && loopPoint !== point) {
22280 loopPoint.selected = loopPoint.options.selected = false;
22281 series.options.data[inArray(loopPoint, series.data)] = loopPoint.options;
22282 loopPoint.setState('');
22283 loopPoint.firePointEvent('unselect');
22284 }
22285 });
22286 }
22287 });
22288 },
22289
22290 /**
22291 * Runs on mouse over the point
22292 *
22293 * @param {Object} e The event arguments
22294 * @param {Boolean} byProximity Falsy for kd points that are closest to the mouse, or to
22295 * actually hovered points. True for other points in shared tooltip.
22296 */
22297 onMouseOver: function(e, byProximity) {
22298 var point = this,
22299 series = point.series,
22300 chart = series.chart,
22301 tooltip = chart.tooltip,
22302 hoverPoint = chart.hoverPoint;
22303
22304 if (point.series) { // It may have been destroyed, #4130
22305 // In shared tooltip, call mouse over when point/series is actually hovered: (#5766)
22306 if (!byProximity) {
22307 // set normal state to previous series
22308 if (hoverPoint && hoverPoint !== point) {
22309 hoverPoint.onMouseOut();
22310 }
22311 if (chart.hoverSeries !== series) {
22312 series.onMouseOver();
22313 }
22314 chart.hoverPoint = point;
22315 }
22316
22317 // update the tooltip
22318 if (tooltip && (!tooltip.shared || series.noSharedTooltip)) {
22319 // hover point only for non shared points: (#5766)
22320 point.setState('hover');
22321 tooltip.refresh(point, e);
22322 } else if (!tooltip) {
22323 point.setState('hover');
22324 }
22325
22326 // trigger the event
22327 point.firePointEvent('mouseOver');
22328 }
22329 },
22330
22331 /**
22332 * Runs on mouse out from the point
22333 */
22334 onMouseOut: function() {
22335 var chart = this.series.chart,
22336 hoverPoints = chart.hoverPoints;
22337
22338 this.firePointEvent('mouseOut');
22339
22340 if (!hoverPoints || inArray(this, hoverPoints) === -1) { // #887, #2240
22341 this.setState();
22342 chart.hoverPoint = null;
22343 }
22344 },
22345
22346 /**
22347 * Import events from the series' and point's options. Only do it on
22348 * demand, to save processing time on hovering.
22349 */
22350 importEvents: function() {
22351 if (!this.hasImportedEvents) {
22352 var point = this,
22353 options = merge(point.series.options.point, point.options),
22354 events = options.events,
22355 eventType;
22356
22357 point.events = events;
22358
22359 for (eventType in events) {
22360 addEvent(point, eventType, events[eventType]);
22361 }
22362 this.hasImportedEvents = true;
22363
22364 }
22365 },
22366
22367 /**
22368 * Set the point's state
22369 * @param {String} state
22370 */
22371 setState: function(state, move) {
22372 var point = this,
22373 plotX = Math.floor(point.plotX), // #4586
22374 plotY = point.plotY,
22375 series = point.series,
22376 stateOptions = series.options.states[state] || {},
22377 markerOptions = defaultPlotOptions[series.type].marker &&
22378 series.options.marker,
22379 normalDisabled = markerOptions && markerOptions.enabled === false,
22380 markerStateOptions = (markerOptions && markerOptions.states &&
22381 markerOptions.states[state]) || {},
22382 stateDisabled = markerStateOptions.enabled === false,
22383 stateMarkerGraphic = series.stateMarkerGraphic,
22384 pointMarker = point.marker || {},
22385 chart = series.chart,
22386 halo = series.halo,
22387 haloOptions,
22388 markerAttribs,
22389 hasMarkers = markerOptions && series.markerAttribs,
22390 newSymbol;
22391
22392 state = state || ''; // empty string
22393
22394 if (
22395 // already has this state
22396 (state === point.state && !move) ||
22397 // selected points don't respond to hover
22398 (point.selected && state !== 'select') ||
22399 // series' state options is disabled
22400 (stateOptions.enabled === false) ||
22401 // general point marker's state options is disabled
22402 (state && (stateDisabled || (normalDisabled && markerStateOptions.enabled === false))) ||
22403 // individual point marker's state options is disabled
22404 (state && pointMarker.states && pointMarker.states[state] && pointMarker.states[state].enabled === false) // #1610
22405
22406 ) {
22407 return;
22408 }
22409
22410 if (hasMarkers) {
22411 markerAttribs = series.markerAttribs(point, state);
22412 }
22413
22414 // Apply hover styles to the existing point
22415 if (point.graphic) {
22416
22417 if (point.state) {
22418 point.graphic.removeClass('highcharts-point-' + point.state);
22419 }
22420 if (state) {
22421 point.graphic.addClass('highcharts-point-' + state);
22422 }
22423
22424 /*attribs = radius ? { // new symbol attributes (#507, #612)
22425 x: plotX - radius,
22426 y: plotY - radius,
22427 width: 2 * radius,
22428 height: 2 * radius
22429 } : {};*/
22430
22431
22432 //attribs = merge(series.pointAttribs(point, state), attribs);
22433 point.graphic.attr(series.pointAttribs(point, state));
22434
22435
22436 if (markerAttribs) {
22437 point.graphic.animate(
22438 markerAttribs,
22439 pick(
22440 chart.options.chart.animation, // Turn off globally
22441 markerStateOptions.animation,
22442 markerOptions.animation
22443 )
22444 );
22445 }
22446
22447 // Zooming in from a range with no markers to a range with markers
22448 if (stateMarkerGraphic) {
22449 stateMarkerGraphic.hide();
22450 }
22451 } else {
22452 // if a graphic is not applied to each point in the normal state, create a shared
22453 // graphic for the hover state
22454 if (state && markerStateOptions) {
22455 newSymbol = pointMarker.symbol || series.symbol;
22456
22457 // If the point has another symbol than the previous one, throw away the
22458 // state marker graphic and force a new one (#1459)
22459 if (stateMarkerGraphic && stateMarkerGraphic.currentSymbol !== newSymbol) {
22460 stateMarkerGraphic = stateMarkerGraphic.destroy();
22461 }
22462
22463 // Add a new state marker graphic
22464 if (!stateMarkerGraphic) {
22465 if (newSymbol) {
22466 series.stateMarkerGraphic = stateMarkerGraphic = chart.renderer.symbol(
22467 newSymbol,
22468 markerAttribs.x,
22469 markerAttribs.y,
22470 markerAttribs.width,
22471 markerAttribs.height
22472 )
22473 .add(series.markerGroup);
22474 stateMarkerGraphic.currentSymbol = newSymbol;
22475 }
22476
22477 // Move the existing graphic
22478 } else {
22479 stateMarkerGraphic[move ? 'animate' : 'attr']({ // #1054
22480 x: markerAttribs.x,
22481 y: markerAttribs.y
22482 });
22483 }
22484
22485 if (stateMarkerGraphic) {
22486 stateMarkerGraphic.attr(series.pointAttribs(point, state));
22487 }
22488
22489 }
22490
22491 if (stateMarkerGraphic) {
22492 stateMarkerGraphic[state && chart.isInsidePlot(plotX, plotY, chart.inverted) ? 'show' : 'hide'](); // #2450
22493 stateMarkerGraphic.element.point = point; // #4310
22494 }
22495 }
22496
22497 // Show me your halo
22498 haloOptions = stateOptions.halo;
22499 if (haloOptions && haloOptions.size) {
22500 if (!halo) {
22501 series.halo = halo = chart.renderer.path()
22502 // #5818, #5903
22503 .add(hasMarkers ? series.markerGroup : series.group);
22504 }
22505 halo[move ? 'animate' : 'attr']({
22506 d: point.haloPath(haloOptions.size)
22507 });
22508 halo.attr({
22509 'class': 'highcharts-halo highcharts-color-' + pick(point.colorIndex, series.colorIndex)
22510 });
22511
22512
22513 halo.attr(extend({
22514 'fill': point.color || series.color,
22515 'fill-opacity': haloOptions.opacity,
22516 'zIndex': -1 // #4929, IE8 added halo above everything
22517 }, haloOptions.attributes));
22518
22519 } else if (halo) {
22520 halo.animate({
22521 d: point.haloPath(0)
22522 }); // Hide
22523 }
22524
22525 point.state = state;
22526 },
22527
22528 /**
22529 * Get the circular path definition for the halo
22530 * @param {Number} size The radius of the circular halo.
22531 * @returns {Array} The path definition
22532 */
22533 haloPath: function(size) {
22534 var series = this.series,
22535 chart = series.chart;
22536
22537 return chart.renderer.symbols.circle(
22538 Math.floor(this.plotX) - size,
22539 this.plotY - size,
22540 size * 2,
22541 size * 2
22542 );
22543 }
22544 });
22545
22546 /*
22547 * Extend the Series object with interaction
22548 */
22549
22550 extend(Series.prototype, /** @lends Series.prototype */ {
22551 /**
22552 * Series mouse over handler
22553 */
22554 onMouseOver: function() {
22555 var series = this,
22556 chart = series.chart,
22557 hoverSeries = chart.hoverSeries;
22558
22559 // set normal state to previous series
22560 if (hoverSeries && hoverSeries !== series) {
22561 hoverSeries.onMouseOut();
22562 }
22563
22564 // trigger the event, but to save processing time,
22565 // only if defined
22566 if (series.options.events.mouseOver) {
22567 fireEvent(series, 'mouseOver');
22568 }
22569
22570 // hover this
22571 series.setState('hover');
22572 chart.hoverSeries = series;
22573 },
22574
22575 /**
22576 * Series mouse out handler
22577 */
22578 onMouseOut: function() {
22579 // trigger the event only if listeners exist
22580 var series = this,
22581 options = series.options,
22582 chart = series.chart,
22583 tooltip = chart.tooltip,
22584 hoverPoint = chart.hoverPoint;
22585
22586 chart.hoverSeries = null; // #182, set to null before the mouseOut event fires
22587
22588 // trigger mouse out on the point, which must be in this series
22589 if (hoverPoint) {
22590 hoverPoint.onMouseOut();
22591 }
22592
22593 // fire the mouse out event
22594 if (series && options.events.mouseOut) {
22595 fireEvent(series, 'mouseOut');
22596 }
22597
22598
22599 // hide the tooltip
22600 if (tooltip && !options.stickyTracking && (!tooltip.shared || series.noSharedTooltip)) {
22601 tooltip.hide();
22602 }
22603
22604 // set normal state
22605 series.setState();
22606 },
22607
22608 /**
22609 * Set the state of the graph
22610 */
22611 setState: function(state) {
22612 var series = this,
22613 options = series.options,
22614 graph = series.graph,
22615 stateOptions = options.states,
22616 lineWidth = options.lineWidth,
22617 attribs,
22618 i = 0;
22619
22620 state = state || '';
22621
22622 if (series.state !== state) {
22623
22624 // Toggle class names
22625 each([series.group, series.markerGroup], function(group) {
22626 if (group) {
22627 // Old state
22628 if (series.state) {
22629 group.removeClass('highcharts-series-' + series.state);
22630 }
22631 // New state
22632 if (state) {
22633 group.addClass('highcharts-series-' + state);
22634 }
22635 }
22636 });
22637
22638 series.state = state;
22639
22640
22641
22642 if (stateOptions[state] && stateOptions[state].enabled === false) {
22643 return;
22644 }
22645
22646 if (state) {
22647 lineWidth = stateOptions[state].lineWidth || lineWidth + (stateOptions[state].lineWidthPlus || 0); // #4035
22648 }
22649
22650 if (graph && !graph.dashstyle) { // hover is turned off for dashed lines in VML
22651 attribs = {
22652 'stroke-width': lineWidth
22653 };
22654 // use attr because animate will cause any other animation on the graph to stop
22655 graph.attr(attribs);
22656 while (series['zone-graph-' + i]) {
22657 series['zone-graph-' + i].attr(attribs);
22658 i = i + 1;
22659 }
22660 }
22661
22662 }
22663 },
22664
22665 /**
22666 * Set the visibility of the graph
22667 *
22668 * @param vis {Boolean} True to show the series, false to hide. If undefined,
22669 * the visibility is toggled.
22670 */
22671 setVisible: function(vis, redraw) {
22672 var series = this,
22673 chart = series.chart,
22674 legendItem = series.legendItem,
22675 showOrHide,
22676 ignoreHiddenSeries = chart.options.chart.ignoreHiddenSeries,
22677 oldVisibility = series.visible;
22678
22679 // if called without an argument, toggle visibility
22680 series.visible = vis = series.options.visible = series.userOptions.visible = vis === undefined ? !oldVisibility : vis; // #5618
22681 showOrHide = vis ? 'show' : 'hide';
22682
22683 // show or hide elements
22684 each(['group', 'dataLabelsGroup', 'markerGroup', 'tracker', 'tt'], function(key) {
22685 if (series[key]) {
22686 series[key][showOrHide]();
22687 }
22688 });
22689
22690
22691 // hide tooltip (#1361)
22692 if (chart.hoverSeries === series || (chart.hoverPoint && chart.hoverPoint.series) === series) {
22693 series.onMouseOut();
22694 }
22695
22696
22697 if (legendItem) {
22698 chart.legend.colorizeItem(series, vis);
22699 }
22700
22701
22702 // rescale or adapt to resized chart
22703 series.isDirty = true;
22704 // in a stack, all other series are affected
22705 if (series.options.stacking) {
22706 each(chart.series, function(otherSeries) {
22707 if (otherSeries.options.stacking && otherSeries.visible) {
22708 otherSeries.isDirty = true;
22709 }
22710 });
22711 }
22712
22713 // show or hide linked series
22714 each(series.linkedSeries, function(otherSeries) {
22715 otherSeries.setVisible(vis, false);
22716 });
22717
22718 if (ignoreHiddenSeries) {
22719 chart.isDirtyBox = true;
22720 }
22721 if (redraw !== false) {
22722 chart.redraw();
22723 }
22724
22725 fireEvent(series, showOrHide);
22726 },
22727
22728 /**
22729 * Show the graph
22730 */
22731 show: function() {
22732 this.setVisible(true);
22733 },
22734
22735 /**
22736 * Hide the graph
22737 */
22738 hide: function() {
22739 this.setVisible(false);
22740 },
22741
22742
22743 /**
22744 * Set the selected state of the graph
22745 *
22746 * @param selected {Boolean} True to select the series, false to unselect. If
22747 * undefined, the selection state is toggled.
22748 */
22749 select: function(selected) {
22750 var series = this;
22751 // if called without an argument, toggle
22752 series.selected = selected = (selected === undefined) ? !series.selected : selected;
22753
22754 if (series.checkbox) {
22755 series.checkbox.checked = selected;
22756 }
22757
22758 fireEvent(series, selected ? 'select' : 'unselect');
22759 },
22760
22761 drawTracker: TrackerMixin.drawTrackerGraph
22762 });
22763
22764 }(Highcharts));
22765 (function(H) {
22766 /**
22767 * (c) 2010-2016 Torstein Honsi
22768 *
22769 * License: www.highcharts.com/license
22770 */
22771 'use strict';
22772 var Chart = H.Chart,
22773 each = H.each,
22774 inArray = H.inArray,
22775 isObject = H.isObject,
22776 pick = H.pick,
22777 splat = H.splat;
22778
22779 /**
22780 * Update the chart based on the current chart/document size and options for responsiveness
22781 */
22782 Chart.prototype.setResponsive = function(redraw) {
22783 var options = this.options.responsive;
22784
22785 if (options && options.rules) {
22786 each(options.rules, function(rule) {
22787 this.matchResponsiveRule(rule, redraw);
22788 }, this);
22789 }
22790 };
22791
22792 /**
22793 * Handle a single responsiveness rule
22794 */
22795 Chart.prototype.matchResponsiveRule = function(rule, redraw) {
22796 var respRules = this.respRules,
22797 condition = rule.condition,
22798 matches,
22799 fn = condition.callback || function() {
22800 return this.chartWidth <= pick(condition.maxWidth, Number.MAX_VALUE) &&
22801 this.chartHeight <= pick(condition.maxHeight, Number.MAX_VALUE) &&
22802 this.chartWidth >= pick(condition.minWidth, 0) &&
22803 this.chartHeight >= pick(condition.minHeight, 0);
22804 };
22805
22806
22807 if (rule._id === undefined) {
22808 rule._id = H.uniqueKey();
22809 }
22810 matches = fn.call(this);
22811
22812 // Apply a rule
22813 if (!respRules[rule._id] && matches) {
22814
22815 // Store the current state of the options
22816 if (rule.chartOptions) {
22817 respRules[rule._id] = this.currentOptions(rule.chartOptions);
22818 this.update(rule.chartOptions, redraw);
22819 }
22820
22821 // Unapply a rule based on the previous options before the rule
22822 // was applied
22823 } else if (respRules[rule._id] && !matches) {
22824 this.update(respRules[rule._id], redraw);
22825 delete respRules[rule._id];
22826 }
22827 };
22828
22829 /**
22830 * Get the current values for a given set of options. Used before we update
22831 * the chart with a new responsiveness rule.
22832 * TODO: Restore axis options (by id?)
22833 */
22834 Chart.prototype.currentOptions = function(options) {
22835
22836 var ret = {};
22837
22838 /**
22839 * Recurse over a set of options and its current values,
22840 * and store the current values in the ret object.
22841 */
22842 function getCurrent(options, curr, ret) {
22843 var key, i;
22844 for (key in options) {
22845 if (inArray(key, ['series', 'xAxis', 'yAxis']) > -1) {
22846 options[key] = splat(options[key]);
22847
22848 ret[key] = [];
22849 for (i = 0; i < options[key].length; i++) {
22850 ret[key][i] = {};
22851 getCurrent(options[key][i], curr[key][i], ret[key][i]);
22852 }
22853 } else if (isObject(options[key])) {
22854 ret[key] = {};
22855 getCurrent(options[key], curr[key] || {}, ret[key]);
22856 } else {
22857 ret[key] = curr[key] || null;
22858 }
22859 }
22860 }
22861
22862 getCurrent(options, this.options, ret);
22863 return ret;
22864 };
22865
22866 }(Highcharts));
22867 return Highcharts
22868}));